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

岁酱吖の 提交于 2019-12-13 13:01:17

问题


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

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, this all works fine for me. However, when I add validation into the mix, for example validating the input field to be a valid email address, the modelValue is set to undefined as soon as the viewValue is changed to an invalid email address.

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

I started out by saving the last valid modelValue like this:

template with validation added:

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

directive 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 while saving, but preserving the invalid value in the view?

EDIT:

Another possibility, as suggested by Jugnu below, would be wrapping and manipulating the build in validators.

I tried to following: wrap all existing validators and remember the last valid value, to restore it if validations fails:

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 with this approach either. Can I set the model value without AngularJS kicking in and try to validate that newly set value?


回答1:


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.




回答2:


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);



回答3:


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



来源:https://stackoverflow.com/questions/32628972/how-to-use-the-last-valid-modelvalue-if-a-model-becomes-invalid

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!