javascript - Vincular ngModel de un control de formulario personalizado (ControlValueAccessor) siempre establece el formulario en estado sucio
Estoy intentando implementar un control de formulario personalizado muy simple.
@Component({
selector: 'text-input',
template: '<input [(ngModel)]="value" />',
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TextInputComponent),
multi: true
}]
})
export class TextInputComponent implements ControlValueAccessor {
private _value: any;
get value(): any { return this._value; }
set value(value: any) { return this.writeValue(value); }
writeValue(value: any) {
if (value !== this._value) {
this._value = value;
this.onChange(value);
}
}
onChange = (x: any) => { };
onTouched = () => { };
registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }
registerOnTouched(fn: () => void): void { this.onTouched = fn; }
}
Pero mi problema es que, cuando lo uso en un formulario (reactivo) solo configurando [(ngModel)], form.dirty se evalúa como true, pero si uso un control de entrada normal, todo funciona como se espera.
<h2>Hello {{text}}</h2>
<form #f="ngForm" >
<text-input [(ngModel)]="text" name="text"></text-input>
</form>
Dirty: {{f.dirty}}
<form #f2="ngForm" >
<input [(ngModel)]="text" name="text" />
</form>
Dirty: {{f2.dirty}}
¿Hay algún problema con mi implementación? ¿Cómo puedo hacer que mi control personalizado funcione de manera que dirty solo se establezca en true cuando ocurra una interacción?
Reproducción de Plunkr: https://plnkr.co/edit/FBGAR4uqwen7nfIvHRaC?p=preview
Respuesta #1
EDITAR: Después de hacer algunos ajustes más, finalmente encontré lo que creo que debería ser la implementación correcta.
No puedo creer que simplemente nunca encontré una buena explicación en línea de cómo las funciones onChange y writeValue corresponden al estado sucio/prístino y cómo se manejan los reinicios.
Tan pronto como llames a onChange, tu control se establecerá en dirty: true. Llamar a form.reset() restablecerá este estado, pero también llamará a writeValue para establecer el nuevo valor. Así que NUNCA llames a onChange en este método porque romperá tus estados.
En mi implementación tengo un setter simple para mi valor en el que llamo a onChange para establecer realmente el estado correcto, porque el setter es llamado solo por el control de entrada en mi plantilla.
Tu control tal vez pueda tener un manejo más complejo de cómo se cambia realmente el valor, pero mientras realmente llames a onChange si quieres reflejar el cambio hacia el exterior y puedas establecer el valor de tu control con writeValue sin cambiar el estado, todo funcionará bien.
@Component({
selector: 'text-input',
template: `<input [(ngModel)]="value"
(blur)="onTouched()" />`,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TextInputComponent),
multi: true
}]
})
export class TextInputComponent implements ControlValueAccessor {
private _value: any;
// NOTE: you could also just bind to a normal field without
// getters and setters and call onChange from your template
// e.g. from (ngModelChange) of the input control
get value(): any { return this._value; }
set value(value: any) {
this.writeValue(value);
this.onChange(value);
}
writeValue(value: any) {
if (value !== this._value) {
this._value = value;
}
}
onChange(x: any) { console.log(x); }
onTouch() { };
registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }
registerOnTouched(fn: () => void): void { this.onTouched = fn; }
}
