HostBindings
Some examples how to reactively handle basic HostBindings
with @rx-angular/state
RxState
.
Sadly HostBindings
are not able to bind to Observable
sources out of the box. So we have to come up with custom solutions
in order to have fully reactive components.
In the following examples we will use the rxLet
directive or the push
pipe as replacements for angular's async
pipe.
rxLet
and push
belong to the not yet released @rx-angular/template
package.
Furthermore we want to express that we will come up with a more convenient solution facing this problem. This can be seen as WIP and
should not be the long term solution to handle HostBindings
in a fully reactive way.
Imagine you have the following state which you want to bind to properties of your host element.
interface ComponentState {
visible: boolean;
top: number;
maxHeight: number;
}
Be aware of changeDetection - HostBindings are not reactive
In this setup we assign our HostBindings
to the get()
method of our state.
As stated in the title, we have to be aware changeDetection. On every changeDetection cycle, angular will re-evaluate
all HostBindings
. If our component doesn't get flagged as dirty, our HostBindings
won't get updated. So we have to make
sure that state changes that are related to the HostBindings
value are actually triggering a re-render.
@Component({
providers: [RxState],
})
export class RxComponent {
// Modifying the class
@HostBinding('[class.is-hidden]') get isHidden() {
return !this.state.get().visible;
}
// Modifying styles
@HostBinding('[style.marginTop]') get marginTop() {
return `${this.state.get().top}px`;
}
// Modifying styles
@HostBinding('[style.maxHeight]') get maxHeight() {
return `${this.state.get().maxHeight}px`;
}
constructor(private state: RxState<ComponentState>) {}
}
With this setup in place we have two options to get things done.
Call ChangeDetection manually
Since rendering is a side-effect, we could utilize the hold
method and register
a function which handles change detection for us.
@Component({
providers: [RxState],
})
export class RxComponent {
// Modifying the class
@HostBinding('[class.is-hidden]') get isHidden() {
return !this.state.get().visible;
}
// Modifying styles
@HostBinding('[style.marginTop]') get marginTop() {
return `${this.state.get().top}px`;
}
// Modifying styles
@HostBinding('[style.maxHeight]') get maxHeight() {
return `${this.state.get().maxHeight}px`;
}
constructor(
private state: RxState<ComponentState>,
private cdRef: ChangeDetectorRef
) {
state.hold(state.select(), () => this.cdRef.markForCheck());
}
}
By calling ChangeDetectorRef#markForCheck
after every state change, we flag our component dirty when needed and let angular's
ChangeDetection
do it's magic for us.
Let the template handle changeDetection
If you happen to need your variables not only for your HostBindings
but as well in the view, we could easily let
our viewHelpers take care of detecting changes. Just make sure all of your variables needed for the HostBindings
are bound
to the view correctly.
Inside the component:
readonly viewState$ = this.state.select();
Inside the template:
<ng-container *rxLet="viewState$; let s">
<span *ngIf="s.visible">I am a visible span</span>
</ng-container>
In this scenario, the rxLet
directive will flag your component as dirty every time a new state arrives. By assigning the
whole state object as to your viewModel$
, any change will result in a re-rendering, thus updating your HostBindings
.
Render on your own
With this setup you can opt-out of the ChangeDetection
of angular and manage HostBindings
completely on your own.
This approach even works when calling ChangeDetectorRef#detach
for your component.
We will utilize the ElementRef
itself for this purpose and manipulate the DOM on our own.
Feel free to use angular's Renderer2
if you want an abstraction layer, should work the exact same way.
@Component({
providers: [RxState],
})
export class RxComponent {
constructor(
private state: RxState<ComponentState>,
private elementRef: ElementRef<HTMLElement>,
private cdRef: ChangeDetectorRef
) {
// optional: cdRef.detach();
this.state.hold(this.state.select(), ({ visible, top, maxHeight }) => {
const { nativeElement } = elementRef;
nativeElement.style.marginTop = `${top ? top : 0}px`;
nativeElement.style.maxHeight = `${maxHeight ? maxHeight : 100}px`;
// by using this, we could assign more classes
const classList: { [cls: string]: boolean } = {
'is-hidden': !visible,
};
Object.keys(classList).forEach((cls) => {
classList[cls]
? nativeElement.classList.add(cls)
: nativeElement.classList.remove(cls);
});
});
}
}