How to reset custom input directive and its parent form to $pristine

New Dev picture New Dev · Jan 30, 2015 · Viewed 11k times · Source

I've implemented a custom input directive - counter with a reset capability. The directive has require: "ngModel".

I am resetting the pristine state of the directive's ngModel with $setPristine(). Unlike $setDirty(), $setPristine() does not touch the $pristine state of the parent form.

Q: How do I "notify" the parent form that this directive is no longer "dirty", such that the parent form could have its $pristine state reset?

Bear in mind that just calling form.$setPristine() is not enough as there may be other "dirty" controls in the form, which my directive wouldn't (and shouldn't) know about.

This is the directive's link function:

link: function(scope, element, attrs, ngModel){

  var original;

  ngModel.$render = function(){
    original = scope.counter = ngModel.$viewValue;
  };

  scope.up = function(){
    ngModel.$setViewValue(++scope.counter);
  };

  scope.reset = function(){
    scope.counter = original;
    ngModel.$setViewValue(scope.counter);
    ngModel.$setPristine(); // this sets $pristine on the directive, but not the form
  };
}

And here's how it is used:

<div ng-form="form">
  <counter ng-model="count"></counter>
</div>

plunker

Answer

New Dev picture New Dev · Jan 30, 2015

As of Angular 1.3.x, there is no built-in solution.

form.$setPristine() sets pristine on all its child controls. (link to code)

ngModel.$setPristine() only sets $pristine on itself (link to code)

One way to solve this is to create a directive that lives alongside a form directive and hijacks form.$setDirty to track dirty controls count. This is probably best done in a pre-link phase (i.e. before child controls start registering themselves).

app.directive("pristinableForm", function() {
  return {
    restrict: "A",
    require: ["pristinableForm", "form"],
    link: function(scope, element, attrs, ctrls) {
      var me = ctrls[0],
        form = ctrls[1];
      me.form = form;
      me.dirtyCounter = 0;
      var formSetDirtyFn = form.$setDirty;
      form.$setDirty = function() {
        me.dirtyCounter++;
        formSetDirtyFn();
      };
    },
    controller: function() {
      this.$notifyPristine = function() {
        if (this.dirtyCounter === 0) return;
        if (this.dirtyCounter === 1) {
          this.dirtyCounter = 0;
          if (this.form) this.form.$setPristine();
        } else {
          this.dirtyCounter--;
        }
      };
    }
  };
});

Then, the custom input directive needs to require: ["ngModel", "^pristinableForm"] and call pristinableForm.$notifyPristine() in its reset function:

scope.reset = function(){
  if (ngModel.$dirty){
    scope.counter = original;
    ngModel.$setViewValue(scope.counter);
    ngModel.$setPristine();
    pristinableForm.$notifyPristine();
  }
};

The usage is:

<div ng-form="form" pristinable-form>
  <counter ng-model="count1"></counter>
  <counter ng-model="count2"></counter>
  <input ng-model="foo" name="anotherControl">
</div>

plunker