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

fix(ngModelOptions): preserve context of getter/setters #10136

Closed
wants to merge 1 commit into from

Conversation

btford
Copy link
Contributor

@btford btford commented Nov 20, 2014

Closes #9394

BREAKING CHANGE: previously, ngModel invoked getter/setters in the global context.

For example:

<input ng-model="model.value" ng-model-options="{ getterSetter: true }">

would previously invoke model.value() in the global context.

Now, ngModel invokes value with model as the context.

It's unlikely that real apps relied on this behavior. If they did they can use .bind to explicilty
bind a getter/getter to the global context, or just reference globals normally without this.

@googlebot
Copy link

Thanks for your pull request.

It looks like this may be your first contribution to a Google open source project, in which case you'll need to sign a Contributor License Agreement (CLA) at https://cla.developers.google.com/.

If you've already signed a CLA, it's possible we don't have your GitHub username or you're using a different email address. Check the information on your CLA or see this help article on setting the email on your git commits.

Once you've done that, please reply here to let us know. If you signed the CLA as a corporation, please let us know the company's name.

@btford
Copy link
Contributor Author

btford commented Nov 20, 2014

oh googlebot, you so crazy

@googlebot
Copy link

CLAs look good, thanks Bri Bri!

pendingDebounce = null,
ctrl = this;

