How to use the last valid modelValue if a model becomes invalid? How to use the last valid modelValue if a model becomes invalid? angularjs angularjs

How to use the last valid modelValue if a model becomes invalid?


I have created a simple directive that serves as a wrapper on the ng-model directive and will keep always the latest valid model value. It's called valid-ng-model and should replace the usage of ng-model on places where you want to have the latest valid value.

I've created an example use case here, I hope you will like it. Any ideas for improvements are welcomed.

This is the implementation code for valid-ng-model directive.

app.directive('validNgModel', function ($compile) {  return {      terminal: true,      priority: 1000,      scope: {        validNgModel: '=validNgModel'      },      link: function link(scope, element, attrs) {        // NOTE: add ngModel directive with custom model defined on the isolate scope        scope.customNgModel = angular.copy(scope.validNgModel);        element.attr('ng-model', 'customNgModel');         element.removeAttr('valid-ng-model');        // NOTE: recompile the element without this directive        var compiledElement = $compile(element)(scope);        var ngModelCtrl = compiledElement.controller('ngModel');        // NOTE: Synchronizing (inner ngModel -> outside valid model)        scope.$watch('customNgModel', function (newModelValue) {          if (ngModelCtrl.$valid) {            scope.validNgModel = newModelValue;          }        });        // NOTE: Synchronizing (outside model -> inner ngModel)        scope.$watch('validNgModel', function (newOutsideModelValue) {          scope.customNgModel = newOutsideModelValue;        });      }    };});

Edit: directive implementation without isolate scope: Plunker.


Since you are sending the entire object for each field modification, you have to keep the last valid state of that entire object somewhere. Use case I have in mind:

  1. You have a valid object { name: 'Valid', email: 'Valid' }.
  2. You change the name to invalid; the autosave directive placed at the name input knows its own last valid value, so the correct object gets sent.
  3. You change the email to invalid too. The autosave directive placed at the email input knows its own last valid value but NOT that of name. If the last known good values are not centralized, an object like { name: 'inalid', email: 'Valid' } will be sent.

So the suggestion:

  1. Keep a sanitized copy of the object you are editing. By sanitized I mean that any invalid initial values should be replaced by valid pristine ones (e.g. zeros, nulls etc). Expose that copy as a controller member, e.g. fooCtrl.lastKnowngood.
  2. Let autosave know the last known good state, e.g. as:

    <input ng-model="fooCtrl.email" autosave="fooCtrl.lastKnowngood" required />
  3. Keep the last known good local value in that object; utilize the ng-model expression, e.g. as:

    var lastKnownGoodExpr = $parse(attrs.autosave);var modelExpr = $parse(attrs.ngModel);function saveIfModelChanged () {    var lastKnownGood = lastKnownGoodExpr(scope);    if (ngModel.$valid) {        // trick here; explanation later        modelExpr.assign({fooCtrl: lastKnownGood}, ngModel.$modelValue);    }    // send the lastKnownGood object to the server!!!}
  4. Send the lastKnownGood object.

The trick, its shortcomings and how can it be improved: When setting the local model value to the lastKnownGood object you use a context object different than the current scope; this object assumes that the controller is called fooCtrl (see the line modelExpr.assign({fooCtrl: lastKnownGood}, ...)). If you want a more general directive, you may want to pass the root as a different attribute, e.g.:

<input ng-model="fooCtrl.email" autosave="fooCtrl.lastKnowngood" required    autosave-fake-root="fooCtrl" />

You may also do some parsing of the ng-model expression yourself to determine the first component, e.g. substring 0 → 1st occurence of the dot (again simplistic).

Another shortcoming is how you handle more complex paths (in the general case), e.g. fooCtrl.persons[13].address['home'].street - but that seems not to be your use case.


By the way, this:

ngModel.$viewChangeListeners.push(function () {    saveIfModelChanged();});

can be simplified as:

ngModel.$viewChangeListeners.push(saveIfModelChanged);


Angular default validators will only assign value to model if its valid email address.To overcome that you will need to override default validators.

For more reference see : https://docs.angularjs.org/guide/forms#modifying-built-in-validators

You can create a directive that will assign invalide model value to some scope variable and then you can use it.

I have created a small demo for email validation but you can extend it to cover all other validator.

Here is fiddle : http://plnkr.co/edit/EwuyRI5uGlrGfyGxOibl?p=preview