Angular’s @Angular/Forms package provides the NgModel directive for bidirectional binding, which binds a JS variable (suppose name) to a DOM element (suppose input) so that the value of name changes. The value of the input element also changes automatically; When the value of the input element changes, the value of name automatically changes. The following code shows the simplest bidirectional binding (also available in stackblitz demo) :

@Component({
  selector: 'my-app',
  template: `
    <input [ngModel] ="name" (ngModelChange) ="this.name=$event">
    <button (click) ="this.name = this.name + ' , apple';">ChangeName</button>
    <p>{{name}}</p>
  `,
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {
  name = 'banana';
}
Copy the code

The above code uses the NgModel directive to bidirectionally bind the variable name to the input DOM element. The [(NgModel)] syntax sugar is not used here for a clearer understanding of the nature of NgModel. In fact, the Template Parser from @Angular/Compiler disassembles the ‘BANANA_BOX’ syntax [(XXX)] into [XXX] and (xxxChange). See L448-L453 and L501-L505, so [(XXX)] is just a simple notation for saving trouble.

As you can see in stackblitz Demo, if you change the input value, the name variable automatically changes as well, as can be seen in the p tag bound to name. If you click button to change the value of name, the value in the input box also changes, as can be seen from the value change in the input box. How does the NgModel directive bind in both directions?

Before we understand the principle of NgModel directive bidirectional binding, we can first look at the simplest form of bidirectional binding:

<input [value] ="country" (input) ="country = $event.target.value">
<button (click) ="country = country + ' , China';">ChangeCountry</button>
<p>Hello {{country}}!</p>
Copy the code

Click button to modify model, will automatically modify the input value, that is, automatically modify view, data flow direction is model -> view; When the values in the input box are updated, the country model value is automatically changed, as can be seen from the bound P tag. In this case, the data flow direction is view -> Model. Of course, this is the simplest and least extensible example of a bidirectional binding, and if you design an instruction, you need to consider not only the different types of views, but also the data validation problem. However, this simple example is essentially similar to the NgModel directive.

If we design such a bidirectional binding directive, the input must be the bound variable name, and the directive receives the name and then updates the value of the input element (also support textarea, SELECT and other DOM elements, and even custom DOM elements such as components). Model -> view; model -> view; The output must be the value of the input element, and then assign the value to name, so that the value of the input element automatically changes the name value, namely view -> model. The difficulty here is that the directive needs to be able to write the value of the DOM element (whether native or custom), listen for changes in the VALUE of the DOM element, and read the changes. So, in order to support native or custom DOM elements, and in order to have a good design pattern, it’s necessary to abstract an interface that helps instructions write and listen to read DOM element values, which makes things a lot easier.

Now, we need to understand two things: how does the input value automatically change when the name value changes; How does the name value of input change automatically?

When the NgModel directive bound to the input is instantiated, its constructor first looks for the ControlValueAccessor object, which is the abstract object mentioned above. This object is specifically responsible for updating and listening to read values of DOM elements. The input element in the template above not only binds the NgModel directive, it actually binds the DefaultValueAccessor directive, as you can see from the selector of the directive if the input template is written like this:

<input [ngModel] ="name" (ngModelChange) ="this.name=$event" type="number">
Copy the code

That binds not only the DefaultValueAccessor directive, but also the NumberValueAccessor directive.

Since the providers property of DefaultValueAccessor provides the NG_VALUE_ACCESSOR token that points to the object DefaultValueAccessor, So the NG_VALUE_ACCESSOR token injected in the NgModel constructor contains the ControlValueAccessor object array with only DefaultValueAccessor. If the input is of type=”number”, valueAccessors contains NumberValueAccessor and DefaultValueAccessor. The selectValueAccessor() method in the constructor iterates through the array of ControlValueAccessor objects provided by the NG_VALUE_ACCESSOR token, Custom ControlValueAccessor is preferred for @Angular/Forms built-in ControlValueAccessor is preferred for @Angular/Forms built-in ControlValueAccessor (there are only six built-in ControlValueAccessor) Otherwise, the default ControlValueAccessor object is selected. For this demo, that is the default DefaultValueAccessor object. One thing to note is that the injected NG_VALUE_ACCESSOR token has a decorator @self, so you can only look up this dependency from itself, which means the NgModel directive itself, along with the other directives it mounts to the input element. In addition, input is not bound to any Validators directives, so the injected NG_VALIDATORS and NG_ASYNC_VALIDATORS token parsing values are empty, and input is used alone, not in the form element. Or FormGroup bound elements, so there is no host ControlContainer, that is, parent is empty.

The NgModel directive runs the _setUpControl() method on its first instantiation, Use ControlValueAccessor (this demo is DefaultValueAccessor object) to bind the FormControl object inside the NgModel directive to DOM elements. In this demo, the input bound by the NgModel directive does not have a parent control container, so the _setUpStandalone method is called. The core method is setUpControl(), which contains two main points: First, register a callback function with the DefaultValueAccessor object by calling setUpViewChangePipeline(), so that the callback is executed when the input event is triggered when the input value changes. The logic of this callback is to update the FormControl value, and to cause the NgModel directive to raise the ngModelChange event, which contains the new value of the current input change. The setUpViewChangePipeline() method pipes the view -> model so that when the view (input) value changes, it synchronizes the FormControl value, And have the NgModel directive print out the new value; Second, register a callback into the FormControl object by calling the setUpModelChangePipeline method, [ngModel]= name [ngModel]= name [ngModel]=”name” IsPropertyUpdated (changes, this.viewModel) to true, so FormControl’s value formControl.setValue (value) needs to be updated, This triggers the callback function in the FormControl object. By calling the ControlValueAccessor. WriteValue () method to modify the view (this is the input), the value of the value (this is used in the demo DefaultValueAccessor. WriteValue (value)), and then let the NgModel directive throws ngModelChange event, the event contains the value is the current new FormControl object change, so, The setUpModelChangePipeline() method pipes the Model -> View so that when the FormControl object changes its value, it updates the view value simultaneously. And have the NgModel directive print out the new value.

Through the above explanation, we can understand how the input value automatically changes when the name value changes. How the name value automatically changes when the input value changes. (It is better to click the link to view the source code one by one, which is more efficient.) In a word: The NgModel directive is initialized with two callbacks installed (one to update the FormControl value when the view changes and the other to update the View value when the FormControl value changes). Update the FormControl object and throw an ngModelChange event that carries the value when the data flows from View -> Model. Use ControlValueAccessor to update the view value and also throw an ngModelChange event that carries the value. The raised ngModelChange event contains the new value, and the $event in the template is specifically handled by @Angular/Compiler for the value thrown by the ngModelChange event.

Of course, this article does not consider the existence of Validators, if the input template is modified to look like this:

<input [ngModel]="name" (ngModelChange)="this.name=$event" required>
Copy the code

In addition to binding NgModel directives, the template also binds RequiredValidator directives, so that no matter whether the data flow is view -> Model or Model -> View, the validator needs to be run before the data flow. The NgModel constructor contains a RequiredValidator object, passes the Validator to the FormControl object, and registers the validatorChange callback. This will run Validators later when the FormControl value is updated.

The NgModel directive manages the data flow of the model <-> view. There is a FormControl object that reads stored values and verifies validity. Values read from FormControl are assigned to the model passed in. View reads and writes values with ControlValueAccessor. The entire @Angular/Forms package is designed with this data flow in mind and is not complicated.

Also read the @Angular/Forms article to learn how to write a custom ControlValueAccessor: Don’t get confused about the ControlValueAccessor for Angular forms.