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

I am working on an application that automatically saves changes when the user changes something, for example, the value of an input field. I wrote an autosave directive that is added to all form fields that should automatically trigger save events.

template:

  <input ng-model="fooCtrl.name" autosave> <input ng-model="fooCtrl.email" autosave> 

directive:

  .directive('autosave', ['$parse', function ($parse) { return { restrict: 'A', require: 'ngModel', link: function (scope, element, attrs, ngModel) { function saveIfModelChanged () { // save object containing name and email to server ... } ngModel.$viewChangeListeners.push(function () { saveIfModelChanged(); }); } }; }]); 

So far, all this works fine for me. However, when I add validation to the mix, for example by checking that the input field is a valid email address, the modelValue parameter is set to undefined as soon as the viewValue variable is changed to an invalid email address.

What I would like to do is: Remember the last valid modelValue and use it when autosaving. If the user changes the email address to be invalid, the object containing name and email must be stored on the server. Using the current valid name and the last valid email .

I started by saving the last valid modelValue as follows:

with verification added:

  <input type="email" ng-model="fooCtrl.name" autosave required> <input ng-model="fooCtrl.email" autosave required> 

with saving lastModelValue:

  .directive('autosave', ['$parse', function ($parse) { return { restrict: 'A', require: 'ngModel', link: function (scope, element, attrs, ngModel) { var lastModelValue; function saveIfModelChanged () { // remeber last valid modelValue if (ngModel.$valid) { lastModelValue = ngModel.$modelValue; } // save object containing current or last valid // name and email to server ... } ngModel.$viewChangeListeners.push(function () { saveIfModelChanged(); }); } }; }]); 

My question is how to use lastModelValue when saving, but keeping an invalid value in the view?

EDIT:

Another feature suggested by Jugnu below will wrap and manipulate the assembly in validators.

I tried to do the following: wrap all existing validators and remember the last valid value, restore it if the checks fail:

 Object.keys(ngModel.$validators).forEach(function(validatorName, index) { var validator = ngModel.$validators[validatorName]; ngModel.$validators[validatorName] = createWrapper(validatorName, validator, ngModel); }); function createWrapper(validatorName, validator, ngModel){ var lastValid; return function (modelValue){ var result = validator(modelValue); if(result) { lastValid = modelValue; }else{ // what to do here? maybe asign the value like this: // $parse(attrs.ngModel).assign(scope, lastValid); } return result; }; } 

But I'm not sure how to continue this approach. Can I set the model value without using AngularJS and try to confirm this new setpoint?

+5
source share
3 answers

I created a simple directive that serves as a wrapper in the ng-model directive and will always support the last valid value of the model. It is called valid-ng-model and should replace the use of the ng-model in those places where you want to have the last valid value.

I created a usage example here , hope you like it. Any ideas for improvement are welcome.

This is the implementation code for the valid-ng 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; }); } }; }); 

Editing: Directive implementation without isolation: Plunker .

+2
source

Since you send the entire object for each field modification, you must somewhere save the last valid state of the entire object. Use case, which I mean:

  • You have a valid object { name: 'Valid', email: 'Valid' } .
  • You change the name to invalid; the autosave directive, placed when entering the name, knows its last valid value, so the correct object is sent.
  • You will also change the email address to an invalid one. The autosave directive, located at the input via email, knows its last valid value, but not its name. If the last known good values ​​are not centralized, an object such as { name: 'inalid', email: 'Valid' } will be sent.

So the sentence:

  • Keep a cleaned copy of the object you are editing. By disinfected, I mean that any invalid starting values ​​must be replaced by real intact ones (e.g. zeros, zeros, etc.). Set the copy as a controller element, for example. fooCtrl.lastKnowngood .
  • Let autosave know the last known good condition, for example. as:

     <input ng-model="fooCtrl.email" autosave="fooCtrl.lastKnowngood" required /> 
  • Keep the last known good local value in this object; use the expression ng-model , for example. 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!!! } 
  • Submit the lastKnownGood object.

The trick, its shortcomings and ways to improve it: when setting the value of the local model to the lastKnownGood object lastKnownGood you use a context object different from the current area; this object assumes that the controller is called fooCtrl (see line modelExpr.assign({fooCtrl: lastKnownGood}, ...) ). If you need a more general directive, you can pass the root as another attribute, for example:

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

You can also parse the ng-model expression yourself to determine the first component, for example. substring 0 → 1st place at the point (again simplified).

Another drawback is the way you handle more complex paths (in the general case), for example. fooCtrl.persons[13].address['home'].street - but that doesn't seem to be your precedent.


By the way, this is:

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

can be simplified as:

 ngModel.$viewChangeListeners.push(saveIfModelChanged); 
+2
source

Angular Default validators will only assign a value to the model if its valid email address. To overcome this, you will need to override the default validators.

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

You can create a directive that assigns an invalidide model value to some variable scope, and then you can use it.

I created a small demo for email authentication, but you can expand it to cover all other validators.

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

+1
source

All Articles