Reputation: 44386
My underlying problem is I need to set an asynchronous function that alters an attribute of the host. Since @HostBinding has no asynchronous abilities, I set a instance variable on the component, bound that, and manually maintained that variable. To get it to work without the infamous ExpressionChangedAfterItHasBeenCheckedError, I needed to do this
@Component(...)
export class MyComponent implements OnChanges {
constructor(private readonly cdr: ChangeDetectorRef) {}
@Input() inputObservable: Observable<boolean>;
@HostBinding('class.some-class') private setCssClass: boolean;
ngOnChanges() {
this.inputObservable.do(v => {
if (this.setCssClass !== v) {
setTimeout(() => {
this.setCssClass = v;
this.cdr.detectChanges();
}, 0);
}
})
.subscribe();
}
}
which is just awful.
Is there some cleaner way to tell the parent "here's a new value for one of your variables, set it at your leisure"?
EDIT:
And it doesn't even work. That setTimeout can, under special circumstances, execute after the component has officially been destroyed, which causing an exception needs an whole 'nother level of hacky horribleness to prevent.
Upvotes: 4
Views: 1123
Reputation: 14257
You could simply go for the Renderer
with ElementRef
approach. And don't forget to unsubscribe from your Observable once the component is destroyed:
@Component(...)
export class MyComponent implements OnInit, OnDestroy {
constructor(private element: ElementRef, private renderer: Renderer2) {}
@Input()
public inputObservable: Observable<boolean>;
private ngUnsubscribe = new Subject<void>()
ngOnInit() {
this.inputObservable
.takeUntil(this.ngUnsubscribe)
.do(v => this.toggleClass('some-class', v))
.subscribe()
}
ngOnDestroy() {
this.ngUnsubscribe.next();
this.ngUnsubscribe.complete();
}
private toggleClass(klass: string, state: boolean) {
if(!this.element.nativeElement) return;
if(state) {
this.renderer.addClass(this.element.nativeElement, klass)
} else {
this.renderer.removeClass(this.element.nativeElement, klass)
}
}
}
A second approach would be to change the input of you component to a boolean
and then from the parent component input the observable with async
-pipe:
@Input()
@HostBinding('class.some-class')
public myInput: boolean;
Parent component
<my-component [myInput]="myObservable$ | async"></my-component>
Upvotes: 5