37

I have a form with few data fields and two buttons.I want to enable the buttons only if the user makes some changes to the form. I have tried using:

this.form.valueChanges.subscribe(data => console.log('form changes', data));

But the changes are detected initially when the form loads also. Is there any other way to check for any changes in the form. I want it to be called only when user makes changes to the field and not when the form loads. Following is my html and typescript code:

profile.html:

<section>
    <div>
        <form [formGroup]="form">
            <fieldset>
                <div class="panel-group m-l-1 m-r-1 accordion vertical-scroll" id="">
                    <div class="form-group required no-gutter">
                        <label for="firstname"> First Name:</label>
                        <div class="col-md-7 col-lg-6">
                            <input type="text" class="form-control" id="firstname" placeholder="" name="firstname" title="firstname" formControlName="firstname" size="128" aria-required="true" maxlength="35">
                        </div>
                    </div>
                </div>

            </fieldset>
            <div>
                <button class="btn btn-primary" type="button" (click)="save()">Save</button>
                <button class="btn btn-primary" type="button" (click)="cancel()">Cancel</button>
            </div>
        </form>
    </div>
</section>

profile.component.ts:

export class ProfileComponent implements OnInit, AfterViewInit, OnChanges {
    public form: FormGroup;

    constructor(private formBuilder: FormBuilder, private app: Application) {

    }

    loadForm(): void {
        this.form = this.formBuilder.group({
            firstname: [this.app.firstName, Validators.required]
        });
        this.form.valueChanges.subscribe(data => console.log('form changes', data));

    }

    save(): void {

    }

    cancel(): void {

    };

    ngOnInit() {
        this.loadForm();
    }

    ngAfterViewInit() {
        this.loadForm();
    }
}
Narendra Jadhav
  • 10,052
  • 15
  • 33
  • 44
