Search code examples
angularangular-componentsangular7angular-changedetection

How to update child components after receiving data from an ajax call in parent


my app structure is as below, my question is how to update children components view on receiving initial or future data, imagine I have only one service which has an event OnDataUpdate, all the child components are receiving the same instance of the service since it has declared in App module providers section, on the other hand, I have tried all these ways & did not work:

  1. ApplicationRef.tick()
  2. ChangeDetectionRef.markForCheck()
  3. ChangeDetectionStrategy
  4. Shared service among components with the OnDataRecieved event which is like this

@Injectable()
export class ApiService {

  public OnDataRecieved: EventEmitter<Model>  = new EventEmitter<Model>();

  constructor(private http: HttpClient, private ngZone: NgZone) {
  }

  public getDataAsync(): Observable<Model> {
      return this.http
        .get<Model>('url')
        .pipe(catchError(er => throwError(er)));
    }
}

and in App root component this is like below code

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
  changeDetection: ChangeDetectionStrategy.Default
})
export class AppComponent implements DoCheck {

  model: BehaviorSubject<Model> = new BehaviorSubject<Model>(new Model()); //with default values
  subModel: BehaviorSubject<SubModel>; 


  constructor(private apiService: ApiService,
    private zone: NgZone) {

    this.apiService.getDashboard().subscribe((data) => {
      this.zone.run(() => {
          this.apiService.OnDataReceived.emit(data);
          this.model = new BehaviorSubject<Model>(data);
      });
    });

    this.model.subscribe((mdl) => {
      this.subModel = new BehaviorSubject<SubModel>(mdl.subModel));
    });
  }

  ngDoCheck() {
  }
}

imagine the model is nested and propagated through the child components as data is loaded or changed, the structure can be like this

 __ AppRootComponent
|_____ Component1
|_________SubCompoent1-1
|_________SubCompoent1-2
|_____ Component2
|_________SubCompoent2-1
|____________SubCompoent2-1-1

I receive the data changes in ngDoCheck, no need to trigger the detect changes, but the UI and child components does not get updated!


Solution

  • i realized how to solve that issue, the structure of that components is hierarchical and I passed the model of each component through @Input(), the problem is the initial request is async and the components are rendered before receiving the real parent object and after receiving the parent object from server there is no object reference for the passed Input objects, so they will not get the changes.

    so, how can we solve the issue? simple! remove all inputs and use event-driven programming How? create an event for each object or one event for the parent (root) object which all other objects depend on, share event in global service, trigger/emit event once you receive the root object, and subscribe to that event in child components. let me show you a simple snippet at below:

    import { HttpClient, HttpParams, HttpErrorResponse } from '@angular/common/http';
    import { Injectable, EventEmitter } from '@angular/core';
    import { Observable, throwError } from 'rxjs';
    import { catchError } from 'rxjs/operators';
    
    import { RootDto } from 'src/app/model/root.dto.model';
    
    @Injectable()
    export class CoreApiService {
    
      public onDataReceived: EventEmitter<RootDto> = new EventEmitter<RootDto>();
    
      constructor(private http: HttpClient) {
      }
    
      public getRootObject(objectId: number): Observable<RootDto> {
         // const _params = new HttpParams().set('objectId', objectId);
          return this.http
            .get<RootDto>(`${Constants.ApiUrl}/root/${objectId}`)
            .pipe(catchError((err: HttpErrorResponse) => {
              return throwError(err);
            }));
        }
    }

    the root componant is like below

    import {
      Component,
      OnInit
    } from '@angular/core';
    
    import { CoreApiService } from './core/services/core-api.service';
    import { RootDto } from 'src/app/model/root.dto.model';
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })
    export class AppComponent implements OnInit {
    
      constructor(private apiService: CoreApiService) {
    
      }
    
      ngOnInit() {
        this.apiService.getRootObject().subscribe((data: RootDto) => {
    			// todo: do something here
              this.apiService.onDataReceived.emit(data);
            },
            (err: HttpErrorResponse) => {
              if (err.status === 401 || err.status === 403) {
                // not authorized
              }else {
              // todo: error happened!
    		  }
            }
          );
      }
    }

    the child components are like below

    import {
      Component,
      OnInit,
      NgZone
    } from '@angular/core';
    
    import { CoreApiService } from '../core/services/core-api.service';
    import { RootDto } from 'src/app/model/root.dto.model';
    import { ChildDto } from '../model/child.dto.model';
    
    @Component({
      selector: 'app-first-child',
      templateUrl: './firstChild.component.html',
      styleUrls: ['./firstChild.component.css']
    })
    export class FirstChildComponent implements OnInit {
    
      dto: ChildDto;
      isLoaded = false;
    
      constructor(private apiService: CoreApiService, private zone: NgZone) {
        this.apiService.onDataReceived.subscribe((rootDto: RootDto) => {
          this.zone.run(() => {
            this.dto = Utils.ObjectFactory.Create(rootDto.firstChildDto); // to make sure that we will have a new reference (so that change detction will be triggered) i use object instantiation
    		// NOTICE:
    		// for arrays don't simply assign or push new item to the array, because the reference is not changed the change detection is not triggered
    		// if the array size is small before assigning new value, you can simply empty (myArray = [];) the array otherwise don't do that
            this.isLoaded = true;
          });
        });
      }
    
      ngOnInit() {
      }
    
      // the rest of logic
    }

    you can do the same for all other components and even you can create more events in share service and trigger it as you wish