angular / angular

Deliver web apps with confidence 🚀
https://angular.dev
MIT License
95.88k stars 25.32k forks source link

Two-way binding gets out of sync if the change handler doesn't set a new state value #44252

Closed todoroff closed 2 years ago

todoroff commented 2 years ago

Which @angular/* package(s) are the source of the bug?

Don't known / other

Is this a regression?

No

Description

I've got the following component

TS:

export class AppComponent {
  state = "";
  onChange(value: string) {
    this.state = "Never change";
  }
}

HTML:

<input type="text" [ngModel]="state" (ngModelChange)="onChange($event)" />
<hr />
<input type="text" [ngModel]="state" (ngModelChange)="onChange($event)" />
<p>{{ state }}</p>

I would expect that once I start typing in any of the two input fields, their value will first turn into "Never change" and then stay that way as I keep typing. However, that only works properly the first time.

What happens is that the first time I type a character in one of the fields, the DOM value of both turns into "Never change". However, if I keep typing, the DOM value of the input I'm typing into, gets out of sync with the actual state variable.

I've also noticed that if in the onChange handler I set state to a random value, it works as expected. It's only a problem when I set it to the same value every time.

I've also tried using [value] and (input) instead of [ngModel] and (ngModelChange), but I'm getting the same issue.

The problem doesn't appear to be limited to just input fields either. I've tried on select elements where I want to run some validation in an onSelect handler and not update the DOM value with the selected option if the check fails.

If this is not a bug, is there a proper way to achieve this? I'd like the DOM value to be bound to the state value at all times, so I can be sure that what's in the DOM is a function of what's in state.

Please provide a link to a minimal reproduction of the bug

https://codesandbox.io/s/practical-shadow-ui72r?file=/src/app/app.component.ts

Please provide the exception or error you saw

No response

Please provide the environment you discovered this bug in (run ng version)

No response

Anything else?

No response

pkozlowski-opensource commented 2 years ago

As surprising as it might be, the observed behaviour actually corresponds to how Angular (and many other frameworks using data-binding) was designed.

To understand what is going on here let's consider a simpler example from this stackblitz: https://stackblitz.com/edit/angular-ivy-eeedib

The key piece of code is [value]="state" which results in the following generated code: i0.ɵɵproperty("value", ctx.state);. The property instruction will do the following:

The crux is in the "update the DOM if the previously stored value and the current value are different" part - in the example stackbitz the state value never changes after the initial input! It is always equal to "'Never change'" and thus Angular never writes back to the DOM! This makes total sense as Angular is working hard in order to avoid unnecessary DOM manipulation.

What might be helpful here is a different mental model: instead of thinking of the model variable as "always being synchronized with the DOM" the actually implemented mental model is closer to "update DOM value when the variable value changes". This is how Angular was designed and this is the design that is followed by other frameworks, ex.: https://svelte.dev/repl/d0e99c1c845f4c9197429d2f679a52ca?version=3.44.2

It is kind of a corner and tricky use-case but the one that makes sense from the design point of view (even if it might be surprising to framework users).

In terms of work-arounds - doing changes manually in the DOM is the best I can think of right now.

angular-automatic-lock-bot[bot] commented 2 years ago

This issue has been automatically locked due to inactivity. Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.