I'm trying to implement a very simple custom form control.
@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; }
}
But my problem is, when I use it in a (reactive) form just setting [(ngModel)]
evaluates form.dirty
to true
, but if I use a normal input control everything works as expected.
<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}}
Is there any problem with my implementation? How can I get my custom control to work in a way that dirty
only gets set to true when an interaction happens?
Plunkr reproduction: https://plnkr.co/edit/FBGAR4uqwen7nfIvHRaC?p=preview
EDIT: After some more tinkering I finally found what I believe should be the correct implementation.
I can't believe that I simply never found a good explanation online how the onChange
and writeValue
functions correspond to the dirty/prestine state and how resets are handled.
As soon as you call onChange
your control will be set to dirty: true
. Calling form.reset()
will reset this state but also call writeValue
to set the new value. So NEVER call onChange
in this method because it will break your states.
In my implementation I have a simple setter for my value in which I call onChange
to actually set the right state, because the setter is only called by the input control in my template.
Your control can maybe have a more complex handling of how the value is actually changed, but as long as you actually call onChange
if you want to reflect the change to the outside and you can set the value of your control with writeValue
without changing the state everything will work fine.
@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; }
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With