Angular two-way binding, a little more understanding

Original author: Pascal Precht
  • Transfer
From translator
From a translator : two years ago I started my first project on Angular (2+), having a large and successful AngularJS background. The transition required a noticeable formatting of thinking, since too much on A1 and A2 + is done “a little bit differently”. The soreness of the transition markedly reduced the thoughtram blog to me . A year ago, I received permission to translate this article "about elementary and easily understandable to everyone." But they are such hands (their articles are a bunch of unfinished ones). Surprisingly, the article translates well on Google translate. But some of the nuances in this translation were lost, not to mention the author's style. The author's style has not been fully preserved in my version. But, I hope, I managed to convey the mood and thoughts of the article.

I understand that Angular is not the most popular topic on Habré, but I hope that the translation will help someone, just as the original article once helped me.

That's what caused the wow effect in the good old AngularJS, so it is "two-way binding." This magic instantly fell in love with AngularJS, and broke all ideas about boring page programming and (oh, horror!) Web forms. Changes to data are instantly displayed on the screen and vice versa. Those who previously developed jQuery applications perceived linking as falling into a fairy tale. And bearded monsters, sawing fat clients before jQuery, began frantically counting the stupidly lost man-months.

And, moreover, the magic of two-way binding was available not only for special notations and selected components. We could easily use it in our own directives and components (just by setting the configuration parameter).

In Angular2 +, the creators abandoned the built-in two-way data binding (except through ngModel). But this does not mean that we cannot use two-way binding in our own directives ... It’s just that the freebie is over and now we need to do something on our own. And, preferably, with an understanding of how it works in Angular.

Table of contents



Two-way binding in a nutshell


In A2 +, only one single directive implements two-way data binding: ngModel . And at first glance, this is the same magic as in AngularJS (only in a different notation). But what's under the hood?

Surprisingly, under the hood, everything is relatively simple and logical: two-way binding is reduced to property binding and event binding. Two unilateral bindings, instead of one bilateral? Ok, let's two.

And immediately an example:

Hello {{username}}!


Yes, yes, this is a beautiful and amazing Angular2 demo from 2009. No kidding, beautiful. When changing the field, the username value falls into the model, and is immediately reflected in the welcome message on the form.

But how does it work? Recall that two-way binding in Angular2 is property binding and event binding. And yes, they can be simultaneously available in one directive. Moreover, even without ngModel , we could easily implement two-way data binding. For example, like this:

Hello {{username}}!


The output {{username}} is clear, but what is written there in input ? Let's understand:

  • [value] = “username” - notation with square brackets, associates the username expression with the value property
  • (input) = "expression" - a notation with parentheses, the expression is attached to the input event (yes, there is such an event). In our case:
    • username = $ event.target.value - this expression will be executed in response to the input event
    • $ event is a synthetic variable in Angular events that carries a payload: in this case, it contains information about what happened and its surroundings

Is it getting clearer? We fix it.

We bind the username property of the Angular model to the value property of the browser input element (one-way binding from model to view).

We also bind an expression to the input event of our element. Which assigns the value of $ event.target.value to the username property of the model.

What is $ event.target.value ? As already mentioned, $ event is full of various useful information about the event. In this case, it is an InputEventObject in which the target property refers to the DOM element that initiatedevent (i.e. our input element).

So, all that we essentially do is read the contents ( value ) of the input element ( $ event.target ) when the user enters a value. And when we assign this username value, the view data will be sent to the model.

That's all. This is "two-way binding in a nutshell . " Beauty?

But when does ngModel come into play? The scenario of working with input elements is very common and in demand. And for some reason I want to have a directive that hides the implementation and saves from extra keystrokes.

Understanding ngModel


If you look at the source, you can make sure that ngModel also has a binding to the property and the event. Here's what our ngModel example looks like, but without using shorthand syntax:

Hello {{username}}!


Almost everything is the same. The binding of the [ngModel] property takes care of updating the value of the input element. An event binding (ngModelChange) notifies the world that changes are occurring in the DOM.

