Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

Ng model options extend #15389

Closed

Conversation

petebacondarwin
Copy link
Contributor

What kind of change does this PR introduce? (Bug fix, feature, docs update, ...)

feat

What is the current behavior? (You can also link to an open issue here)

ngModelOptions cannot be inherited from ancestor ngModelOptions

What is the new behavior (if this is a feature change)?

ngModelOptions can inherit if specified via $inherit values

Does this PR introduce a breaking change?

A small internal-ish change to the programmatic API for reading options.

Please check if the PR fulfills these requirements

Other information:

@@ -0,0 +1,378 @@
'use strict';

/* global $DefaultModelOptionsProvider: true */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't it be exported, rather than global?

* @name $defaultModelOptionsProvider
* @description
*
* Here, you can change the default settings from which {@link ngModelOptions}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This service is not only used for ngModelOptions, but for ngModel as well.
It would be good to clarify that, so people know what they are modifying 😃

* @description
* This directive allows you to modify the behaviour of ngModel and input directives within your
* application. You can specify an ngModelOptions directive on any element and the settings affect
* the ngModel and input directives on all descendent elements.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on all descendent elements

Not exactly 😃 It is worth clarifying that it doesn't (necessarily) affect elements past its closest ngModelOptions descendant.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK

* @name ngModelOptions
*
* @description
* This directive allows you to modify the behaviour of ngModel and input directives within your
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why input directives? It only affects ngModel afaict.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK OK

* ```
*
* Notice that the `debounce` setting now inherits The value from the outer `<div>` element.
*
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is a good place to add a warning alert for library authors, mentioning that "*" should be used with caution, because of the possibility of new options being introduced in future versions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK

if (key === '*') {
inheritAll = true;
} else {
options[key] = isDefined(parentOptions[key]) ? parentOptions[key] : defaultOptions[key];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we always extended options with defaultOptions (see comment above), then here too we could simply use: options[key] = parentOptions[key]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK

* specified.
*/
$get: function() {
return new ModelOptions(defaultOptions, defaultOptions);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would simplify the ModelOptions implementation if defaultOptions was a top-level var, so we didn't have to pass it around from parent to child.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you suggesting we create a global variable!?!
I agree.

expect(inputOptions.getOption('updateOnDefault')).toEqual($defaultModelOptions.getOption('updateOnDefault'));
expect(inputOptions.getOption('debounce')).toEqual($defaultModelOptions.getOption('debounce'));
expect(inputOptions.getOption('getterSetter')).toEqual($defaultModelOptions.getOption('getterSetter'));
expect(inputOptions.getOption('allowInvalid')).toEqual($defaultModelOptions.getOption('allowInvalid'));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about timezone?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right

/* globals generateInputCompilerHelper: false */
describe('ngModelOptions', function() {

describe('$defaultModelOptions', function() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would add a test showing the default options are overwritable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually they are not :-P
I just need to fix the docs - see my other comment

'</form>')($rootScope);
$rootScope.$digest();

var inputElm = doc.find('input').eq(0);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Isn't .eq(0) redundant?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that find does not return the object that we want (i.e. some collection thing) so we must convert it to get an instance.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that it is not used elsewhere... I'll try removing it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

eq returns a new collection with a subset of the items (typically just one):

  eq: function(index) {
      return (index >= 0) ? jqLite(this[index]) : jqLite(this[this.length + index]);
  }

Here there is only one input element anyway, so it is redundant.

petebacondarwin added a commit to petebacondarwin/angular.js that referenced this pull request Nov 15, 2016
…ModelOptions`

Previously, you had to apply the complete set of `ngModelOptions` at every point where
you might want to modify just one or two settings.

This change allows more general settings to be applied nearer to the top of the DOM
and then for more specific settings to override those general settings further down
in the DOM.

To prevent unwanted inheritance you must opt-in on a case by case basis:
* To inherit as single property you simply provide the special value `"$inherit"`.
* To inherit all properties not specified locally then include a property `"*": "$inherit"`.

Closes angular#10922
Closes angular#15389

