10

I need to be able to temporarily persist data that isn't fully validated yet, then enforce validation when I'm ready to make it permanent. But Angular is preventing that.

I have a form. The user can saveDraft() on early versions of the form, which are persisted to the server. Then, when the user is ready, they can submit() the form, which will persist it with different flags, thus beginning the actual processing of that data.

The problem I'm running into is with Angular's built-in validations. When a user enters some data into an input with validations on it, that data is cached on the $viewValue property. But if validation fails, it's never copied to the $modelValue property, which is a reference to the actual $scope property I bound the input to. And hence the 'invalid' value will not be persisted.

But we need to persist it. We'll deal with forcing the user to correct their validation failures later, when they submit(). Also, we have no way to know whether the user is going to saveDraft() or submit() on a particular instance of the view/controller, so we can't setup the views and validation differently beforehand.

My thinking is that we need to somehow iterate the form elements and get Angular to somehow let the data go through. But I can't find a way that's flexible and scalable.

I've tried setting $scope.formName.inputName.$modelValue = $scope.formName.inputName.$viewValue, but that seems to just upset the gods, as both values are then null'ed.

I've tried using $scope.formName.inputName.$setValidity('', true), but that only updates the UI state. It never touches $modelValue.

I can successfully use $scope.model.boundPropertyName = $scope.formName.inputName.$viewValue but that feels very imperative and won't allow any variance between the identifiers for boundPropertyName and inputName. In other words, you either need individual functions for all form controls, or you need to rely on naming conventions. Both of which are super-brittle.

So... how can I get that $modelValue updated elegantly? And/Or, is there another, better way to obtain the same results, whereby sometimes I can persist with validation, and sometimes persist without?

Other options I'm considering, but not happy with:

  • Run validation manually, only when the user hits submit(). But that defeats the UX value of instant, inline validation in the UI. We might as well just offload all the validation to the server and do a round-trip each time.
  • Make copies of ngModel and ngModelController, and monkey-patch them to update $modelValue regardless of validity. But that hacks the framework when there should be a more elegant path.

See CodePen here.

(Side-note: Angular appears to be filtering the data according to the validator in both directions. If you set a default value on the model for formData.zip of '1234', which isn't enough characters to validate, Angular doesn't even show it. It never reaches the initial $viewValue.)

3
  • Why not go with angular design principles and enforce validation at all times? If you must go with your desired output, then disregard angulars validation system and simply validate your inputs on submit(). Commented Jan 25, 2014 at 3:54
  • You could use a directive that requires ngModel, there by having access to the view and model values via the ngModel controller Commented Jan 25, 2014 at 5:07
  • @fooby: I can •access* the values without any trouble, from the form's parent controller. I just can't see an effective way to write them all programmatically that isn't very brittle. Can you point me to an effective example of a directive that delegates to ngModel? Commented Jan 27, 2014 at 2:03

4 Answers 4

19

The following solution can be used since Angular version 1.3:

You can set ng-model-options="{ allowInvalid: true }" in the input field of the model where you want to persist invalid attributes.

allowInvalid: boolean value which indicates that the model can be set with values that did not validate correctly instead of the default behavior of setting the model to undefined

https://docs.angularjs.org/api/ng/directive/ngModelOptions

Then, when you are ready to show the user their validation errors, you are free to do it your way. Just remember to give your inputs and forms name attributes, so that you can reference them in your scope.

E.g. if($scope.myFormName.my_input_name.$invalid) { ... }

A relevant tutorial: http://blog.thoughtram.io/angularjs/2014/10/19/exploring-angular-1.3-ng-model-options.html

Sign up to request clarification or add additional context in comments.

1 Comment

I think your answer should be marked as the correct one! It's the most cleanest and easiest one!
6

Emil van Galen has a blog post that covers exactly this issue. I've used his input directive, which works perfectly.

As he point out, The $parsers array of NgModelController is:

Array of functions to execute, as a pipeline, whenever the control reads value from the DOM. Each function is called, in turn, passing the value through to the next. Used to sanitize / convert the value as well as validation. For validation, the parsers should update the validity state using $setValidity(), and return undefined for invalid values.

So, to allow the model to be updated to an invalid value, yet retain the validation results, create a directive that does not return undefined for invalid values. For example, Emil's directive reverts invalid, undefined string values to the model value, otherwise it returns the view value:

