fix(forms): preserve intermediate number values in signal forms - #69637
Conversation
There was a problem hiding this comment.
I had to update this to avoid the mismatch between the input and the model for the specific case of -0, which is found in the added tests
|
That seems to be an interesting change. Not sure if we should consider that a bug or a feature. |
Good catch, that's a good question. According to the HTML standard, we shouldn't allow it, so maybe it's a bug? |
|
Looks like some files are having formatting issues, it will be fixed by #69679 |
Preserve raw native input text while editing so parsed model values are not written back on every keystroke. Fixes angular#69635.
3321b98 to
042e645
Compare
| (rawValue: unknown) => { | ||
| // Mark the parsed value as already seen from this native control so the next update pass | ||
| // does not reformat and write it back over the user's in-progress input text. | ||
| bindings['controlValue'] = rawValue; |
There was a problem hiding this comment.
Adding this line seems to be breaking. I'll need to investigate more.
There was a problem hiding this comment.
What kind of problem is this causing? Would there be a minimal reproduction example to validate it?
There was a problem hiding this comment.
Here would be one
import {Component, effect, signal} from '@angular/core';
import {FormField, form} from '@angular/forms/signals';
@Component({
selector: 'app-root',
imports: [FormField],
template: `
<input [formField]="field" placeholder="Type 'hello ' with a space" />
<p>Local Signal (Input Buffer): "{{ text() }}"</p>
<p>Normalized Model (Trimmed): "{{ model() }}"</p>
`,
})
export class App {
// 1. Local signal bound to the input
text = signal<string>('');
// 2. The FormField directive from @angular/forms/signals
field = form(this.text);
// 3. The "normalized" model or external state
model = signal<string>('');
constructor() {
// Effect A: Sync Local -> Model (Trimming)
// When the user types, trim the input and update the model.
effect(() => {
const currentText = this.text();
const trimmed = currentText.trim();
if (this.model() !== trimmed) {
this.model.set(trimmed);
}
});
// Effect B: Sync Model -> Local
// DEPENDENCY TRAP: This effect reads 'text()', meaning it runs on every keystroke.
effect(() => {
const currentModel = this.model();
// Reading this.text() makes this effect dependent on the 'text' signal.
if (this.text() !== currentModel) {
// --- 🔴 THE REGRESSION POINT ---
// In the culprit Angular version, calling text.set() here eagerly
// overwrote the DOM buffer, swallowing the space the user just typed.
this.text.set(currentModel);
}
});
}
}

Preserve raw native input text while editing so parsed model values are not written back on every keystroke.
Fixes #69635.