Valla
  • 2,414
  • 11
  • 42
  • 73
  • Possible duplicate of [How to watch for form changes in Angular 2?](http://stackoverflow.com/questions/34615425/how-to-watch-for-form-changes-in-angular-2) – blo0p3r Feb 03 '17 at 13:33

11 Answers11

32

You can use the .dirty (or .pristine) values to determine if a user has used the UI to change the control value:

<button class="btn btn-primary" type="button" (click)="save()" [disabled]="!form.dirty" >Save</button>
<button class="btn btn-primary" type="button" [disabled]="!form.dirty"(click)="cancel()">Cancel</button>

https://angular.io/docs/ts/latest/api/forms/index/AbstractControl-class.html#!#dirty-anchor

dirty : boolean A control is dirty if the user has changed the value in the UI.

Note that programmatic changes to a control's value will not mark it dirty.

touched : boolean A control is marked touched once the user has triggered a blur event on it.

silentsod
  • 8,165
  • 42
  • 40
20

The problem with the .dirty and .pristine booleans, is that once they change, they do not go back, even if you undo all the changes you introduced. I managed to find a way of solving this, by creating a class that monitors changes in the entire form, and will check the changed values with the original form values. This way, if the user changes are undone, the form can go back to pristine, or optionally emit a boolean on an observable (ReplaySubject) you can provide and subscribe to.

The use will be something like this:

private _formIntactChecker:FormIntactChecker;

constructor(private _fb: FormBuilder) { 

    this._form = _fb.group({
        ...
     });

    // from now on, you can trust the .dirty and .pristine to reset
    // if the user undoes his changes.
    this._formIntactChecker = new FormIntactChecker(this._form);

}

Alternatively, instead of resetting the .pristine/.dirty booleans, the class can be configured to emit a boolean whenever the form changes from intact to modified and viceversa. A true boolean means, the form went back to being intact, while a false boolean means the form is no longer intact.

Here's an example on how you would use it:

private _formIntactChecker:FormIntactChecker;

constructor(private _fb: FormBuilder) { 

     this._form = _fb.group({
        ...
     });

     var rs = new ReplaySubject()

     rs.subscribe((isIntact: boolean) => {
        if (isIntact) {
            // do something if form went back to intact
        } else {
            // do something if form went dirty
        }
     })

     // When using the class with a ReplaySubject, the .pristine/.dirty
     // will not change their behaviour, even if the user undoes his changes,
     // but we can do whatever we want in the subject's subscription.
     this._formChecker = new FormIntactChecker(this._form, rs);

}

Finally, the class that does all the work:

import { FormGroup } from '@angular/forms';
import { ReplaySubject } from 'rxjs';

export class FormIntactChecker {

    private _originalValue:any;
    private _lastNotify:boolean;

    constructor(private _form: FormGroup, private _replaySubject?:ReplaySubject<boolean>) {

        // When the form loads, changes are made for each control separately
        // and it is hard to determine when it has actually finished initializing,
        // To solve it, we keep updating the original value, until the form goes
        // dirty. When it does, we no longer update the original value.

        this._form.statusChanges.subscribe(change => {
            if(!this._form.dirty) {
                this._originalValue = JSON.stringify(this._form.value);
            }
        })

        // Every time the form changes, we compare it with the original value.
        // If it is different, we emit a value to the Subject (if one was provided)
        // If it is the same, we emit a value to the Subject (if one was provided), or
        // we mark the form as pristine again.

        this._form.valueChanges.subscribe(changedValue => {

            if(this._form.dirty) {
                var current_value = JSON.stringify(this._form.value);

                if (this._originalValue != current_value) {
                    if(this._replaySubject && (this._lastNotify == null || this._lastNotify == true)) {
                        this._replaySubject.next(false);
                        this._lastNotify = false;
                    }
                } else {
                    if(this._replaySubject)
                        this._replaySubject.next(true);
                    else
                        this._form.markAsPristine();

                    this._lastNotify = true;
                }
            }
        })
    }

    // This method can be call to make the current values of the
    // form, the new "orginal" values. This method is useful when
    // you save the contents of the form but keep it on screen. From
    // now on, the new values are to be considered the original values
    markIntact() {
        this._originalValue = JSON.stringify(this._form.value);

        if(this._replaySubject)
            this._replaySubject.next(true);
        else
            this._form.markAsPristine();

        this._lastNotify = true;
    }
}

IMPORTANT: Careful with initial values

The class uses JSON.stringify() to quickly compare the whole formGroup value object. However, be careful when you initialize control values.

For example, for checkboxes, you must set the value binding it to a boolean. If you use other types, such as "checked", "0", "1", etc., the comparison will fail to work properly.

<input type="checkbox" ... [(ngModel)]="variable"> <!-- variable must be a boolean -->

The same goes to <select>, you must bind its value to a string, not a number:

<select ... [(ngModel)]="variable"> <!-- variable must be a string -->

For regular text input controls, also use a string:

<input type="text" ... [(ngModel)]="variable"> <!-- variable must be a string -->

Here is an example why otherwise it won't work. Suppose you have a text field, and you initialize it with an integer. The stringify of the original value would be something like this:

{ field1: 34, field2: "some text field" }

However, if the user updates field1 to a different value and goes back to 34, the new stringify will be:

{ field: "34", field2: "some text field" }

As you can see, although the form did not really changed, the string comparison between the original and the new value will result false, due to the quotes around the number 34.

kontiki
  • 37,663
  • 13
  • 111
  • 125
  • 2
    I read recently that you are not suppose to use `EventEmitter` for anything other than `@Output()`. I have updated the code and replaced `EventEmitter` by `ReplaySubject`. – kontiki Apr 10 '17 at 17:15
  • 1
    I wish it had this functionality built in :-/ Especially the 'stringify' problems – Simon_Weaver Jan 30 '18 at 04:39
  • 1
    You can set a 'replacer' function for JSON.stringify that can be used to force numbers to become strings. This should make the comparison a lot more reliable : `JSON.stringify(model, function(i,val){if (typeof(val)!=='object') return val.toString(); else return val;})` – Simon_Weaver Jan 30 '18 at 04:57
  • You can also add`.toUpperCase()` after the `toString()` if you want to ignore casing. – Simon_Weaver Jan 30 '18 at 04:58
  • Thank you for contributing this solution. It works very well to keep track of form changes and helps keep my component code clean. – Daniel Kuhlwein May 01 '19 at 19:28
8

first of all use "NgForm".
<form #myForm="ngForm" (ngSubmit)="onSubmit(myForm)">
Then on the "onSubmit()" function do this -

onSubmit(myForm: NgForm): void {
  let formControls = myForm.controls;
  if(formControls.firstName.dirty) {
    console.log("It's dirty");
  }
  else {
    console.log("not dirty");
  }
} 

It will definitely work. You can print the whole "myForm" and see for yourselves what all options are available to use.

Shahrukh khan
  • 196
  • 1
  • 7
4

I use some trick for my code, i think this not the best solution but somehow it's work for me.

profile.component.ts:

tempForm: any
ngOnInit() {
  this.loadForm();
  this.tempForm = this.form.value
}

save(): void {
  if (this.tempForm === this.form.value) {
    // Do Save
  } else {
    // Value is Same as initial
  }
}

hope this solve your question, or maybe just give some inspiration.

Sae
  • 502
  • 1
  • 4
  • 20
  • 1
    The triple equals isn't a robust way to check for equality here: http://adripofjavascript.com/blog/drips/object-equality-in-javascript.html – Jay Ordway Apr 04 '19 at 21:32
  • Yes,thank you, you correct that not a robust way, i say it not best solusion. but it's working for me and i quote this from your link https://jsfiddle.net/z37tqefa/ just to prove that code above is working. – Sae Apr 09 '19 at 03:27
3

I guess you can just ignore the first change

this.form.valueChanges
.skip(1)
.subscribe(data => console.log('form changes', data));

Hint: import the skip operator

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
3

Try the following to see if the form has changed:

ngOnChanges() {
    if (!!this.form && this.form.dirty) {
        console.log("The form is dirty!");
    }
    else {
        console.log("No changes yet!");
    }      
}  
dvanrensburg
  • 1,351
  • 1
  • 14
  • 21
3

I manage to work arround this by having a variable modified:

<button ion-button icon-only clear type="submit" [disabled]="!modified || !editForm.valid">
    <ion-icon name="checkmark"></ion-icon>
</button>

And then on the inputs you set the modified variable on the ionChange event:

<ion-input type="text" (ionChange)="modified=true"></ion-input> 
alexalejandroem
  • 1,094
  • 12
  • 17
3

You can pass { emitEvent: false } as options for the below reactive form methods to prevent them from triggering the valueChanges event

this.form.patchValue(value, { emitEvent: false })
this.form.setValue(value, { emitEvent: false })
this.form.controls.email.updateValueAndValidity({ emitEvent: false })
this.form.disable({ emitEvent: false })

yes disable triggers the valueChanges event

PS: above this.form is a reactive form

Read this excellent post, it'll answer all your questions and even give some great insights on reactive forms:

https://netbasal.com/angular-reactive-forms-tips-and-tricks-bb0c85400b58

tejas n
  • 638
  • 1
  • 8
  • 14
1

you can check for the changes in perticular formcontrol like this:

this.profileForm.controls['phone'].valueChanges.subscribe(
                data => console.log('form changes', data)

                );
Shailesh kala
  • 1,618
  • 18
  • 16
1

You can compare your object against the result of the form when submitting

let changes = false;
for ( let [ key, value ] of Object.entries( this.form.value ) ) {
    const form = this.form.value;
    const record = this.record;
    if ( form[ key ] != record[ key ] ) {
        changes = true;
        break;
    }
}
if ( !changes ) {
    // No changes
} else {
    this.record = this.form.value;
    this.UpdateRecord();
}
edu
  • 434
  • 1
  • 8
  • 17
  • 1
    This will only check for a shallow difference. perhaps: `if ( form[ key ] != record[ key ] ) {` change it to `if ( JSON.stringify(form[ key ]) !== JSON.stringify(record[ key ]) ) {` and even this way in some cases will not be enough – Rip3rs Nov 13 '19 at 14:24
0

There is an answer

https://stackoverflow.com/a/63488981/11186896

You can use markAsPristine method to set pristine status true, when the data changes back to defaultValue like this:

ngOnInit() {
    const defaultValue = this.registrationForm.value;
    this.registrationForm.valueChanges
      .pipe(debounceTime(200))
      .subscribe(value => {
        if (JSON.stringify(defaultValue) == JSON.stringify(value)) {
          this.registrationForm.markAsPristine();
        }
      });
  }
bug5layer
  • 81
  • 1
  • 4