Our content is free thanks to ag-Grid

ag-Grid is the industry leading JavaScript datagrid

ag-grid.com

Angular Forms: Why is ngModelChange late when updating ngModel value

Post Editor

Understanding some of the inner workings of Angular Forms by solving an interesting problem.

3 min read
post

Angular Forms: Why is ngModelChange late when updating ngModel value

Understanding some of the inner workings of Angular Forms by solving an interesting problem.

post
post
3 min read
3 min read

The @angular/forms package is rich in functionalities and although is widely used, it still has some unsolved mysteries. The aim of this article is to clarify why the problem in question occurs and how it can be solved. This involves strong familiarity with Angular Forms, so it would be preferable to read A thorough exploration of Angular Forms first, but not mandatory, as I will cover the necessary concepts once again in the following sections.

This article has been inspired by this Stack Overflow question.

The problem

Let's say we want to create a directive which will perform some changes on what the user types into an input, so that the bound FormControl will have the altered value.

Our directive may look like this:

@Directive({
  selector: '[myDirective]'
})
export class Mydirective {
  constructor(private control: NgControl) { }
  
  processInput(value: any) {
    return value.toUpperCase();
  }

  @HostListener('ngModelChange', ['$event'])
  ngModelChange(value: any) {
    this.control.valueAccessor.writeValue(this.processInput(value));
  }
}

and could be used like this:

<hello name="{{ name }}"></hello>
<input class="form-control" id="label" [(ngModel)]='modelValue' required myDirective>

Model: {{ modelValue }}

In this snippet we're using the well known banana in a box syntax, which is the same as: [ngModel]='modelValue' (ngModelChange)='modelValue = $event'.

Here's the corresponding StackBlitz:

As soon as we start typing into the input, the problem becomes evident.

Understanding the problem

Note: ControlValueAccessor does not refer to a certain entity (such as an interface), but to the concept behind it.

Angular has default value accessors for certain elements, such as for input type='text', input type='checkbox' etc...

A ControlValueAccessor is the middleman between the VIEW layer and the MODEL layer. When a user types into an input, the VIEW notifies the ControlValueAccessor, which has the job to inform the MODEL.

For instance, when the input event occurs, the onChange method of the ControlValueAccessor will be called. Here's how onChange looks like for every ControlValueAccessor:

function setUpViewChangePipeline(control: FormControl, dir: NgControl): void {
  dir.valueAccessor!.registerOnChange((newValue: any) => {
    control._pendingValue = newValue;
    control._pendingChange = true;
    control._pendingDirty = true;

    if (control.updateOn === 'change') updateControl(control, dir);
  });
}

The magic happens in updateControl:

function updateControl(control: FormControl, dir: NgControl): void {
  if (control._pendingDirty) control.markAsDirty();
  control.setValue(control._pendingValue, {emitModelToViewChange: false});
 
  // !
  dir.viewToModelUpdate(control._pendingValue);
  control._pendingChange = false;
}

dir.viewToModelUpdate(control._pendingValue); is what invokes the ngModelChange event in the custom directive.

/* ... */

@Output('ngModelChange') update = new EventEmitter();

/* ... */

viewToModelUpdate(newValue: any): void {
  this.viewModel = newValue;
  this.update.emit(newValue);
}

/* ... */

What this means is that the model value is the value from the input (in lowercase). Because ControlValueAccessor.writeValue only writes the value to the VIEW, there will be a delay between the VIEW's value and the MODEL's value. Here is how DefaultValueAccessor.writeValue() is defined:

  writeValue(value: any): void {
    const normalizedValue = value == null ? '' : value;
    this._renderer.setProperty(this._elementRef.nativeElement, 'value', normalizedValue);
  }

It's worth mentioning that FormControl.setValue(val) will write val to both layers, VIEW and MODEL, but if we were to use this, there would be an infinite loop, since setValue() internally calls viewToModelUpdate(because the MODEL has to be updated, e.g the modelValue in [(ngModel)]='modelValue'), and viewToModelUpdate calls setValue().

And this is the snippet depicted in the image above:

function setUpModelChangePipeline(control: FormControl, dir: NgControl): void {
  control.registerOnChange((newValue: any, emitModelEvent: boolean) => {
    // control -> view
    dir.valueAccessor!.writeValue(newValue);

    // control -> ngModel
    if (emitModelEvent) dir.viewToModelUpdate(newValue);
  });
}

The solution

A way to solve the problem is to add this snippet to the directive:

ngOnInit () {
  const initialOnChange = (this.ngControl.valueAccessor as any).onChange;

  (this.ngControl.valueAccessor as any).onChange = (value) => initialOnChange(this.processInput(value));
}

With this approach, we're modifying the data at the VIEW layer, before it is sent to the ControlValueAccessor.

And we can be sure that onChange exists on every built-in ControlValueAccessor:

If you are going to create a custom one, just make sure it has an onChange property. TypeScript can help you with that.

Conclusion

A ControlValueAccessor is responsible for keeping in sync the 2 main layers, VIEW and MODEL. By understanding some of the inner workings of Angular Forms, we were able to see why the problem occurred and how to solve it.

Thanks for reading!

Discuss with community

Share

About the author

author_image
Andrei Gatej

A curious software developer with a passion for solving problems and learning new things.

author_image

About the author

Andrei Gatej

A curious software developer with a passion for solving problems and learning new things.

About the author

author_image
Andrei Gatej

A curious software developer with a passion for solving problems and learning new things.

NxAngularCli
NxAngularCli
NxAngularCli

Featured articles

Angularpost
14 January 20216 min read
Demystifying Taiga UI root component: portals pattern in Angular

Just before new year we announced our new Angular UI kit library Taiga UI. If you go through Getting started steps, you will see that you need to wrap your app with the tui-root component. Let's see what it does and explore what portals are and how and why we use them.

JavaScriptpost
22 December 20203 min read
throwError is not throw error

The American poet Edward Estlin Cummings was famous for his eccentric use of spacing and capitalization, to the point that his name is usually styled as e e cummings. I wonder what he would think of an RxJS question that a friend asked me: “Is returning throwError the same as writing ‘throw error’?”

JavaScriptpost
22 December 20203 min read
throwError is not throw error

The American poet Edward Estlin Cummings was famous for his eccentric use of spacing and capitalization, to the point that his name is usually styled as e e cummings. I wonder what he would think of an RxJS question that a friend asked me: “Is returning throwError the same as writing ‘throw error’?”

Read more
JavaScriptpostthrowError is not throw error

22 December 2020

3 min read

The American poet Edward Estlin Cummings was famous for his eccentric use of spacing and capitalization, to the point that his name is usually styled as e e cummings. I wonder what he would think of an RxJS question that a friend asked me: “Is returning throwError the same as writing ‘throw error’?”

Read more