Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Binding ngModel of custom form control (ControlValueAccessor) always sets form to dirty state

Tags:

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.

enter image description here

<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

like image 921
Staeff Avatar asked Feb 20 '18 14:02

Staeff


1 Answers

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; }
}
like image 69
Staeff Avatar answered Sep 20 '22 12:09

Staeff