'Angular 2+ one-time binding

In angular 1 we could do one time binding in this way: {{ ::myFunction() }}.

In angular 2 this is throwing:

EXCEPTION: Template parse errors:
Parser Error: Unexpected token : at column 2 in [{{ ::consent(false, undefined, box) }}] in CookieConsent@5:29 ("ull-right" href="" (click)="consent(true, $event, box)">De acuerdo</a>
        <span class="hidden">[ERROR ->]{{ ::consent(false, undefined, box) }}</span>

How can we do one time binding in angular2?



Solution 1:[1]

I found solution for one time binding in angular 2 here: https://github.com/angular/angular/issues/14033

I created this directive:

 import { Directive, TemplateRef, ViewContainerRef, NgZone } from "@angular/core";

@Directive({
    selector: '[oneTime]',
})
export class OneTimeDirective {
    constructor(template: TemplateRef<any>, container: ViewContainerRef, zone: NgZone) {
        zone.runOutsideAngular(() => {
            const view = container.createEmbeddedView(template);
            setTimeout(() => view.detach());
        })
    }
}

And used it:

  <some-selector *oneTime [somePropertyToOneTimeBinding]="someValueToOneTimeBinding"></some-selector>

For example:

     <iframe *oneTime [src]="myUrl"></iframe>

Solution 2:[2]

Currently, you cannot do one time binding with Angular 2. However, you can know when your binding changes and reset your inputs.

Angular 2 provides OnChanges lifecycle hook for the same. You need to implement OnChanges interface to get the changes.

See the code example below where, I am storing the data-bound property in a private variable when OnInit is called.

export class Footer implements OnInit, OnChanges {
  @Input() public name: string;
  private privname: string;

  constructor() { }

  ngOnInit() {
    this.privname = this.name;
  }


  ngOnChanges(changes: { [key: string]: SimpleChange }): void {
    if (!changes["name"].isFirstChange()) {
        this.name = this.privname;
    }
  }
}

Later when other changes occur, I am setting the value to its old value on subsequent changes.

This mechanism works like One-time binding.

Alternate solutions: You could also use a setter function to capture the changes.

Solution 3:[3]

ChangeDetectionStrategy.CheckOnce is the solution for this problem.

This has been updated to OnPush see also comment in code:

export declare enum ChangeDetectionStrategy {
    /**
     * `OnPush` means that the change detector's mode will be set to `CheckOnce` during hydration.
     */
    OnPush = 0,
    /**
     * `Default` means that the change detector's mode will be set to `CheckAlways` during hydration.
     */
    Default = 1,
}

Solution 4:[4]

Use ngOnInit()

In Angular 2+ we have ngOnInit() which will generally only run one time, exactly when the component is initialized. This is the simplest and usually best solution to the issue of one-time binding.

Binding to a function can lead to dozens of unneeded calls to that function and slow down your app.

Instead of {{ ::myFunction() }}, create a property on the component and set its value in ngOnInit():

export class MyComponent implements OnInit {
  myValue: any;

  constructor() { }

  ngOnInit() {

    this.myValue = /* CALL FUNCTIONS OR CALCULATE VALUE HERE */

  }
}

And then in the template, simply use:

 {{ myValue }} 

Your calculation will run just once. ?

Solution 5:[5]

Since one time read/bind isnt possible by default in angular, I think writing a public getter function will solve the issue.

For example

public getValue():number {
 return mynumber ? mynumber : 25; // if mynumber is not undefined the mynumber else return 25
}

//In html template
<input type="range" min="getValue()" max="100">

This will work perfectly if the getter function is able to reply back before the template rendering takes place. So the initialization of mynumber would be great if done in ngOnInit() function

Solution 6:[6]

Angular doesn't provide any syntactic sugar to control the binding frequency of inputs.

However, you can control when to react when inputs are updated. There are two ways, and either or both could be used to get the desired behaviour:

  1. Using a setter for Input and conditionally forward the update to the component body or component's template.
  2. Switching the change detection in the component on or off using ChangeDetectorRef when inputs are satisfied.

Note that turning the change detection off doesn't mean that the inputs won't be updated. Inputs will always update, and so ngOnChanges will be called whether or not change detection is off or on. However, the template won't update against updated inputs if change detection is off. See the codesandbox to understand this effect.

Example 1

To explain the point 1 above, consider the following:

  _input1: number;
  @Input() set input1(input: number) {
    if (this._input1 === undefined || this._input1 === null) {
      this._input1 = input;
    }
  }

Consuming in template

  <div>Bound Input1: {{ _input1 }}</div>

_input1 is component's local copy and is only updated when desired i.e. in the above particular case, the local copy in only updated if it's previously null or undefined. input1 is the input property. Here we assume that an undefined or null value will never be considered a valid value and unless input1 is passed a non-null non-undefined value, we will consider that the "bind once" hans't occurred.

Example 2

ChangeDetectorRef excerpt from docs:

Base class that provides change detection functionality. A change-detection tree collects all views that are to be checked for changes. Use the methods to add and remove views from the tree, initiate change-detection ...

So ChangeDetectorRef's method detach could be used to detach a component from change detection tree. To demonstrate point 2 above, the following example waits until all inputs are satisfied and then turns off the change detection:

import {
  ChangeDetectorRef,
  Component,
  Input,
  OnChanges,
  SimpleChanges
} from "@angular/core";

@Component({
  selector: "bind-until-all",
  template: `
    <div>Input1: {{ input1 }}</div>
    <div>Input2: {{ input2 }}</div>
    <div>Input3: {{ input3 }}</div>
  `
})
export class BindUntillAllComponent implements OnChanges {
  /**
   * We assume that a null or undefined value is not an empty.
   * And until a non-empty value is passed to an input, it'll be considered as not-assigned.
   *
   * Based on the use case, define what is a suitable "empty" value
   * and assign that value as the default value to the inputs.
   */
  @Input() input1: number;
  @Input() input2: number;
  @Input() input3: number;

  private isDetached: boolean = false;

  constructor(private cdr: ChangeDetectorRef) {}

  ngOnChanges(changes: SimpleChanges) {
    // Check whether all inputs are satisfied i.e. they have non-empty values assigned
    // For our case, type === "number" satisfies that the value is non-empty
    const areAllInputsSatisfied = [this.input1, this.input2, this.input3].every(
      (n) => typeof n === "number"
    );

    // Stop change detection after triggering the manual change detection once
    if (areAllInputsSatisfied && !this.isDetached) {
      this.cdr.detectChanges();
      this.cdr.detach();

      this.isDetached = true;
    }
  }
}

Live example in the above mentioned codesandbox.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Community
Solution 3 eav
Solution 4
Solution 5 Abdeali Chandanwala
Solution 6 abdul-wahab