angular.module('jdFixInvalidValueFormatting', [])
.directive('input', function() {
    return {
        require: '?ngModel',
        restrict: 'E',
        link: function($scope, $element, $attrs, ngModelController) {
            var inputType = angular.lowercase($attrs.type);

            if (!ngModelController || inputType === 'radio' ||
                    inputType === 'checkbox') {
                return;
            }

            ngModelController.$formatters.unshift(function(value) {
                if (ngModelController.$invalid && angular.isUndefined(value)
                        && typeof ngModelController.$modelValue === 'string') {
                    return ngModelController.$modelValue;
                } else {
                    return value;
                }
           });
       }
    };
});

You can see it working in his Plunker (also note his take on an improved handling of null rather than undefined): http://plnkr.co/edit/gist:6674554?p=preview

7 Comments

This is supremely clever, I like it, and trumps my answer as a much more angular way to approach this, but I notice in your plunker that the model value is not always equal to the value of the <input> which I believe what the OP is looking for. Am I missing something?
In the case were the view value is undefined, invalid and of type string, the model value is substituted - this condition can be omitted as desired - in which case the function becomes return value;
btw: it's not my plunker, but rather Emil van Galen's. So the implementation doesn't exactly match the OP's requirements, as the blog post and plunker pre-date the question.
Note: for anyone interested in more background on this, there's an open Angular issue on a closely-related problem, on which Emil van Galen has also commented: github.com/angular/angular.js/issues/1412. Bottom-line: the integration of validation into the $parsers/$formatters pipeline produces a number of confusing behaviors and makes validation more difficult. But it would be a huge breaking change to disentangle it all. Maybe in 2.0?
I had to return ngModelController.$viewValue to get this to always hold on to the value entered in the field regardless of validation....per @marfarma, return value; did not work when invalid.
|
3

I took a stab at doing this. The basic idea is to keep the model updated whether or not the input is valid by grabbing the text right off the input element. Then, updating the view an $render with the model data, even if the view is undefined. I haven't gotten the from to change the style on load with bad data. I'm sure that's just a matter of calling $setViewValue() with something invalid then updating the element.

It's not a terribly angular way to do things. If you needed an invalid version of the form, then I might used a directive to bind think my-model-no-validation="myDirtyModel.value", but that's a task for another day.

Here is a fiddle: http://jsfiddle.net/fooby12/dqPbz/

Shortened code for directive:

angular.module('bindApp', []).
controller('bindToViewCtrl', function($scope) {
    $scope.zip = /(^\d{5}$)|(^\d{5}-\d{4}$)/;
    $scope.formData = {
        zip: '1234',
        bindToView: true
    };
}).
directive('bindToView', function($log) {
    return {
        require: 'ngModel',
        scope: {bindToViewIsOn: '&bindToView', ngModel: '='},
        link: function (scope, iElem, iAttr, ngModel) {
            if(!ngModel) return;    

            ngModel.$render = function() {
                iElem[0].value = ngModel.$viewValue || ngModel.$modelValue;  
            };

            iElem.on('blur keyup change', function() {
                scope.$apply(updateModel);
            });

            scope.$watch(function() { return scope.bindToViewIsOn(); }, function() {
                updateModel();
            });

            function updateModel(){
                if(scope.bindToViewIsOn()){  
                    scope.ngModel = iElem[0].value;
                }
            }
        }
    };
});

Example HTML:

<div ng-app="bindApp" ng-controller="bindToViewCtrl">
    <form name="bindForm">
        <label> Zip Code
            <input type="text" ng-pattern="zip" required ng-model="formData.zip" bind-to-view="formData.bindToView" name="zipCode"/>
        </label>
        <span>$scope.formData.zip: {{formData.zip}}</span>
        <br/>
        <button ng-click="formData.bindToView = !formData.bindToView">
            Bind to View is {{formData.bindToView ? 'On' : 'Off' }}
        </button> 
    </form>
</div>

Comments

0

make sure you are injecting $scope on that controller initialization, because i got same issue with typeahead auto complete, i fix this issue by set validity on saving as below:

 if (!self.editForm.$valid && self.editForm.txtCustomer.$invalid) {//workaround to fix typeahead validation issue.
                self.editForm.txtCustomer.$setValidity('editable', true);
            }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.