BREAKING CHANGE:

The programmatic API for `ngModelOptions` has changed. You must now read options
via the `ngModelController.getOption(name)` method, rather than accessing the option directly as a property
of the `ngModelContoller.$options` object. This does not affect the usage in templates and only
affects custom directives that might have been reading options for their own purposes.
…ModelOptions`

Previously, you had to apply the complete set of `ngModelOptions` at every point where
you might want to modify just one or two settings.

This change allows more general settings to be applied nearer to the top of the DOM
and then for more specific settings to override those general settings further down
in the DOM.

To prevent unwanted inheritance you must opt-in on a case by case basis:
* To inherit as single property you simply provide the special value `"$inherit"`.
* To inherit all properties not specified locally then include a property `"*": "$inherit"`.

Closes angular#10922
Closes angular#15389

BREAKING CHANGE:

The programmatic API for `ngModelOptions` has changed. You must now read options
via the `ngModelController.getOption(name)` method, rather than accessing the option directly as a property
of the `ngModelContoller.$options` object. This does not affect the usage in templates and only
affects custom directives that might have been reading options for their own purposes.
this.$$options[key] = this.$$parentOptions[key];
}
}
}, this);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Who needs arrow functions 😄

* { allowInvalid: true, updateOn: 'default', debounce: 200 }
* ```
*
* Notice that the `debounce` setting now inherits The value from the outer `<div>` element.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The value --> the value

$$processOptions: function() {
var options = this.$$options;
// updateOn and updateOnDefault
if (isDefined(options.updateOn) && options.updateOn.trim()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to handle this differently now. For example:

<div ng-model-options="{updateOn: 'default blur'}">
  <div ng-model-options="{'*': '$inherit'}"></div>
</div>
  • The outer ModelOptions will have updateOn: 'blur', updateOnDefault: true.
  • During the $$extendParent() phase, the values will be copied onto the inner ModelOptions.
  • Then, during the $$processOptions phase, updateOn: 'blur' will be treated as the initial value and updateOnDefault will be reset to false.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right! Now this is a good review point. I knew there must be something that needed fixing. I'll take a look right now.

…ModelOptions`

Previously, you had to apply the complete set of `ngModelOptions` at every point where
you might want to modify just one or two settings.

This change allows more general settings to be applied nearer to the top of the DOM
and then for more specific settings to override those general settings further down
in the DOM.

To prevent unwanted inheritance you must opt-in on a case by case basis:
* To inherit as single property you simply provide the special value `"$inherit"`.
* To inherit all properties not specified locally then include a property `"*": "$inherit"`.

Closes angular#10922
Closes angular#15389

BREAKING CHANGE:

The programmatic API for `ngModelOptions` has changed. You must now read options
via the `ngModelController.getOption(name)` method, rather than accessing the option directly as a property
of the `ngModelContoller.$options` object. This does not affect the usage in templates and only
affects custom directives that might have been reading options for their own purposes.
@@ -244,7 +245,7 @@ function NgModelController($scope, $exceptionHandler, $attr, $element, $parse, $
this.$pending = undefined; // keep pending keys here
this.$name = $interpolate($attr.name || '', false)($scope);
this.$$parentForm = nullFormCtrl;
this.$options = $modelOptions;
this.$options = $defaultModelOptions;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Typically, internal/private variables are not prefixed. The $ prefix is supposed to indicate public stuff.

} else {
options[key] = this.$$options[key];
}
} else if (key === 'updateOn') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are still some issues with this implementation:

  • If {updateOn: '$inherit'} it will inherit the updateOn, but not the updateOnDefault (which might be inherited by $defaultModelOptions).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great. I am really working your reviewing skills today!

petebacondarwin added a commit to petebacondarwin/angular.js that referenced this pull request Nov 16, 2016
…ModelOptions`

Previously, you had to apply the complete set of `ngModelOptions` at every point where
you might want to modify just one or two settings.

This change allows more general settings to be applied nearer to the top of the DOM
and then for more specific settings to override those general settings further down
in the DOM.

To prevent unwanted inheritance you must opt-in on a case by case basis:
* To inherit as single property you simply provide the special value `"$inherit"`.
* To inherit all properties not specified locally then include a property `"*": "$inherit"`.

Closes angular#10922
Closes angular#15389

BREAKING CHANGE:

The programmatic API for `ngModelOptions` has changed. You must now read options
via the `ngModelController.getOption(name)` method, rather than accessing the option directly as a property
of the `ngModelContoller.$options` object. This does not affect the usage in templates and only
affects custom directives that might have been reading options for their own purposes.
petebacondarwin added a commit to petebacondarwin/angular.js that referenced this pull request Nov 16, 2016
…ModelOptions`

Previously, you had to apply the complete set of `ngModelOptions` at every point where
you might want to modify just one or two settings.

This change allows more general settings to be applied nearer to the top of the DOM
and then for more specific settings to override those general settings further down
in the DOM.

To prevent unwanted inheritance you must opt-in on a case by case basis:
* To inherit as single property you simply provide the special value `"$inherit"`.
* To inherit all properties not specified locally then include a property `"*": "$inherit"`.

Closes angular#10922
Closes angular#15389

BREAKING CHANGE:

The programmatic API for `ngModelOptions` has changed. You must now read options
via the `ngModelController.getOption(name)` method, rather than accessing the option directly as a property
of the `ngModelContoller.$options` object. This does not affect the usage in templates and only
affects custom directives that might have been reading options for their own purposes.
@petebacondarwin
Copy link
Contributor Author

OK, trying again. See if you can find another failing scenario @gkalpak

Copy link
Member

@gkalpak gkalpak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fourth time's a charm 😃
LGTM

it('should `updateOnDefault` as well if we have `updateOn: "$inherit"`', function() {
var container = $compile(
'<div ng-model-options="{updateOn: \'keyup\'}">' +
'<input ng-model-options="{\'updateOn\': \'$inherit\'}">' +
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary quotes around updateOn (here and below).

…ModelOptions`

Previously, you had to apply the complete set of `ngModelOptions` at every point where
you might want to modify just one or two settings.

This change allows more general settings to be applied nearer to the top of the DOM
and then for more specific settings to override those general settings further down
in the DOM.

To prevent unwanted inheritance you must opt-in on a case by case basis:
* To inherit as single property you simply provide the special value `"$inherit"`.
* To inherit all properties not specified locally then include a property `"*": "$inherit"`.

Closes angular#10922
Closes angular#15389

BREAKING CHANGE:

The programmatic API for `ngModelOptions` has changed. You must now read options
via the `ngModelController.getOption(name)` method, rather than accessing the option directly as a property
of the `ngModelContoller.$options` object. This does not affect the usage in templates and only
affects custom directives that might have been reading options for their own purposes.
ellimist pushed a commit to ellimist/angular.js that referenced this pull request Mar 15, 2017
…ModelOptions`

Previously, you had to apply a complete set of `ngModelOptions` at many places in
the DOM where you might want to modify just one or two settings.

This change allows more general settings to be applied nearer to the root of the DOM
and then for more specific settings to inherit those general settings further down
in the DOM.

To prevent unwanted inheritance you must opt-in on a case by case basis:
* To inherit as single property you simply provide the special value `"$inherit"`.
* To inherit all properties not specified locally then include a property `"*": "$inherit"`.

Closes angular#10922
Closes angular#15389

BREAKING CHANGE:

The programmatic API for `ngModelOptions` has changed. You must now read options
via the `ngModelController.$options.getOption(name)` method, rather than accessing the
option directly as a property of the `ngModelContoller.$options` object. This does not
affect the usage in templates and only affects custom directives that might have been
reading options for their own purposes.

One benefit of these changes, though, is that the `ngModelControler.$options` property
is now guaranteed to be defined so there is no need to check before accessing.

So, previously:

```
var myOption = ngModelController.$options && ngModelController.$options['my-option'];
```

and now:

```
var myOption = ngModelController.$options.getOption('my-option');
```
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants