27

I have several custom form control components in my Angular application, which implement ControlValueAccessor interface and it works great.

However, when markAsPristine() is called on parent form, or on my custom control directly I need to update it's state: my custom control is actually have internal control and I need to call markAsPristine() on it too.

SO, how do I know when markAsPristine() is called on my control?

The ControlValueAccessor interface has no members, related to this problem, which I can implement.

Slava Fomin II
  • 26,865
  • 29
  • 124
  • 202
  • [**statusChanges**](https://angular.io/api/forms/AbstractControl#statusChanges)? – developer033 Jun 24 '17 at 00:57
  • 1
    According to the docs, the `statusChanges` observable emits when validation state changes, but I'm looking for dirty/pristine state changes. – Slava Fomin II Jun 24 '17 at 01:15
  • 1
    You're absolutely right. I read so fast that I didn't understand what you really wanted. Well, AFAIK you can't know when the `markAs-*` methods are being called. – developer033 Jun 24 '17 at 01:22
  • Thanks. Do you know a way to access the `FormControl` instance maybe? – Slava Fomin II Jun 24 '17 at 01:24
  • I've extracted this sub-question to another question here: https://stackoverflow.com/questions/44731894/get-access-to-formcontrol-from-the-custom-form-component-in-angular – Slava Fomin II Jun 24 '17 at 01:30
  • There is another way to check if the form is dirty or not. We can compare the object through which form is bind to. – rahul rathore Jun 03 '19 at 11:40

4 Answers4

24

After thorough investigation I've found out that this functionality is not specifically provided by Angular. I've posted an issue in the official repository regarding this and it's gained feature request status. I hope it will be implemented in near future.


Until then, here's two possible workarounds:

Monkey-patching the markAsPristine()

@Component({
  selector: 'my-custom-form-component',
  templateUrl: './custom-form-component.html',
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: MyCustomFormComponent,
    multi: true
  }]
})
export class MyCustomFormComponent implements ControlValueAccessor, OnInit {

  private control: AbstractControl;


  ngOnInit() {
    const origFunc = this.control.markAsPristine;
    this.control.markAsPristine = function() {
      origFunc.apply(this, arguments);
      console.log('Marked as pristine!');
    }
  }

}

Watching for changes with ngDoCheck

Be advised, that this solution could be less performant, but it gives you better flexibility, because you can monitor when pristine state is changed. In the solution above, you will be notified only when markAsPristine() is called.

@Component({
  selector: 'my-custom-form-component',
  templateUrl: './custom-form-component.html',
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: MyCustomFormComponent,
    multi: true
  }]
})
export class MyCustomFormComponent implements ControlValueAccessor, DoCheck {

  private control: AbstractControl;

  private pristine = true;


  ngDoCheck(): void {
    if (this.pristine !== this.control.pristine) {
      this.pristine = this.control.pristine;
      if (this.pristine) {
        console.log('Marked as pristine!');
      }
    }
  }

}

And if you need to access the FormControl instance from your component, please see this question: Get access to FormControl from the custom form component in Angular.

Slava Fomin II
  • 26,865
  • 29
  • 124
  • 202
1

My workaround is inspired by Slava's post and Get access to FormControl from the custom form component in Angular, and mixing template forms (ngModel) and reactive forms together. The checkbox control inside the component reflects dirty/pristine state and reports its state back outside to form group. So I can apply styles to checkbox input (<label>) control based on classes ng-dirty, ng-valid etc. I have not implemented markAsTouched, markAsUntouched as it can be done in similar way. Working demo on StackBlitz

The example component code is:

import { AfterViewInit, Component, Input, OnInit, Optional, Self, ViewChild } from "@angular/core";
import { ControlValueAccessor, NgControl, NgModel } from "@angular/forms";

