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:
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
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);
}
}