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.for
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.
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
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>
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
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();
}
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.