Robert Koritnik Robert Koritnik - 28 days ago 12
AngularJS Question

Reset form as pristine if user changed input values to their initial value

Angular has

$dirty
and
$pristine
properties on
FormController
that we can use to detect whether user has interacted with the form or not. Both of these properties are more or less the opposite face of the same coin.

I would like to implement the simplest way to actually detect when form is still in its pristine state despite user interacting with it. The form may have any inputs. If user for instance first changes something but then changes it back to initial value, I would like my form to be
$pristine
again.

This may not be so apparent with text inputs, but I'm having a list of checkboxes where user can toggle some of those but then changes their mind... I would only like the user to be able to save actual changes. The problem is that whenever user interacts with the list the form becomes dirty, regardless whether user re-toggled the same checkbox making the whole form back to what it initially was.

One possible way would be I could have default values saved with each checkbox and add
ngChange
to each of them which would check all of them each time and call
$setPristine
if all of them have initial values.

But I guess there're better, simpler more clever ways of doing the same. Maybe (ab)using validators or even something more ingenious?

Question



What would be the simplest way to detect forms being pristine after being interacted with?

Answer

It can be done by using a directive within the ngModel built-in directive and watch the model value and make changes to pristine when needed. It's less expensive than watching the entire form but still seems an overkill and I'm not sure about the performance in a large form.

Note: The following snippet is not the newest version of this solution, check on UPDATE 1 for a newest and optimized solution.

angular.module('app', [])
  .directive('ngModel', function() {
    return {
      restrict: 'A',
      require: ['ngModel', '^?form'],
      priority: 1000, // just to make sure it will run after the built-in
      link: function(scope, elem, attr, ctrls) {
        var
          rawValue,
          ngModelCtrl = ctrls[0],
          ngFormCtrl = ctrls[1],
          isFormValue = function(value) {
            return typeof value === 'object' && value.hasOwnProperty('$modelValue');
          };

        scope.$watch(attr.ngModel, function(value) {

          // store the raw model value
          // on initial state
          if (rawValue === undefined) {
            rawValue = value;
            return;
          }

          if (value == rawValue) {

            // set model pristine
            ngModelCtrl.$setPristine();

            // don't need to check if form is not defined
            if (!ngFormCtrl) return;

            // check for other named models in case are all pristine
            // sets the form to pristine as well
            for (key in ngFormCtrl) {
              var value = ngFormCtrl[key];
              if (isFormValue(value) && !value.$pristine) return;
            }

            // if haven't returned yet, means that all model are pristine
            // so then, sets the form to pristine as well
            ngFormCtrl.$setPristine();
          }
        });
      }
    };
  })
  .controller('myController', function($rootScope, $timeout) {

    var $ctrl = this;

    $ctrl.model = {
      name: 'lenny',
      age: 23
    };
    $timeout(function() {
      console.log('watchers: ' + $rootScope.$$watchersCount)
    }, 1000);
  });

angular.element(document).ready(function() {
  angular.bootstrap(document, ['app']);
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.1/angular.js"></script>
<div ng-controller="myController as $ctrl">
  <form name="$ctrl.myForm" novalidate>
    <label>Name :
      <input name="test1" ng-model="$ctrl.model.name">
    </label>
    <label>age :
      <input name="test2" ng-model="$ctrl.model.age">
    </label>

    <label>Pristine: {{ $ctrl.myForm.$pristine }}</label>
    <div><pre>
  </pre>
    </div>
  </form>
</div>

UPDATE 1

Changed the watching system to watch once and get rid of the extra watchers.Now the changes comes from the change listeners of ngModelController and the watcher is unbinded on the first model set . As can be noticed by a console log, the numbers of watchers on root was always doubling the number of watchers, by doing this the number of watchers remains the same.

angular.module('app', [])
  .directive('ngModel', function() {
    return {
      restrict: 'A',
      require: ['ngModel', '^?form'],
      priority: 1000,
      link: function(scope, elem, attr, ctrls) {
        var
          rawValue,
          ngModelCtrl = ctrls[0],
          ngFormCtrl = ctrls[1],
          isFormValue = function(value) {
            return typeof value === 'object' && value.hasOwnProperty('$modelValue');
          };

        var unbindWatcher = scope.$watch(attr.ngModel, function(value) {
          // set raw value 
          rawValue = value;

          // add a change listenner
          ngModelCtrl.$viewChangeListeners.push(function() {
            if (rawValue === undefined) {
              //rawValue = ngModelCtrl.$lastCommit;
            }
            if (ngModelCtrl.$modelValue == rawValue) {
              // set model pristine
              ngModelCtrl.$setPristine();

              // check for other named models in case are all pristine
              // sets the form to pristine as well
              for (key in ngFormCtrl) {
                var value = ngFormCtrl[key];
                if (isFormValue(value) && !value.$pristine) return;
              }
              ngFormCtrl.$setPristine();
            }
          });

          // unbind the watcher at the first change
          unbindWatcher();

        });


      }
    };
  })
  .controller('myController', function($rootScope, $timeout) {

    var $ctrl = this;

    $ctrl.model = {
      name: 'lenny',
      age: 23
    };
    $timeout(function() {
      console.log('watchers: ' + $rootScope.$$watchersCount)
    }, 1000);
  });

angular.element(document).ready(function() {
  angular.bootstrap(document, ['app']);
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.1/angular.js"></script>
<div ng-controller="myController as $ctrl">
  <form name="$ctrl.myForm" novalidate>
    <label>Name :
      <input name="test1" ng-model="$ctrl.model.name">
    </label>
    <label>age :
      <input name="test2" ng-model="$ctrl.model.age">
    </label>

    <label>Pristine: {{ $ctrl.myForm.$pristine }}</label>
    <div><pre>
  </pre>
    </div>
  </form>
</div>