AngularJS ng-model value is lost after custom vali

2020-07-11 06:46发布

问题:

I created a custom validation directive and used it in a form. It can be triggered with no problem, but after the validation is triggered, I found that the model value is just lost. Say I have

ng-model="project.key" 

and after validation, project.key doesn't exist in the scope anymore. I think somehow I understood AngularJS wrong and did something wrong.

Code speaks.

Here is my html page:

 <div class="container">
    ...
    <div class="form-group"
            ng-class="{'has-error': form.key.$invalid && form.key.$dirty}">
            <label for="key" class="col-sm-2 control-label">Key</label>
            <div class="col-sm-10">
                <input type="text" class="form-control text-uppercase" name="key"
                    ng-model="project.key" ng-model-options="{ debounce: 700 }"
                    placeholder="unique key used in url"
                    my-uniquekey="vcs.stream.isProjectKeyValid" required />
                <div ng-messages="form.key.$error" ng-if="form.key.$dirty"
                    class="help-block">
                    <div ng-message="required">Project key is required.</div>
                    <div ng-message="loading">Checking if key is valid...</div>
                    <div ng-message="keyTaken">Project key already in use, please
                        use another one.</div>
                </div>
            </div>
        </div>
    <div class="col-sm-offset-5 col-sm-10">
        <br> <a href="#/" class="btn">Cancel</a>
        <button ng-click="save()" ng-disabled="form.$invalid"
            class="btn btn-primary">Save</button>
        <button ng-click="destroy()" ng-show="project.$key"
            class="btn btn-danger">Delete</button>
    </div>
</form>

And here's my directive:

    .directive('myUniquekey', function($http) {
        return {
            restrict : 'A',
            require : 'ngModel',
            link : function(scope, elem, attrs, ctrl) {
                var requestTypeValue = attrs.myUniquekey;

                ctrl.$parsers.unshift(function(viewValue) {
                    // if (viewValue == undefined || viewValue == null
                    // || viewValue == "") {
                    // ctrl.$setValidity('required', false);
                    // } else {
                    // ctrl.$setValidity('required', true);
                    // }

                    setAsLoading(true);
                    setAsValid(false);

                    $http.get('/prism-cmti/2.1', {
                        params : {
                            requestType : requestTypeValue,
                            projectKey : viewValue.toUpperCase()
                        }
                    }).success(function(data) {
                        var isValid = data.isValid;
                        if (isValid) {
                            setAsLoading(false);
                            setAsValid(true);

                        } else {
                            setAsLoading(false);
                            setAsValid(false);
                        }
                    });

                    return viewValue;
                });

                function setAsLoading(bool) {
                    ctrl.$setValidity('loading', !bool);
                }

                function setAsValid(bool) {
                    ctrl.$setValidity('keyTaken', bool);
                }

            }
        };
    });

Here's the controller for the form page:

angular.module('psm3App').controller(
        'ProjectCreateCtrl',
        [ '$scope', '$http', '$routeParams', '$location',
                function($scope, $http, $routeParams, $location) {
                    $scope.save = function() {
                            $http.post('/prism-cmti/2.1', {requestType:'vcs.stream.addProject', project:$scope.project})
                            .success(function(data) {
                                $location.path("/");
                            });
                        };
                }]);

Before this bug, somehow I need to handle the required validation in my custom validation directive too, if I don't do it, required validation would go wrong. Now I think of it, maybe the root cause of these two problems is the same: the model value is gone after my directive link function is triggered.

I'm using Angular1.3 Beta 18 BTW.

Any help is appreciated. Thanks in advance.

Update: Followed @ClarkPan's answer, I updated my code to return viewValue in ctrl.$parsers.unshift() immediately, which makes required validation works well now, so I don't need lines below any more.

        // if (viewValue == undefined || viewValue == null
                    // || viewValue == "") {
                    // ctrl.$setValidity('required', false);
                    // } else {
                    // ctrl.$setValidity('required', true);
                    // }

But the {{project.key}} still didn't get updated. Then I tried to comment out these two lines here:

                    setAsLoading(true);
                    setAsValid(false);

Model value {{project.key}} got updated. I know that if any validation fails, the model value will be cleared, but I thought

                      function(data) {
                            var isValid = data.isValid;
                            if (isValid) {
                                setAsLoading(false);
                                setAsValid(true);
                            } else {
                                setAsLoading(false);
                                setAsValid(false);
                            }
                        }

in $http.get(...).success() should be executed in $digest cycle, which means the model value should be updated.

What is wrong?

回答1:

This is happening because angular does not apply any change to the scope and $modelValue if there is any invalid flag set in the model. When you start the validation process, you are setting the 'keyTaken' validity flag to false. That is telling to angular not apply the value to the model. When the ajax response arrives and you set the 'keyTaken' validity flag to true, the $modelValue was already set to undefined and the property 'key' was gone. Try to keep all validity flags set to true during the ajax request. You must avoid the calls to setAsLoading(true) and setAsValid(false) before the ajax call and keep all validity flags set to true. Only after the ajax response set the validity flag.



回答2:

NOTE:This answer below only applies if you're using angular versions prior to 1.3 (before they introduced the $validators concept).


From my reading of your myUniqueKey directive, you want to validate the projectkey asynchronously. If that is the case, that would be your problem. ngModel's $parser/$formatter system doesn't expect asynchronous calls.

The anonymous function you used in the $parsers array does not return a value, as $http is an asynchronous method that returns a method. You'll want to return the viewValue immediately from that method.

Then in the .success callback of your $http call, you can ten set the validity and loading status. I don't recommend you try to change the viewValue (unless that is not your purpose in returning either undefined or viewValue) at this point as it will probably trigger another run of the $parsers.

So:

ctrl.$parsers.unshift(function(viewValue){
    //...omitted for clarity

    $http.get(
        //...
    ).success(function(data){
        setAsLoading(false);
        setAsValid(data.isValid);
    });

    //... 

    return viewValue;
});


回答3:

If the value is not valid, by default the model will not be updated (as explained in the accepted answer), but you can make the model to be updated in any case by using allowInvalid in ng-model-options

For the input field in the question:

<input type="text" class="form-control text-uppercase" name="key"
                ng-model="project.key" ng-model-options="{ debounce: 700, 
                allowInvalid: true }"
                placeholder="unique key used in url"
                my-uniquekey="vcs.stream.isProjectKeyValid" required />