I have a parent that is getting data from a database by subscribing to an observable.
My problem is that I have a child component that is using the data from the parent component in its ngOnInit method. That creates an error since the data is not there yet when the child component is rendered.
In the following, I get the error because "disciplines", which I got from my observable hadn't been returned yet. But the child was rendered and the ngOnInit was accessing an empty array at this point.
This works fine if I hardcode disciplines with a value instead of waiting for the observable to return the data.
So the question is, how to make sure the data is in my variable before the ngOnInit of the child component accesses it.
In the Parent I have this in the ngOnInit method:
this.disciplines = [];
let getOptions = this.service.getOptions().subscribe((data: any) => {
this.populateOptions(data);
});
...
populateOptions(data: any): void {
this.disciplines = data['disciplines'];
}
In the parents template
<cmp-child
[disciplines]="disciplines"
</cmp-child>
In the ngOnInit of the child component,
ngOnInit(): void {
this.populateOptions()
}
populateOptions(): void {
this.advanced = this.disciplines.filter(d => d.isAdvancedDiscipline === true).map(m => m.id);
console.log("disciplines: ", this.disciplines);
}
Thanks,
Tom
CodePudding user response:
You could change the parent's template to render the child component only if discipline has value:
<cmp-child *ngIf="disciplines"
[disciplines]="disciplines"
</cmp-child>
This way, your child component is only instantiated and initialized when discipline has value. It also means that your child component gets destroyed when discipline is set back to null or undefined.
CodePudding user response:
Instead of relying on ngOnInit lifecycle hook, you should be reacting in ngOnChanges hook
public ngOnChanges(changes: SimpleChanges): void {
const disciplinesChanged = 'disciplines' in changes;
if(disciplinesChanged){
this.populateOptions()
}
}
CodePudding user response:
Use NgOnChange.
ngOnChanges(changes: SimpleChanges) {
if(changes.disciplines && changes.disciplines.currentValue) {
this.reports = changes.disciplines.currentValue;
}
populateOptions(): void {
this.advanced = this.disciplines.filter(d => d.isAdvancedDiscipline ===
true).map(m => m.id);
console.log("disciplines: ", this.disciplines);
}
And in your NgOnInit you can also put an if check to avoid the issue of running the code when it is undefined.
if(this.disciplines)
//call populate
Way this works is that at first your code will try to execute what is available in its OnInit method as you noticed it is possible results may still be undefined. The OnChange however will be fired again once the underlying input data has been changed.
Another possible approach is to show some sort of loading indicator in your parent component. You can achieve this using either a full implementation of a store with reducers and the like, or just do it within the parent component.
In your parent.ts file:
//By convention observables have a $ appended at the end of their variable name
loading$ = new BehaviorSubject(true)
disciplines$ = new BehaviorSubject(YourDisiciplineObject[]);
OnInit() {
let getOptions = this.service.getOptions().subscribe((data: any) => {
loading$.next(false); //finished loading
this.populateOptions(data);
});
}
populateOptions(data: any): void {
disciplines$.next(data['disciplines')
this.disciplines = data['disciplines'];
}
In your front end you can then using angular async pipes (here we've added a loading input which is of type boolean)
<cmp-child
[loading] = "(loading$ | async)"
[disciplines]="(disciplines$ | async)"
</cmp-child>
The beauty of async pipes is that is that it will actually update the results as normal once some data is returned or no data depending on the end result of the API call. It is best paired when using the redux pattern (in my opinion), but it does handle the observable for you. No need to subscribe to it or anything like that.
