Tim Büthe Tim Büthe - 9 hours ago 2
AngularJS Question

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?

Answer

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.