@Component({
  selector: "app-custom-checkbox-control",
  template: '<input id="checkBoxInput"\
  #checkBoxNgModel="ngModel"\
  type="checkbox"\
  name="chkbxname"\
  [ngModel]="isChecked"\
  (ngModelChange)="checkboxChange($event)"\
>\
<label for="checkBoxInput">\
{{description}}\
</label>\
<div>checkbox dirty state: {{checkBoxNgModel.dirty}}</div>\
<div>checkbox pristine state: {{checkBoxNgModel.pristine}}</div>',
  styleUrls: ["./custom-checkbox-control.component.css"]
})
export class CustomCheckboxControlComponent
  implements OnInit, AfterViewInit,  ControlValueAccessor {
  disabled: boolean = false;
  isChecked: boolean = false;
 
  @Input() description: string;
  @ViewChild('checkBoxNgModel') checkBoxChild: NgModel;

  constructor(@Optional() @Self() public ngControl: NgControl) {
    if (this.ngControl != null) {
      this.ngControl.valueAccessor = this;
    }
  }

  checkboxChange(chk: boolean) {
    console.log("in checkbox component: Checkbox changing value to: ", chk);
    this.isChecked = chk;
    this.onChange(chk);

  }
  ngOnInit() {}

  ngAfterViewInit(): void {
    debugger
    this.checkBoxChild.control.setValidators(this.ngControl.control.validator);

    const origFuncDirty = this.ngControl.control.markAsDirty;
    this.ngControl.control.markAsDirty = () => {
      origFuncDirty.apply(this.ngControl.control, arguments);
      this.checkBoxChild.control.markAsDirty();
      console.log('in checkbox component: Checkbox marked as dirty!');
    }

    const origFuncPristine = this.ngControl.control.markAsPristine;
    this.ngControl.control.markAsPristine = () => {
      origFuncPristine.apply(this.ngControl.control, arguments);
      this.checkBoxChild.control.markAsPristine();
      console.log('in checkbox component: Checkbox marked as pristine!');
    }

  }


  //ControlValueAccessor implementations

  writeValue(check: boolean): void {
    this.isChecked = check;
  }

  onChange = (val: any) => {};

  onTouched = () => {};

  registerOnChange(fn: any): void {
    this.onChange = fn;
  }

  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

  setDisabledState(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }
}
Michal.S
  • 501
  • 6
  • 12
0

Another suggestion, based upon Slava's answer, is to replace the markAsDirty, markAsPristine, and _updatePristine methods in the FormGroup class:

ngOnInit(): void {
  const markAsDirty = this.formGroup.markAsDirty;
  this.formGroup.markAsDirty = (opts) => {
    markAsDirty.apply(this.formGroup, opts);
    console.log('>>>>> markAsDirty');
  };
  const markAsPristine = this.formGroup.markAsPristine;
  this.formGroup.markAsPristine = (opts) => {
    markAsPristine.apply(this.formGroup, opts);
    console.log('>>>>> markAsPristine');
  };
  const updatePristine = this.formGroup['_updatePristine'];
  this.formGroup['_updatePristine'] = (opts) => {
    updatePristine.apply(this.formGroup, opts);
    console.log('>>>>> updatePristine');
  };
}

I'm emitting events in the console.log locations, but other approaches would work, of course.

Andy King
  • 1,632
  • 2
  • 20
  • 29
0

There is another way to check if the form is dirty or not. We can compare the object through which form is bind to.Below function can be used for object properties comparison

isEquivalent(a, b) {
// Create arrays of property names
var aProps = Object.getOwnPropertyNames(a);
var bProps = Object.getOwnPropertyNames(b);

// If number of properties is different,
// objects are not equivalent
if (aProps.length != bProps.length) {
    return false;
}

for (var i = 0; i < aProps.length; i++) {
    var propName = aProps[i];

    // If values of same property are not equal,
    // objects are not equivalent
    if (a[propName] !== b[propName]) {
        return false;
    }
}

// If we made it this far, objects
// are considered equivalent
return true;

}

if you want to check this use below stackblitz link. I have tested it and is working well. Stackblitz link

rahul rathore
  • 329
  • 1
  • 3
  • 16