Angular

Two-way Data Binding in Angular

If there was one feature in AngularJS that made us go “Wow”, then it was probably its two-way data binding system. Changes in the application state have been automagically reflected into the view and vise-versa. In fact, we could build our own directives with two-way data bound scope properties, by setting a configuration value.

Angular >= 2.x doesn’t come with such a (built-in) two-way data binding anymore. However, this doesn’t mean we can’t create directives that support two-way data binding. In this article we’re going to explore how two-way data binding in Angular >= 2.x is implemented and how we can implement it in our own directives.

Two-way data binding in a nutshell

There’s one directive in Angular >= 2.x that implements two-way data binding: ngModel. On the surface, it looks and behaves as magical as we’re used to (from AngularJS). But how does it really work? It’s not that hard really. In fact, it turns out that two-way data binding really just boils down to event binding and property binding.

In order to understand what that means, let’s take a look at this code snippet here:

<input [(ngModel)]="username">

<p>Hello {{username}}!</p>

Right, this is that one demo that blew our minds back in 2009, implemented in Angular >= 2.x. When typing into the input, the input’s value is written into the username model and then reflected back into the view, resulting in a nice greeting.

How does this all work? Well, as mentioned earlier, since version 2.x, two-way data binding in Angular really just boils down to property binding and event binding. There is no such thing as two-way data binding. Without the ngModel directive, we could easily implement two-way data binding just like this:

<input [value]="username" (input)="username = $event.target.value">

<p>Hello {{username}}!</p>

Let’s take a closer look at what’s going on here:

  • [value]=“username” - Binds the expression username to the input element’s value property
  • (input)=“expression” - Is a declarative way of binding an expression to the input element’s input event (yes there’s such event)
  • username = $event.target.value - The expression that gets executed when the input event is fired
  • $event - Is an expression exposed in event bindings by Angular, which has the value of the event’s payload

Considering these observations, it becomes very clear what’s happening. We’re binding the value of the username expression to the input’s value property (data goes into the component).

We also bind an expression to the element’s input event. This expression assigns the value of $event.target.value to the username model. But what is $event.target.value? As mentioned, $event is the payload that’s emitted by the event. Now, what is the payload of the input event? It’s an InputEventObject, which comes with a target property, which is a reference to the DOM object that fired the event (our input element). So all we do is, we’re reading from the input’s value property when a user enters a value (data comes out of the component).

That’s it. That’s two-way data binding in a nutshell. Wasn’t that hard right?

Okay cool, but when does ngModel come into play then? Since a scenario like the one shown above is very common, it just makes sense to have a directive that abstracts things away and safes us some keystrokes.

Understanding ngModel

If we take a look at the source code, we’ll notice that ngModel actually comes with a property and event binding as well. Here’s what our example looks like using ngModel, but without using the shorthand syntax:

<input [ngModel]="username" (ngModelChange)="username = $event">

<p>Hello {{username}}!</p>

Same rules apply. The property binding [ngModel] takes care of updating the underlying input DOM element. The event binding (ngModelChange) notifies the outside world when there was a change in the DOM. We also notice that the handler expression uses only $event and no longer $event.target.value. Why is that? As we’ve mentioned earlier, $event is the payload of the emitted event. In other words, ngModelChange takes care of extracting target.value from the inner $event payload, and simply emits that (to be technically correct, it’s actually the DefaultValueAccessor that takes of the extracting that value and also writing to the underlying DOM object).

Last but not least, since writing username and ngModel twice is still too much, Angular allows the shorthand syntax using [()], also called “Banana in a box”. So after all, it’s really an implementation detail of ngModel that enables two-way data binding.

Creating custom two-way data bindings

Using this knowledge, we can now build our own custom two-way data bindings. All we have to do is to follow the same rules that ngModel follows, which are:

  • Introduce a property binding (e.g. [foo])
  • Introduce a event binding with the same name, plus a Change suffix (e.g. (fooChange))
  • Make sure the event binding takes care of property extraction (if needed)

As you can see, there’s a bit more work involved to make two-way data binding work compared to AngularJS. However, we should also always consider if a custom two-way data binding implementation is really needed, or if we can just take advantage of ngModel. This, for example is the case when building custom form controls.

Let’s say we create a custom counter component and ignore of a second that this would rather be a custom form control.

@Component({
  selector: 'custom-counter',
  template: `
    <button (click)="decrement()">-</button>
    <span>{{counter}}</span>
    <button (click)="increment()">+</button>
  `
})
export class CustomCounterComponent {

  counterValue = 0;

  get counter() {
    return this.counterValue;
  }

  set counter(value) {
    this.counterValue = value;
  }

  decrement() {
    this.counter--;
  }

  increment() {
    this.counter++'
  }
}

It has an internal counter property that is used to display the current counter value. In order to make this property two-way data bound, the first thing we have to do is to make it an Input property. Let’s add the @Input() decorator:

@Component()
export class CustomCounterComponent {

  counterValue = 0;

  @Input()
  get counter() {
    return this.counterValue;
  }
  ...
}

This already enables us to bind expression to that property as a consumer of that component like this:

<custom-counter [counter]="someValue"></custom-counter>

The next thing we need to do, is to introduce an @Output() event with the same name, plus the Change suffix. We want to emit that event, whenever the value of the counter property changes. Let’s add an @Output() property and emit the latest value in the setter interceptor:

@Component()
export class CustomCounterComponent {

  ...
  @Output() counterChange = new EventEmitter();

  set counter(val) {
    this.counterValue = val;
    this.counterChange.emit(this.counterValue);
  }
  ...
}

That’s it! We can now bind an expression to that property using the two-way data binding syntax:

<custom-counter [(counter)]="someValue"></custom-counter>

<p>counterValue = {{someValue}}</p>

Again, please keep in mind that a component like a custom counter, would better serve as a custom form control and takes advantage of ngModel to implement two-way data binding as explained in this article.

Conclusion

Angular doesn’t come with built-in two-way data binding anymore, but with APIs that allow to implement this type of binding using property and event bindings. ngModel comes as a built-in directive as part of the FormsModule to implement two-way data binding and should be preferred when building components that serve as custom form controls.

Written by  Author

Pascal Precht