Link to home
Start Free TrialLog in
Avatar of Dev Me
Dev Me

asked on

Angular subscribe and passing data between components

I am new to Angular and using Angular 6 CLI. Problem I have is, I have a JSON object that I am getting data from http.get and then trying to parse out different JSON blocks in an object. Then I am passing these objects to respective components. I can see that in the subscribe block the data is populated correctly but outside subscribe block, those objects are 'undefined'.

So from any .ts file, the objects outside subscribe block is undefined. However, I could access data from template files that is from .html files.

for example,  below is where I am calling service from "Component A" so component A has "myappRoot", "myapp", "myproblem"..etc all objects of custom type defined. The custom type is basically interface which I am exporting from index.d.ts file. So objects are populated  inside subscribe block is fine. But if I do console.log(this.myproblem) it comes as undefined outside ngOnInit(). If I pass this object to "Component A HTML" file like {{ this.myproblem? | json}} it prints correctly. but if I pass it to some other component B like <app-component B [myproblem] = 'this.myproblme' ></app-component B> and try to do console.log(this.myproblem) in component B.ts file, it comes as undefined.  Is it due to async nature of observables? How do I solve this?

ngOnInit() {
    // lets  make a call and initialized the root object and children
    this.myproblemService.getmyproblem().subscribe(root => {
      this.myappRoot = root;
      this.myapp = root.myapp;
      this.myproblem = root.myapp.myproblem;
      this.notes = root.myapp.myproblem.Notes;
      this.files = root.myapp.myproblem.Files;
      this.auditTrails = root.myapp.myproblem.AuditTrail;
      this.serviceRequests = root.myapp.myproblem.ServiceRequests;
      this.relatedmyproblems = root.myapp.myproblem.Relatedmyproblems;
      this.foreignBugs = root.myapp.myproblem.ForeignBugs;
      this.discussions = root.myapp.myproblem.Discussions;
      this.myproblemFields = root.myapp.myproblem.Field;
    });

Open in new window

Avatar of Julian Hansen
Julian Hansen
Flag of South Africa image

Welcome to EE.
First point: we prefer to use code tags to encapsulate code in our posts - it makes your code easier to read and refer to. (I have done this for you: highlight code, click CODE button in the toolbar).

Secondly, the more information you provide the easier it will be to help you. In this case it would be useful to see the component definition.

On to your code and questions
But if I do console.log(this.myproblem) it comes as undefined outside ngOnInit().
This is correct behaviour. Subscribe is an asynchronous function that is called when the data from the service is ready. A console.log after this statement will run immediately - but before the subscribe fires, which relatively speaking, takes several more months to complete.

Secondly, why are you setting individual properties on an object. Why not define an object that accepts the result and then just assign it in the subscribe()?
ngOnInit() {
    // lets  make a call and initialized the root object and children
    this.myproblemService.getmyproblem().subscribe(root => {
       this.data = root;
    });

    // The console.log problem
    // This will not show anything because this runs a long long long long time
    // before the data is ready in the subscribe. 
    console.log(this.data);
})

Open in new window


In terms of passing the data to another component a couple of things here
1. Consider using a service in all your components to get the data you need. If the data is an Observable then consider (in your service) maintaining a BehaviorSubject version of the data in your service. This acts like an Observable but will return the last resolved result when bound. This way you can get your model from the service in all your components. The only time you pass data to a child component is when you want to give your child some state that is specific to the child function - rather than global state the child will need to act on.

2. There is no reason that passing your data to the child should not work as you have it. Basic implementation is
<my-component [data]="data"></my-component>
The in my-component.ts
import { Component, Input } from '@angular/core';

@Component({
  selector: 'my-component',
  templateUrl: './my-component.component.html',
  styleUrls: ['./my-component.component.css']
})
export class MyComponentComponent {
@Input() post;

mySelect: string;
  constructor() { }
}

Open in new window

And in the template
<div>
{{data.myappRoot}}
<div>

Open in new window


To provide any more assistance I would need to see more of your code.
Avatar of Dev Me
Dev Me

ASKER

Hi Julian,

  First of all, thanks for the reply and appreciate the suggestion about posting questions.  Coming back to you answers:

  1. I am relieved to have confirmation from the expert that "console.log(this.data)" won't show anything in subscribe.
  2.  When you said  "Consider using a service in all your components to get the data you need" , did you mean new HTTP service?  My component.service.ts makes HTTPClient call which will return Obeservable. Are you suggesting to have "BehaviorVersion" of the data in their? I am not much familiar with how "BehaviorVersion" works so pardon my ignorance.
3. Regarding the template behavior. Yes, templates are working fine. I don't have any issues showing data into child template. The issue is when I need to manipulate data which is shown in the child from child.component.ts. I don't see that data or have that data in child.component.ts and I believe that is because I am trying to find that data in child.component.ts's ngOnInit() method. I read it that I would have it on ngOnChanges event. And indeed I do.
   
   I am not sure which is right behavior. Manipulate data in ngOnChanges or use "BehaviorSubject" like you suggested to get data in ngOnInit.

    Is there a way not to render child component until the child data is available? Not sure if using *ngif in parent component will work or the right way to achieve this. Here is how my template is laid out.

