Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AngularJS ng-model value is lost after custom validation directive is triggered

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?

like image 427
David Z. Avatar asked Aug 14 '14 22:08

David Z.


People also ask

What is difference between ng model and Ng bind directive?

ngModel usually use for input tags for bind a variable that we can change variable from controller and html page but ngBind use for display a variable in html page and we can change variable just from controller and html just show variable. Save this answer.

Is NG model a directive?

The ngModel directive is a directive that is used to bind the values of the HTML controls (input, select, and textarea) or any custom form controls, and stores the required user value in a variable and we can use that variable whenever we require that value. It also is used during form validations.

What is Ng Onchange?

ngOnChanges gets called before ngOnInit and whenever a component's bound input is changed FROM THE PARENT COMPONENT. Remember that ngOnChanges is specific to bound inputs on the component. This means if you don't have any @Input properties on a child, ngOnChanges will never get called.

Which ng directive is used to initialize?

The ng-init Directive is used to initialize AngularJS Application data. It defines the initial value for an AngularJS application and assigns values to the variables. The ng-init directive defines initial values and variables for an AngularJS application.


2 Answers

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.

like image 73
Silvio Lucas Avatar answered Sep 24 '22 03:09

Silvio Lucas


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;
});
like image 29
Clark Pan Avatar answered Sep 24 '22 03:09

Clark Pan