AngularJS set global ngModelOptions

2019-02-01 07:08发布

问题:

The default behaviour for updating ngModel (and subsequently validation) is on change; I would like to change that to on blur. The docs only explain how to do this on a case-by-case basis via: <ANY ng-model-options="{ updateOn: 'blur' }"></ANY>. I went so far as to look thru the source code, but somehow neither ngModelOptions nor ng-model-options is found (despite both occurring in the documentation, which is scraped from the source code).

回答1:

While the ngModel decorators written by Jon/John provide a good behind the scene solution, one must also be aware that declaratively, ngModelOptions need not have to be declared at the individual input field level but can be declared at the module level.

<body ng-app = "myApp" ng-model-options="{ updateOn: 'blur' }">

Doing the above would have all the input fields in myApp module inherit the ng-model-options. These can then be overriden in specific input fields if required (eg. search filters).

This plunker demonstrates: http://plnkr.co/edit/2L1arGgHJwK82xVucJ4p?p=preview



回答2:

As @Angad mentions you can set ng-model-options on any element, and it will be applied to all its descendants. However, the problem with this is that if you set it like this, radios and checkboxes stop working as expected:

<body ng-app="myApp" ng-model-options="{ updateOn: 'blur' }">

This can be circumvented by adding change and click to the updateOn:

<body ng-app="myApp" ng-model-options="{ updateOn: 'change click blur' }">

If you would also want to update after a certain delay after typing you could use the following:

<body ng-app="myApp" ng-model-options="{ updateOn: 'keyup change click blur', debounce: { keyup: 500, click: 0, change: 0, blur: 0 } }">

I tested these techniques in Firefox, Chome, Internet Explorer (10, 11) and Safari. If you use other events then these, be sure to test it cross browser, for example radios fire change directly after clicking, on all browsers except IE.



回答3:

This is a really good question so I've written a more indepth blog article about this. The only real generic way I have come up with to do this is to decorate the ngModel directive as it is this directive that really uses the ngModelOptions.

If you look at the angular source for the ngModel directive it has a pre link function to effectively setup the ngModelOptions on the ngModelController through the use of the property $options. Note the $options is created in the ngModelOptionsDirective which is effectively a $eval on the ng-model-options attribute.

What we need to do in our ngModel dectorator is after this pre link function add a default value for this $options property if it is undefined. I am assuming here that if the developer has explicitly set the ngModelOptions in your project that they don't want it magically changed! Therefore we will only set the defaults if the $options property is undefined.

Here is the code:

(function (angular) {
'use strict';

angular.module('myAppOverridesModule').config(['$provide',
    function ($provide) {
        $provide.decorator('ngModelDirective', [
            '$delegate',
            function ($delegate) {
                var directive = $delegate[0],
                    link = directive.link,
                    shouldSetBlurUpdateEvent = function (nodeName, inputType) {
                        // The blur event is only really applicable to input controls so
                        // we want to stick with the default events for selects, checkboxes & radio buttons
                        return nodeName.toLowerCase() === 'textarea' ||
                               (nodeName.toLowerCase() === 'input' && 
                               inputType.toLowerCase() !== 'checkbox' && 
                               inputType.toLowerCase() !== 'radio');
                    };

                directive.compile = function () {
                    return function (scope, element, attrs, ctrls) {
                        var ngModelCtrl = ctrls[0];
                        link.pre.apply(this, arguments);

                        // if ngModelOptions is specified leave it unmodified as developer is explicitly setting it.
                        if (ngModelCtrl.$options === undefined && shouldSetBlurUpdateEvent(element[0].nodeName, element[0].type)) {
                            console.log('set');
                            ngModelCtrl.$options = {
                                updateOn: 'blur',
                                updateOnDefault: false
                            };
                        }

                        link.post.apply(this, arguments);
                    };
                };

                return $delegate;
            }
        ]);
    }
]);
}(angular));

UPDATE: I've updated the code to ignore selects, checkboxes & radio buttons as the blur event is not the optimal update event for them.



回答4:

I modified Jon Samwell's answer because I couldn't get his to work anymore.

This overrides the ngModelDirective's compile function by storing a reference to it then returning the pre/postLink functions w/ calls to their originals plus our extra override code.

Enjoy!

app.config(function($provide) {

    $provide.decorator('ngModelDirective', function($delegate) {
        var directive = $delegate[0],
            link = directive.link,
            shouldSetBlurUpdateEvent = function (nodeName, inputType) {
              // The blur event is only really applicable to input controls so
              // we want to stick with the default events for selects, checkboxes & radio buttons
              return nodeName.toLowerCase() === 'textarea' ||
                    (nodeName.toLowerCase() === 'input' && 
                     inputType.toLowerCase() !== 'checkbox' && 
                     inputType.toLowerCase() !== 'radio');
                };

        // save a reference to the original compile function
        var compileFn = directive.compile;

        directive.compile = function () {   

            var link = compileFn.apply(this, arguments);

            return {
                pre: function ngModelPostLink(scope, element, attr, ctrls) {

                    if(!ctrls[2]) {
                        ctrls[2] = {};
                    }

                    var ngModelOptions = ctrls[2];

                    if (ngModelOptions.$options === undefined && shouldSetBlurUpdateEvent(element[0].nodeName, element[0].type)) {
                        ngModelOptions.$options = {
                            updateOn: 'blur',
                            updateOnDefault: false
                        };
                    }

                    link.pre.apply(this, arguments);
                },
                post: link.post
            };
        };

        return $delegate;
    });

});


回答5:

Instead of using

ng-model-options="{ updateOn: 'blur' }

you could use

ng-model-options="{ debounce : { default : 500 } }"

and apply it to a parent element in the dom, such as a container div. The debounce setting above tells Angular to only evaluate the validation rules after 500 milliseconds of no activity.

Using debounce this way is superior to using blur because blur creates problems for radio and checkboxes when applied to the entire form.

The debounce option is an integer value which causes the model update to delay an integer number of milliseconds. It reduces the frequency of $digest cycles occurring, which causes the js application to consume less resources as well as allowing the user time to type before validation rules are applied.

YearOfMoo.com recomends using blur and debounce together like this

ng-model-options="{ debounce : { default : 500, blur : 0 } }"

Now model value and validations are applied immediately after the user blurs out of the field. This sets a wait time of 0 ms for the blur event. Blur overrides the default value.