app.component.html
 
 <my-parent-component></my-parent-component>

Open in new window


app-parent-component.html
<my-child-component-1 [childData-1]='parentRoot?childData-1'></my-child-component-1>
<my-child-component-2 [childData-2]='parentRoot?childData-2'></my-child-component-1>
.....and so on

Open in new window


app-parent.component.ts will call parent.service.ts.

parent.service.ts
return (this.http.get<RootObject>('api/...'));

Open in new window


parent.component.ts subscribes it as shown in the ngOnInit method in my first post. Here the reason that I am assigning those individual object properties is because those are "child-data-1" "child-data-2" etc and I want to directly pass those to child template but I guess I can access those from the "rootObject" directly in the template. Again not sure what the best practice is here.

Your guidance on all of the above questions will be really appreciated.
did you mean new HTTP service
No, I meant an Angular service. The service can obtain the data by HTTP on load and then return the data when requested. However, this depends on your data model. The current thinking is to have your model wrapped in a service (either a custom one or using a library like Redux) that then provides methods for getting various parts of the data out, or setting various bits of data in the model. Components get the data directly from the model. Passing data to a component would usually be something that is decided at run time like a user input - the child component would still act on the data model through the data service.

BehaviorVersion => BehaviorSubject - similar to an Observable except that when you first subscribe to it you get the last resolved value. A normal Observable will only return a value when it resolves (Calls .next()) - with BehaviourSubject even if the Observable resolved before you subscribe to it you still get its last value. This is useful when you want to create a service that makes an initial http call but then make that data available to other components that may be instantiated only after the original call completes.

Manipulate data in ngOnChanges or use "BehaviorSubject" like you suggested to get data in ngOnInit
.
It depends which direction you are taking. If you are sending an Observable to a child component then you will need to implement ngOnChanges - if you are getting the data from the model - then BehaviourSubject is the one to use to ensure the child gets the latest version.
Avatar of Dev Me

ASKER

Hmm..I just tried this in my child component after removing the child property initialization in subscribe call..that is

on parent component for ngOnInit
// lets  make a call and initialized the root object and children
    this.myproblemService.getmyproblem().subscribe(root => {
      this.myappRoot = root;
}

Open in new window


in parent template file
<div *ngIf='myappRoot?.myapp?.myProblem?.RelatedDefects'>
<app-related-defects [relatedDefects]=''myappRoot?.myapp?.myProblem?.RelatedDefects'></app-related-defects>
</div>

Open in new window


RelatedDefect.ts
@Component({
  selector: 'app-related-defects',
  templateUrl: './app-related-defects.html',
  styleUrls: ['./app-related-defects.css']
})
export class RelatedDefectComponent {
@Input() relatedDefects;

  constructor() { }
}

Open in new window


app-related-defect.html
<a href="relatedDefects" [ngClass]="{'icon-chevron-down': !isCollapsed , 'icon-chevron-up':isCollapsed}">
  Related Defects ({{relatedDefects?.RelatedDefect?.length || 0}})</a>
  {{relatedDefects | json}}

Open in new window


This is printing nothing. or printing null.

but as soon as I put the initialization back in the subscribe() for child component, app-related-defect.html shows data.

Wasn't expecting this. I was expecting to see the data. Any thoughts?
Avatar of Dev Me

ASKER

Disregard my last post. It was my bad.

I will check on your last pot and get back. I am not familiar with Angular Service. I am able to get data till template though. That is parent and child.

As you said, an event sends data to child.ts from child.html. So for now, I could live with that. I would still like to have data available in child component and I will need to know more about 'Angular services'.
Avatar of Dev Me

ASKER

Any example of the 'Angular Service' you are talking about will be appreciated.
ASKER CERTIFIED SOLUTION
Avatar of Julian Hansen
Julian Hansen
Flag of South Africa image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Avatar of Dev Me

ASKER

ok, kind of getting it. quick question though ...since you are having "async" pipes two places, does this app make two service calls? Trying to find out in real work if this will be making two HTTP calls?
The call to the server is only made once - in the constructor of the service.

So to answer your question - no only one HTTP call is made - you can confirm this by observing the console while the app is running.

The async pipe is just Angular's way of dealing with Observable data.
Avatar of Dev Me

ASKER

Ok thanks. I can mark this one as I have my answer. If I have any follow up question on this, what is the best way to address that. Post on the same thread of open up new question?
Open a new question and reference this one.

You can also post a comment here with a link to the new question.
Avatar of Dev Me

ASKER

Thanks a lot for your help.
Avatar of Dev Me

ASKER

Julian, helped clarify certain fundamental concepts and questions with example. That was awesome.
You are welcome.