And you noticed that the handler expression uses only $ event , not $ event.target.value . Is something wrong here? Not at all. As stated above, $ event is a synthetic variable that carries a payload . The decision of what is considered useful is taken by Angular. In other words, ngModelChange takes care of extracting target.value from the internal $ eventand just gives us what we want, without packaging and a tambourine. To be technically accurate, these are the ones of DefaultValueAccessor : it is he who extracts the data and transfers it to the base DOM object, although ... you can just not think about it).

Last but not least, since writing username and ngModel twice is still redundant, Angular allows the use of the abbreviated syntax [()] , also called “banana in a box”. Which is similar to the previous example, and returns us to the example from the beginning of the section, but with an understanding of the ngModel implementation . Providing the same two-way binding.

Hello {{username}}!



Create your own two-way data bindings


Now we know enough to create our own two-way data bindings. All you need to do is simply follow the same rules as ngModel , namely:

  • Enter a property binding (for example: [foo] )
  • Bind to an event with the same name and suffix Change (for example: (fooChange) )
  • Ensure that the event binding takes care of retrieving the property (if necessary)

Notice that creating two-way data binding requires significantly more work than AngularJS? This could be very frustrating for us ... If we would try to use our own two-way binding wherever possible. In real life, you should always consider whether we need two-way binding, and if necessary, is it easier to take advantage of ngModel. The latter, for example, takes place when creating custom form controls .

But let's say we create a custom counter component (and don’t want to use a custom form control).

@Component({
  selector: 'custom-counter',
  template: `
    {{counter}}
  `
})
export class CustomCounterComponent {
  counterValue = 0;
  get counter() {
    return this.counterValue;
  }
  set counter(value) {
    this.counterValue = value;
  }
  decrement() {
    this.counter--;
  }
  increment() {
    this.counter++;
  }
}

We have the property of the counter component to display the current value of the counter. To provide it with two-way binding, the first thing to do is turn it into an Input parameter. For this, the @Input () decorator is very useful :

@Component()
export class CustomCounterComponent {
  counterValue = 0;
  @Input()
  get counter() {
    return this.counterValue;
  }
  ...
}

This already allows you to bind the component property to the consumer as follows:


Now we need to set the  @Output () event with the same name ( counter ) and the suffix Change (it turns out counterChange). We want to raise this event every time counter changes . Why add the  @Output () property. And we’ll finish, in a couple of getters, the counter setter, in which we will intercept the value change and throw an event with the current counter value:

@Component()
export class CustomCounterComponent {
  ...
  @Output() counterChange = new EventEmitter();
  set counter(val) {
    this.counterValue = val;
    this.counterChange.emit(this.counterValue);
  }
  ...
}

This is it! Now we can bind the expression to this property using the two-way data binding syntax:

counterValue = {{someValue}}


Check out the demo and try it!

Again, keep in mind that a component such as a custom counter is best implemented with a custom form control, and take advantage of ngModel to implement two-way data binding, as described in this article .

Conclusion


Angular no longer comes with built-in two-way data binding. Instead, there are APIs in the box that allow you to implement full binding as binding properties and events.

ngModel comes as a built-in two-way binding directive FormsModule (do not forget to add it to a section of the imports Classified  @NgModule : a lane.). Linking via ngModel should be preferred when creating components that serve as custom form controls. Otherwise, it all depends on your imagination.

PS from the translator:The binding implementation in A2 + has become more modern. Now, almost “free” setters are used to monitor changes by "feng shui" (although it is clear that the mechanisms for dirty-checking remain, at least for high-level user components). This made it possible to abandon 100,500 watchers (procedures monitoring changes in “their” data). Which in A1 loved to create a malicious load on the browser and required unusually direct hands when planning rich interactive pages.

With properly designed components, A2 out of the box has become significantly more responsive. Let at the expense of the work of programmers. Now you can place a legion of components on the page and do not worry about processor resources.

The flip side of the coin was the initial cost of the "entry process" in A2 +, which affected the popularity of the framework. But A1 also had a high entry cost, only it was relegated to the major league. Due to a lack of understanding of how to organize large applications, many prototypes “took off” on A1, then “crumbled” and corresponded to React and Vue.

I hope that with this article I will help to slightly lower the threshold for the initial entrance to A2 +, which continues to be in demand (which I know firsthand).

Also popular now: