Angular.js: Setting debounce via ngModel.$$setOptions does not seem to work (allow programatically setting ngModelOptions)

Created on 18 Sep 2015  路  26Comments  路  Source: angular/angular.js

Consider this simple directive:

function AwesomeDirective () {
  return {
    restrict: 'A',
    require: 'ngModel',
    link: function ($scope, element, attrs, ngModel) {
      ngModel.$$setOptions({
        debounce: 300
      });

      ngModel.$asyncValidators.validator = function () {
        // Will not get executed.
      };
    }
  };
}

All is dandy and fine when setting ng-model-options via (in same directive):

element.attr('ng-model-options', '{debounce: 300}');

Usage of directive is like so:

<input type="text" ngModel="model" awesome-directive>

Angular version - 1.3.4

EDIT: Plunker http://plnkr.co/edit/a1rktAnIR0Gazv3v44q4

EDIT2: I am a noob, and element.attr('ng-model-options', '{debounce: 300}'); does not actually do anything. The main issue still exists thought:

It looks like ngModel.$asyncValidators.validator will not get invoked (it will only single time, when starting) when there is debounce.

forms low confusing feature

Most helpful comment

For those late to the party: looks like there is an $overrideModelOptions method exactly for this.

All 26 comments

$$setOptions is a quasi private API, you should not rely on it. Or do you mean that async validators never get invoked correctly when you have a debounce set?

Yeah, I know I should not, but there is no other way to do so, when directive and model is on same element (without defining ng-model-options in the view, of course).

And yes, once I set debounce this way, validator is never invoked

The "problem" is that ngModelOptions do some other stuff than merely setting the $options attribute.
I.e. they also set updateOnDefault if necessary and remove default from the updateOn value (if present).

You can get around this by changing ngModel.$$setOptions({debounce: 300}); to ngModel.$$setOptions({debounce: 300, updateOnDefault: true});.
Nevertheless, it's NOT recommended to use private/internal API (as mentioned by @Narretz), because your apps may break without notice.

Yes, I am aware of dangers of using internal API.

Although, it would be nice, if ngModelController exposed $setOptions and would behave exactly the same as ng-model-options (which would then could simple call this.$setOptions(opts)).

I think that would be extremely useful in situations like this, where accessing DOM is not ideal, or if one does not want to set ng-model-options in the view. WDYT?

@jutaz, sounds reasonable.
@Narretz, wdyt about moving all logic in $setOptions and making it part of the public API ?

IIRC there are plans (or at least thoughts) about making ngModelOptions inherit (instead of overwrite) parent ngModelOptions. If there is a refatoring coming to ngModelOptions, we should see if/how the proposed change fits into it.

I think that would bring terrific value!

If there is not going to be some big refactor you guys have in mind, I would like to submit a patch to implement this. Or is this something you guys are planning on doing yourselves? WDYT?

i do not think this would be a huge deal in terms of refactoring the functionality itself, since most of the code now lives in https://github.com/angular/angular.js/blob/24cd70058d081b93061734aec29e05f9e37abb3a/src/ng/directive/ngModel.js#L1241, which can be simply be moved to somewhere around https://github.com/angular/angular.js/blob/24cd70058d081b93061734aec29e05f9e37abb3a/src/ng/directive/ngModel.js#L249.

Let's do this in 1.6

That is amazing that this will be added! :clap:

@petebacondarwin now that 1.6.0 is released, $$setOptions no longer seems to exist. Has this been replaced by a public API of some sort? Can't find any reference to it in the changelog for 1.6.

There is a ngModel.$options.getOption() now, but no equivalent setOption() from what I can see. Are we meant to simply set the option, e.g. ngModel.$options.$$options.allowInvalid = true?

There is a new API method called createChild (see https://docs.angularjs.org/api/ng/type/ModelOptions) which can be used to achieve a programmatic effect. Here is an example: http://plnkr.co/edit/ow6EG1URu2Kw7vd4getR?p=preview

.directive('debounce', function() {
  return {
    require: 'ngModel',
    link: function(scope, element, attrs, ngModel) {
      var debounce = parseInt(attrs.debounce, 10);
      ngModel.$options = ngModel.$options.createChild({ debounce: debounce });
    }
  };
});

Thanks! I was misguided by the name, I didn't think it had anything to do with setting options.

So this would be a valid way to do it:

$ctrl.ngModel.$options = $ctrl.ngModel.$options.createChild({
  allowInvalid: true,
});

Yep. The reason we did it this way is because allowing these options objects to be mutated creates lots of potential corner cases where the inherited options are not updated correctly, or where sibling options are updated when not intended.

BTW, if you want to inherit existing options and only overwrite/set a specific one, you can use:

ngModel.$options.createChild({
  '*': '$inherit',
  allowInvalid: true
});

Doesn't create child already imply inheritance?

No. In order to avoid breaking changes, any kind of inheritance from an ngModelOptions parent is opt-in.
For values not directly specified on the options argument passed to chreateChild, children will have the default model options, unless you exlicitly request them to inherit from their parent.

Yes @gkalpak is right. I forgot about that.

It seems to be impossible to set updateOnDefault: true with controller.$options.createChild.

controller.$options = controller.$options.createChild({
    '*': '$inherit',
    updateOn: 'blur',
    updateOnDefault: true,
    debounce: {
        // delay input focus change by 0.5s
        blur: 500,
        // delay typing change by 1s
        default: 800,
    },
});

Then controller.$options.$$options.updateOnDefault is still false.

updateOnDefault is a private API. updateOn: 'default ...' should work.

For those late to the party: looks like there is an $overrideModelOptions method exactly for this.

@amalitsky Thanks a lot for mentioning that...much cleaner way to programatically set ngModelOptions! 馃憤

@amalitsky @gkalpak is $overrideModelOptions a publicly accessible replacement for the removed $$setOptions? Is it in effect an alternative for the createChild approach mentioned here above?

@adamreisnz, both are public, documented APIs, so you can use either. In fact $overrideModelOptions just calls createChild under the hood:

NgModelController.prototype.$overrdeModelOptions = function(options) {
  this.$options = this.$options.createChild(options);
}

Thanks. I didn't find the documentation of $overrideModelOptions very clear tbh.

We are happily accepting PRs with docs improvements 馃槢

And btw, the functions that are documented are always public, except otherwise noted

Was this page helpful?
0 / 5 - 0 ratings