Search code examples
angulartypescriptcomponentsproperty-binding

How to re-apply a property binding without re-render the component?


Context

A business component A rely on a business service which say either:

  • true: all collapsable components should be open by default.

  • false: all collapsable component should be closed by default.

A pass this knowledge (a calculation, not a stored value) through a one-way property binding to multiple technical children components B, which define the template for a collapsable component (their content is the projection of a specific business component).

When A and B are first rendered, the bindings are resolved and B receive the knowledge: the collapsable components know if they should display their content or hide it. Nonetheless, they are still providing a button to display or hide the content if the user wants to.

Note: a collapsable component does not inject the service itself to access the knowledge directly, because it is a business service and it is a technical component.

Problem

After an action in A, I want to "re-resolve" the property binding with B, so I want A to pass the service knowledge to B again and thus override any action of the user (ex: if he opened a collapsable component which is closed by default, I want it to be restored to its default state, so closed).

The simplest solution would be to re-render the component (destroy/create), but I don't want that because :

1) I don't want the user to see a blink caused by the destruction/rendering of the component.

2) There is no good reason beyond this issue to re-render the component.

Code

@Component({
    selector: 'business-parent',
    template: '
    <generic-collapsable-component [opened]="businessHelper.conditionA">
        // A business-child component
    </generic-collapsable-component> 
    // More generic-collapsable-component
    '
})
export class BusinessParentComponent {

    constructor(private businessHelper: BusinessHelper) {
    }

    onBusinessAction() {
        // Here I do some business stuff...
        // And I want to force the binding [opened] to re-execute its default value, so I want GenericCollapsableComponent.opened = businessHelper.conditionA, and not what I currently have, which is the value of the last GenericCollapsableComponent.switch() I called.
    }
}

@Component({
    selector: 'generic-collapsable-component',
    template: '
    <button (click)="switch()">Switch to show or hide content</button>
    <div [ngClass]="{'is-hidden': !isOpen()}"
        <ng-content></ng-content>
    </div>
    '
})
export class GenericCollapsableComponent {

    @Input() opened: boolean; // Is intialized by the parent component but can be modified by the inner switch() method called in the template.

    constructor() {
    }

    switch(): void {
        this.opened = !this.opened;
    }

    isOpen(): boolean {
        return this.opened;
    };
}

Solutions

  • Re-render the component: NO.
  • Bind a function () => boolean to set the initial value and use an other private boolean value to respond to the user actions: this is what I did and it works, but it is not ideal.

Solution

  • I think what you're asking for is a two-way binding (aka "BANANA IN A BOX"). This will allow your parent component to pass in the initial state. The child component can then communicate back the value when it changes in the child. You can then also change the "opened" state in the parent component based on whatever business logic you have, keeping your generic collapsible component ignorant of whatever business rules dictate when something is opened/closed.

    The collapsible component:

    export class GenericCollapsableComponent {
    
        @Input() opened: boolean;
        @Output() openedChange = new EventEmittter<boolean>(); //must have the variable name followed by "Change"
    
        constructor() {
        }
    
        switch(): void {
            this.opened = !this.opened;
            this.openedChange.emit(this.opened); //alert the parent that the "opened" state has changed
        }
    
        isOpen(): boolean {
            return this.opened;
        };
    }
    

    Usages of the collapse component:

    <generic-collapsible [(opened)]="isOpened">
       <div>Some content!</div>
    </generic-collapsible>
    

    And the code-behind:

    export class ParentComponent {
        isOpened: boolean = true;
    }
    

    Here's a stackblitz showing the two-way binding and a number of different ways to to change the collapsible state.