angular: NgModel works incorrectly when using onPush ChangeDetection strategy

I’m submitting a …

[x] bug report - related issue https://github.com/angular/angular/issues/10148, but it's about testing.

Current behavior I have two components: the first one is a simple child component with some input and basic support of [ngModel] directive via ControlValueAccesor interface. The second one is a parent component has onPush change detection strategy and populates a value into child component via [ngModel] directive (right after that it’s marked for changes via ChangeDetectorRef class). When [ngModel] is updated child component still displays an old value in the template while the component object has actual value in target property.

As I can see NgModel directive updates target component asynchronously - https://github.com/angular/angular/blob/2.0.0/modules/%40angular/forms/src/directives/ng_model.ts#L191-L193 so the following scenario occurs

  1. Change detection executes for parent component.
  2. Change detection updates [ngModel] value
  3. [ngModel] schedules async task that updates child component
  4. Change detection executes for child component
  5. Actual value arrives in child component.

Expected/desired behavior Change detection should work with already updated component to ensure that new value has been populated into UI.

So it looks like that last two steps should be swapped (or even better - [ngModel] can update child component synchronously).

Reproduction of the problem Here is a tiny example with the issue. I’m expecting here that right after 2 seconds App Component should display “new value”. ~~http://plnkr.co/edit/IabIsT3323GpZmQSPd4z?p=preview~~ new example can be found here http://plnkr.co/edit/KowgH96EtDbVJao13Hz5?p=preview, thanks to @andreialecu

if I use the following markup in the example above <my-child [value]="value"></my-child> instead of <my-child [ngModel]="value"></my-child> all work as expected

Please tell us about your environment:

  • Angular version: 2.0.0-rc.05
  • Browser: [ Chrome 52]
  • Language: [TypeScript 1.8 | ES5]

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Reactions: 79
  • Comments: 39 (14 by maintainers)

Commits related to this issue

Most upvoted comments

Any update on this issue?

Doesn’t look like it will be fixed in the near future.

temporary workaround?

Don’t use OnPush with form components

4 years and still going nowhere with this…

This issue is really big flaw of Angular IMO. OnPush strategy helps us build fully controllable, predictable and unidirectional applications with great performance. But various third parties which are using two-way data binding is forcing us to workaround child components by manually detecting changes.

If you can modify the source code of the component that implements the ControlValueAccessor, a workaround is to call markForCheck eg:

    public writeValue(value: any): void {
        this.value = value;
        this.cdf.markForCheck();
    }

Here’s the plunker from the original poster, using this technique: http://plnkr.co/edit/fy0gZdT5qpBZ3nKVOAzo?p=preview

Bumping this issue. I just spent hours debugging why change detection isn’t working until I found this thread 😕

Closing this issue since PR #44886 is now merged, the fix will be available in v13.2.1 release.

@m-przybylski I know it’s 8 months later and you’ve probably already fixed the problem one way or another, but maybe someone else will come here with the similar issue so I wanted to explain what’s happening.

Problem is that with OnPush change detection changes in control.errors done by async validators aren’t spotted, so you can’t just do *ngIf="control.errors". Solution is to wrap errors into Observable and use async pipe.

In component you’d need to do sth like this:

errors$ = control.statusChanges.pipe(
  startWith(null),
  map(() => control.errors)
);

and in template:

<div *ngIf="errors$ | async as errors">
  ... display errors ...
</div>

startWith is needed to trigger an initial validation.

See working example here: https://stackblitz.com/edit/angular-nmrtjx

I think the angular architecture does not permit this. If proper way to fix the issue is to force the component library author to make it work with OnPush change detection, maybe there should be a guideline for them. Currently I am unable to make all my form components work with OnPush.

Also it will be better in future to remove the “default” change detection altogether. This will force everyone to use OnPush, which will result in better applications.

This worked for me:

ngAfterViewInit() { this.changeDectorRef.markForCheck() }

Have the same issue. Cannot use onPush in my components because I use 3rd party components (value accessors) as child components that cannot be pinged outside to markForCheck() themselves and trigger change detection.

I would also add explicitly that the ngOnChanges() life-cycle event method should be fired. I didn’t see that mentioned in this version of the ticket.

Any update on this issue? or a temporary workaround? I need this for my project too. I have a parent component (with OnPush strategy & ControlValueAccessor) that wraps a third party component (with default strategy & ControlValueAccessor). When the parent component changes the value, the child component is never updated, unless it receives an user event (ie: hover, click).

onPush strategy is not meant to be used with component that has its own state. But for the component that receives data from outside. Form control does have internal state.

Otherwise you need to work manually with ChangeDetectorRef as @christianacca shows, which is not convenient way.

I think I can get around this by subscribing to statusChanges outside the validator:

// When you go from PENDING to anything else :p
this.myFormCtrl.statusChanges
  .bufferCount(2, 1)
  .map(states => states[0])
  .filter(prevState => prevState === 'PENDING')
  .subscribe(status => {
    this._ref.markForCheck();
  });

@mlc-mlapis sorry, but you are wrong. Change detection strategy to default and it works as expected…

@m-przybylski … I would prefer to not talk about OnPush at all in that case … because it’s not really related … and it’ll always be just confusing. Your case is just about async-validators and nothing more.

Updated plunker to work with Angular 2.0 final: http://plnkr.co/edit/KowgH96EtDbVJao13Hz5?p=preview Old one wasn’t working any more.

This seems pretty severe, I cannot use ngModel because of this and have to resort to avoiding ControlValueAccesor and using separate explicit @Input and @Output properties with a different name than ngModel.

Another good repro is available here: https://github.com/angular/angular/issues/12607

Here’s my cheesy work-around…

Write a Directive with the following code:

@Directive({
    selector: '[onPushNgModelComponent]'
})
export class OnPushNgModelDirective implements OnInit {

    private cdr: ChangeDetectorRef;

    constructor(private injector: Injector, private ngZone: NgZone) { }

    public manuallyDetectChanges() {
        this.ngZone.runOutsideAngular(() => {
            requestAnimationFrame(() => {
               if (this.cdr && !((this.cdr as ViewRef).destroyed)) { this.cdr.detectChanges(); }
            });
        });
    }

    ngOnInit() {
        this.cdr = this.injector.get<ChangeDetectorRef>(ChangeDetectorRef as any) as ChangeDetectorRef;
    }
}

Attach the directive to your third-party components that need it, and then when you would normally trigger a ‘detectChanges’ from the parent component, cycle through your view children that have this directive, and run the ‘manuallyDetectChanges’ function.