Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 405
Tim Büthe Avatar asked Sep 17 '15 11:09

Tim Büthe


People also ask

What is model validation in ASP NET MVC?

Model Validation in ASP.NET MVC. Model validation is the process of checking whether the user input is suitable for model binding and if not it should provide useful error messages to the user. The first part is to ensure that only valid entries are made. This should filter inputs which don’t make any sense.

What to do when modelstate IsValid fails?

Model validation occurs prior to each controller action being invoked, and it's the action method's responsibility to inspect ModelState.IsValid and react appropriately. In many cases, the appropriate reaction is to return an error response, ideally detailing the reason why model validation failed.

How do I check if a model is valid?

One possible approach of checking if the model is valid is checking directly in the action. I check if every field contains the expected value. If not, I add a model error to the Modelstate.

What is user input model validation?

Model validation is the process of checking whether the user input is suitable for model binding and if not it should provide useful error messages to the user. The first part is to ensure that only valid entries are made.


2 Answers

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.

like image 99
S.Klechkovski Avatar answered Nov 15 '22 06:11

S.Klechkovski


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);
like image 37
Nikos Paraskevopoulos Avatar answered Nov 15 '22 06:11

Nikos Paraskevopoulos