Michael Lorton
Michael Lorton

Reputation: 44386

Can I access host-bindings some other way than through @HostBinding?

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

Answers (1)

cyr_x
cyr_x

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

Related Questions