angulartypescriptangular-changedetection

How to update parent HTML based on child value in Angular 11 without causing NG0100?


I am struggling to find a solution that works and does not throw a NG0100 error. https://angular.io/errors/NG0100

I have logic in my AppComponent to create child components when the header element is clicked. When that child component is created it kicks off a series of data calls that take some time to load. During that time I display a loading bar. Currently, a user is able to click the header element in the App HTML over and over, which is resulting in the selected child component being created and destroyed over and over again. This also has the effect of multiple data calls being made over and over again.

Ideally, I'd like to disable the click action on the parent component and change the opacity so the user knows it's disabled while loading. I did accomplish this in the following ways:

  1. static methods in the AppComponent and static variables
  2. @Input methods on the child component with Objects as input
  3. @Output methods on the child component with primitive values as output

However, all of these solutions threw the NG0100: Expression has changed after it was checked error in the console. I don't want to implement a hacky solution that works, but throws errors. What is the right way?

  certificationView: View = {
    shouldEnable: true,
    shouldRender: false
  }

  toggleViewShouldRender(view: View): void {
    if (view.shouldEnable) {
      view.shouldRender = !view.shouldRender;
    }
  }

app.component.ts

<div>
  <mat-card
    [ngClass]="{'disabled': !certificationView.shouldEnable}"
    (click)="toggleViewShouldRender(certificationView)"
  >
    Certifications
  </mat-card>
  <app-certifications *ngIf="certificationView.shouldRender"></app-certifications>
</div>

app.component.html

  ngOnInit(): void {
    this.facade.retrieve()
      .pipe(
        takeUntil(this.destroyed$),
        filter(state => !!state?.data)
      )
      .subscribe(state => {
        this.state = state;
      });
  }

child.component.ts

Note: I removed all the logic that was causing the error. Having a clean execution is more important to me than this feature. I was previously modifying the View.shouldEnable property inside the ngOnInit before subscribing to the data service as well as inside the subscription.

For example,

<div>
  <mat-card
    [ngClass]="{'disabled': !certificationView.shouldEnable}"
    (click)="toggleViewShouldRender(certificationView)"
  >
    Certifications
  </mat-card>
  <app-certifications 
    *ngIf="certificationView.shouldRender"
    [(shouldEnable)]="certificationView.shouldEnable"
  >
  </app-certifications>
</div>

app.component.html


  @Input shouldEnable: boolean;
  @Output shouldEnableChange = new EventEmitter<boolean>();

  ngOnInit(): void {
    // disable header in parent
    this.shouldEnableChange.emit(false); 

    // load data
    this.facade.retrieve()
      .pipe(
        takeUntil(this.destroyed$),
        filter(state => !!state?.data)
      )
      .subscribe(state => {
        this.state = state;
        
        // enable header in parent
        this.shouldEnableChange.emit(true); 
      });
  }

child.component.ts


Solution

  • You could forward the reference of the parent to the child component by using an InjectionToken. Then you can trigger methods or set properties on the parent from the child.

    First you would create an injection token I would put it in its own file such as tokens.ts.

    export const PARENT_COMPONENT = new InjectionToken<ParentComponent>('MyParentInjectionToken')
    

    Next you would provide the token on the parent component like this. It will then forward itself to its children (see next code block).

    @Comonent({
      providers: [{
        provide: PARENT_COMPONENT, 
        useExisting: forwardRef(() => ParentComponent)
      }]
    })
    export class ParentComponent {
      valueToSet = false;
      setValue(val: boolean) {
        this.valueToSet = val
      }
    }
    

    Here is where we @Inject() the parent into the constructor so we have access. Sometimes there isn't always a parent in which case you can use @Optional() @Inject().

    @Component({
      template: '<button (click)="doSomething()">Click Me</button>'
    })
    export class ChildComponent {
      constructor(
        @Inject(PARENT_COMPONENT) parentComponent: ParentComponent
      ){}
    
      doSomething() {
        this.parentCompnent.setValue(true);
      }
    }