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.
- Class Based (Classic)
- Functional Creation (NEW)
@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>) {}
}
@Component({...})
export class RxComponent {
readonly #state = rxState<ComponentState>()
// 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`;
}
}
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.
- Class Based (Classic)
- Functional Creation (NEW)
@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());
}
}
@Component({...})
export class RxComponent {
readonly #state = rxState<ComponentState>()
readonly #effects = rxEffects();
// 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 cdRef: ChangeDetectorRef
) {
this.#effects.register(this.#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.
- Class Based (Classic)
- Functional Creation (NEW)
@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);
});
});
}
}
@Component({...})
export class RxComponent {
readonly #state = rxState<ComponentState>()
readonly #effects = rxEffects();
constructor(
private elementRef: ElementRef<HTMLElement>,
private cdRef: ChangeDetectorRef
) {
// optional: cdRef.detach();
this.#effects.register(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);
});
});
}
}