var ngModelGet = function ngModelGet() {
var modelValue = parsedNgModel($scope);
if (ctrl.$options && ctrl.$options.getterSetter && isFunction(modelValue)) {
modelValue = modelValue();
modelValue = $scope.$eval($attr.ngModel + '()');
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 feel just a tiny bit bad about this, but I think perf-wise and breaking change-wise it's the least harmful.

Copy link
Contributor

Choose a reason for hiding this comment

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

Uh, maybe I'm reading this wrong, but this will invoke $parse constantly... this seems like quite a performance risk, no?

Storing the parsed expression like @jbedard proposed is similar but only involves calling $parse once, on init, like how the non-getter/setter version of ngModel works.

Copy link
Contributor

Choose a reason for hiding this comment

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

$parse does cache it by the string, but I agree, this does still add a few calls ($eval + $parse) and a string concat to the watcher...

@jbedard
Copy link
Contributor

jbedard commented Nov 20, 2014

Another question... do we intentionally support the ngModelOptions object being modified after $$setOptions is called?

<input ng-model="model.value" ng-model-options="{ getterSetter: variableThatChangesRandomly }">

My comment about changing ngModelGet = parsedNgModel and overriding at $$setOptions would remove support for this because we would read getterSetter only once. I think removing support for this is good and removes a bunch of weird edge cases, but it could be considered a breaking change...

@btford
Copy link
Contributor Author

btford commented Nov 20, 2014

do we intentionally support the ngModelOptions object being modified

Nope. I agree about this being a potential source of headaches. I think getter/setters already gives devs enough dynamicity.

@jbedard
Copy link
Contributor

jbedard commented Nov 20, 2014

I think something a little closer to jbedard@3392351 would be nice then...

@btford
Copy link
Contributor Author

btford commented Nov 20, 2014

@jbedard – nice!

$parse already memoizes parsed expressions, so I don't think your change adds too much in terms of run-time performance (just a few lookups, maybe a bit of GC pressure from string concats), and possibly introduces additional memory costs from the additional closure.

I think if we continue discussion down this path we should have a benchmark for it.

if (ctrl.$options && ctrl.$options.getterSetter && ctrl.$options.getterSetterContext) {
// Use the provided context expression to specify the context used when invoking the
// getter/setter function
parsedNgModelContext = $parse(ctrl.$options.getterSetterContext);
Copy link
Contributor

Choose a reason for hiding this comment

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

This is unused

@NevilleS
Copy link
Contributor

The "trick" of adding () is nice because it doesn't require an API change and kinda just does the "right" thing. It breaks in at least one odd case, for expressions like: ng-model="someService.expr;", since appending parentheses after a semicolon explodes as you might expect.

But if you can make the error message helpful in that case, maybe it's OK?

@NevilleS
Copy link
Contributor

Amusingly, something else like ng-model="someService.getterSetter || fallbackService.getterSetter" would have some interesting behaviour when you append parentheses too 😄

@jbedard
Copy link
Contributor

jbedard commented Nov 20, 2014

:|

We could do $parse("(" + ngModel + ")()"), I think losing the context in that case would be correct...

@jbedard
Copy link
Contributor

jbedard commented Nov 20, 2014

Actually what I said in that last comment won't work. Doing (ngModel)() will make it lose the context in every case :(

I've updated mine to be not as breaking jbedard@081195a. The main advantage is that it only checks options.getterSetter once on initialization and if it is false the watcher method invoked per-digest is much simpler (better performance). I'm thinking this change is breaking enough we may as well break one more thing and fulfill the todo I added in that commit though. That would make it a bit simpler and better performance for the getterSetter case as well.

Sorry if I'm spamming this PR. Should I open another one instead?

@NevilleS
Copy link
Contributor

I don't think your updated PR handles the weird cases I was talking about... the test passes, but only because your ternary expression is wrapped in brackets 😃

In any case, I think the examples I provided were pretty contrived. 99.999% of ngModel expressions are just simple scope bindings like data or user.name, so implicitly appending () to them will turn those into getters, and solve the initial problem in #9394, which was that services/typescript objects were being invoked without the necessary context. Therefore, I think your earlier one (jbedard/angular.js@3392351) is pretty close, I'd just think to cover the bases we should improve the error message to provide some guidance in the weird cases where $attrs.ngModel + "()" results in an invalid expression, because currently I'd be pretty confused to see a $parse error for an expression that I didn't even write!

Unless of course someone can come up with a non-contrived counter example where this won't work, because I still have an uneasy feeling I can't quite shake...

@caitp
Copy link
Contributor

caitp commented Nov 21, 2014

(tentatively moving this to the next milestone --- rearrange at your discretion)

@btford
Copy link
Contributor Author

btford commented Nov 22, 2014

I like the approach in jbedard@081195a.

I've updated my PR accordingly, and made the tests around this feature more explicit.

I'm not interested in supporting weird ternary expressions for ngModelOptions. If you want conditional logic like that, it should belong in a function within your scope. There's no reason to allow yet another level of indirection in the template there.

@btford btford force-pushed the pr-9865 branch 2 times, most recently from 0e94771 to d0a2f74 Compare November 22, 2014 19:57
@jbedard
Copy link
Contributor

jbedard commented Nov 22, 2014

That approach will also help with #9609 by removing the wrapper and options/getterSetter check from the watch function.

If you want to simplify it one step further, with an additional breaking change, then jbedard@3392351 is even simpler where getterSetter mode only supports getterSetter functions and no longer supports plain values. I think this makes things simpler, but it is a bigger breaking change.

@lgalfaso
Copy link
Contributor

otherwise, LGTM

Many thanks to @NevilleS and @jbedard for collaborating with me on a solution to this!

Closes angular#9394
Closes angular#9865

BREAKING CHANGE: previously, ngModel invoked getter/setters in the global context.

For example:

```js
<input ng-model="model.value" ng-model-options="{ getterSetter: true }">
```

would previously invoke `model.value()` in the global context.

Now, ngModel invokes `value` with `model` as the context.

It's unlikely that real apps relied on this behavior. If they did they can use `.bind` to explicilty
bind a getter/getter to the global context, or just reference globals normally without `this`.
@btford
Copy link
Contributor Author

btford commented Nov 22, 2014

Landed as bb4d3b7.

Thanks everyone! 👯

@btford btford closed this Nov 22, 2014
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.

ngModelOption's getterSetter does not work with Factory objects
6 participants