5

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

New Dev
  • 48,427
  • 12
  • 87
  • 129
  • I'm not an expert but are you sure you think it the right way? if you need to reset the form from your counter maybe you should include it in the directive ? or your counter should expose a function to be reset and this will be call by the form reset ? – MamaWalter Jan 30 '15 at 20:14
  • I only want to notify the parent form that this directive is no longer "dirty". The form, ideally, should manage its own dirty state. I *could* also `require: "^form"`, but what do I do then? I'm not sure I understand the second suggestion to expose a function to be called by the form's reset. – New Dev Jan 30 '15 at 20:17
  • can you show us your whole directive declaration. – levi Jan 30 '15 at 20:27
  • @levi, I have attached a link to a plunker at the bottom - can you look to see if there is something that is relevant to be included in the question (I was trying to keep the code in the question to a minimum) – New Dev Jan 30 '15 at 20:28

2 Answers2

3

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

New Dev
  • 48,427
  • 12
  • 87
  • 129
  • Sounds a bit too complex to my taste. Can you please check my related question https://stackoverflow.com/questions/48528957/re-set-all-forms-to-pristine-status-from-the-bottom-up – Naomi Jan 30 '18 at 23:37
0

This is a not so good solution. Iterate through controls attached to the form and check if there still a dirty one.

i used the method explain here to get the controls.

Plunker

app.directive("counter", function(){
  return {
    require: "ngModel",
    template: '<button ng-click="up()">{{counter}}</button><button ng-click="reset()">reset</button>',
    link: function(scope, element, attrs, ngModel){

      var original;

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

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

      scope.reset = function(){
        scope.counter = original;
        ngModel.$setViewValue(scope.counter);
        ngModel.$setPristine();

        // check if one of the controls attached to the form is still dirty
        var dirty = false;
        angular.forEach(scope.form, function(val, key) {
            if (key[0] != '$') {
              if (val.$dirty) {
                dirty = true; 
              }
            }
        });
        if(!dirty) scope.form.$setPristine();


      };
    }
  };
});   
Community
  • 1
  • 1
MamaWalter
  • 2,073
  • 1
  • 18
  • 27
  • Thanks for the help, but I explicitly stated that just calling `$setPristine()` on the form does not cut it, as the form may have other child controls that may be dirty. Also, the best way to do so would be to `require: "^form"`, rather than assume that `scope.form` exists. – New Dev Jan 30 '15 at 20:29
  • ok i think i understand, not sure there is built-in solution for that. – MamaWalter Jan 30 '15 at 21:13
  • I've answered my own question, but this is along the lines of what I am thinking of doing, except in your solution you are making an assumption about `form` existing on the scope. A directive's aim is to be re-usable, so it should not know in what form it resides. – New Dev Jan 30 '15 at 21:31