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

feat(ngModel): add ngModelContext for getter/setter bindings #9865

Closed
wants to merge 1 commit into from

Conversation

NevilleS
Copy link
Contributor

Adds an optional attribute, ng-model-context, that will be evaluated to provide the calling
context used when invoking the ngModel getter/setter function. When not provided, falls back to the
existing behavior of invoking getter/setter functions from the global context.

Closes #9394

@mary-poppins
Copy link

I'm sorry, but I wasn't able to verify your Contributor License Agreement (CLA) signature. CLA signature is required for any code contributions to AngularJS.

Please sign our CLA and ensure that the CLA signature email address and the email address in this PR's commits match.

If you signed the CLA as a corporation, please let us know the company's name.

Thanks a bunch!

PS: If you signed the CLA in the past then most likely the email addresses don't match. Please sign the CLA again or update the email address in the commit of this PR.
PS2: If you are a Googler, please sign the CLA as well to simplify the CLA verification process.

@NevilleS
Copy link
Contributor Author

Respectfully speaking, @mary-poppins, I think you're mistaken 😄

@gkalpak
Copy link
Member

gkalpak commented Nov 1, 2014

Why add a new directive, since we already have ngModelOptions ?

@NevilleS
Copy link
Contributor Author

NevilleS commented Nov 1, 2014

Yeah, that would make this a bit nicer, I could move this into a property
on ngModelOptions like getterSetterContext, which would be more obvious
to users.

Fundamentally the feature would be the same though, and it's still a little
weird that you specify the context manually like this... Any comment on how
it's actually used by ngModel?

I'll update the PR to use ngModelOptions instead later today.

On Nov 1, 2014 2:57 AM, "Georgios Kalpakas" notifications@github.com
wrote:

Why add a new directive, since we already have ngModelOptions ?


Reply to this email directly or view it on GitHub.

@jbedard
Copy link
Contributor

jbedard commented Nov 1, 2014

Why add anything new? Could this not be done without adding to the API. Something such as...

var ngModelContext = $attr.ngModel.match(/(.+)\./);
var parsedNgModelContext = ngModelContext && $parse(ngModelContext[1]);

If using a hacky regex doesn't cover some cases then maybe $parse could be changed to expose the context or something like that. But this way it doesn't involve any new API and additional options/attributes etc.

@NevilleS
Copy link
Contributor Author

NevilleS commented Nov 1, 2014

I don't particularly like this kind of "opt-in" behaviour and would prefer to see $parse do The Right Thing™ (see the comments on the issue #9394), but there were performance concerns with that. Although this hacky regex would avoid those by being self-contained to users of getterSetter. I can't come up with a case where that hacky regex doesn't work right now...

If we did do this, there'd be no way to use the current behaviour, and all uses of getterSetter would start using the "right" context instead of the global context. Would that constitute a breaking change? I can come up with applications that would be broken by that, but they involve stashing things on $window, so...

@jbedard
Copy link
Contributor

jbedard commented Nov 1, 2014

Things like getMyGetterSetter('string.with.dots') wouldn't work very well. I guess the regex could just change a bit to fix that though, something such as /^(.+)\.[\w$]+$/?

I think the default context should be null, not the window. You're right that could be a breaking change, but I'd hope not many people would be depending on that :|

@NevilleS
Copy link
Contributor Author

NevilleS commented Nov 1, 2014

I thought of that case, but I don't think it's really an issue: isn't that an invalid getterSetter function? ngModel already assumes it can invoke it with an argument, i.e. getterSetter(ctrl.$modelValue), so...

@NevilleS
Copy link
Contributor Author

NevilleS commented Nov 1, 2014

Also if we are breaking the default context to not be window, I'd argue that the $scope should be the new default...

@jbedard
Copy link
Contributor

jbedard commented Nov 1, 2014

Ya maybe $scope would be better, just please not the window!

I think that case is valid - the ngModel expression gets executed to return a function, then that returned function is the getterSetter which gets called, it doesn't matter what type of expression that original ngModel was.

@NevilleS
Copy link
Contributor Author

NevilleS commented Nov 1, 2014

Hrm, that's fair, but I don't think ngModel currently supports providing an expression that returns a getterSetter. You can see that here: http://plnkr.co/edit/pqyebcDFw4BjSpe8dJqT?p=preview

@NevilleS
Copy link
Contributor Author

NevilleS commented Nov 1, 2014

Wait - nevermind, it does work like you described, I had a typo in there... Gross

@NevilleS
Copy link
Contributor Author

NevilleS commented Nov 1, 2014

Also, I'm pretty sure your regex is defeated by any string with non-word characters, e.g.

<input ng-model="contextObj.weirdLookupFn('the keyword is... !@#%^%')" ng-model-options="{ getterSetter: true }">

My gut says that regexes can't solve the problem of determining the calling context generically, you need the tokenizing that $parse implements...

@jbedard
Copy link
Contributor

jbedard commented Nov 1, 2014

That should still be covered by the second regex which ensures it ends with
only alphanumeric, _ and $...

@NevilleS
Copy link
Contributor Author

NevilleS commented Nov 1, 2014

Hmm, maybe I'm misunderstanding you...?

"contextObj.weirdLookupFn('the keyword is... !@#%^%')".match(/^(.+)\.[\w$]+$/)
// returns null

@jbedard
Copy link
Contributor

jbedard commented Nov 1, 2014

That looks correct, it should be null and use the default context...

@NevilleS
Copy link
Contributor Author

NevilleS commented Nov 1, 2014

OK I was misunderstanding you, I thought you were trying to get expressions like that to match and return contextObj. I suppose that seems safe.

It occurs to me that we could also just ask $parse if the ngModel is assignable, and if so, the original regex is probably also good, no?

@NevilleS NevilleS force-pushed the add-ng-model-context branch from 776472b to 591b036 Compare November 2, 2014 02:17
@NevilleS
Copy link
Contributor Author

NevilleS commented Nov 2, 2014

OK, I updated this to automatically determine the context instead. I definitely prefer this... Let me know if it's still too weird to use, though, or if there's a case where /(.+)\./ doesn't determine the correct context for an assignable ngModel expression...

@jbedard
Copy link
Contributor

jbedard commented Nov 2, 2014

That looks right. That test makes me lean toward making the default context null instead of the scope though, but that's just my opinion (and my examples below all use the scope as context still).

Might be able to make it a bit simpler though? jbedard@6188949

Or honestly I think the current getterSetter implementation is weird how it allows plain values even if getterSetter is true, and I'm not sure if it intentionally allows ngModelOptions.getterSetter to change at any time (there's no tests enforcing that one). Removing those 2 abilities would be a breaking change but I think it would simplify things a lot and improve $watch performance a bit (in all cases, getterSetter or not): jbedard@3392351

Those are just ideas though. Someone on the angular team should probably make these decisions :P

@NevilleS
Copy link
Contributor Author

NevilleS commented Nov 2, 2014

I like setting up the parsedNgModel context during $$setOptions, that's definitely a good suggestion and will simplify my implementation quite a bit. I still don't trust that regex entirely (sorry), so I'd prefer to keep the assignable check there instead... but maybe I'm being too paranoid. I'll update again, thx

I assume the Angular team will chime in eventually 😄

@NevilleS NevilleS force-pushed the add-ng-model-context branch from 591b036 to 807d947 Compare November 2, 2014 04:53
@gkalpak
Copy link
Member

gkalpak commented Nov 2, 2014

With trying it, wouldn't it be possible to have array notation instead of dot notation ?
E.g. someObject[someGetter] (or simply someObject['getter']) would not be properly recognized by the used RegExp.

UPDATE:
Tried it and it is indeed possible to use array notation. So, that RegExp needs more work I guess (or maybe a RegExp is not sufficient to handle this).

@NevilleS
Copy link
Contributor Author

NevilleS commented Nov 3, 2014

Yup, you're totally right... And since array notation uses an enclosed string which can have any characters, I think it safely rules out using a regex to determine the context, unless someone more well versed in regex-fu can write something that returns the correct context for all of these:

"context.value"
"super.context.value"
"super('odd... get!').context.value"
"context['value']"
"context['value.foo']"
"context['well... f$%!@\"\'value\'\"[]']"

As I see it there are four options:

  1. Find a regex or some equivalent way to determine the context automatically... seems unlikely
  2. Provide context expr via ngModelOptions, e.g. ng-model-options={ getterSetter: true, getterSetterContext: "context" }
  3. Find a way to modify $parse to remember the context for dot/array operators, probably via a stack of some kind
  4. Do nothing, leave the current global context behaviour as is, update docs to warn people to bind their getter/setter functions manually to the appropriate context

Am I missing anything? What do you guys think makes the most sense?

@gkalpak
Copy link
Member

gkalpak commented Nov 3, 2014

I don't really use getterSetter, so I am probably not the right guy to have opinion on this one, but here is my order of preference:

  1. (2) <-- Worried about the runtime performance implications.
  2. (4) <-- Just use a closure, dude 😃
  3. (3) <-- Unless the context will be useful in cases besides ngModel + ngModelOptions + getterSetter: true, I don't like the idea of modifiying the whole parsing process for one "corner case".

@jbedard
Copy link
Contributor

jbedard commented Nov 3, 2014

And that is what I was afraid would happen using a regex like that...

I'd vote for jbedard@3392351 if the breaking change is acceptable. This will also improve performance a bit for the non getterSetter case.

@NevilleS
Copy link
Contributor Author

NevilleS commented Nov 3, 2014

@gkalpak re: the "do nothing" case, as I understand it the folks who use Typescript can't just "use a closure" since it's generated code, or something like that. So not providing an option like an implicit or explicit context parameter leaves them out of the party 😢

@gkalpak
Copy link
Member

gkalpak commented Nov 3, 2014

😢

@NevilleS
Copy link
Contributor Author

NevilleS commented Nov 7, 2014

So if we've abandoned hope of an implicit way to determine the context, I'll just go back to the explicit version, provided via ngModelOptions.

I still think that the default context should be changed from window to $scope though, but that'd still be technically breaking...

@NevilleS NevilleS force-pushed the add-ng-model-context branch from 807d947 to a9fa4d3 Compare November 7, 2014 21:11
@NevilleS
Copy link
Contributor Author

NevilleS commented Nov 7, 2014

OK, updated. Still not using the performance improvements recommended by @jbedard since that's a larger change to the behaviour of ngModelOptions.

…getter/setter bindings

Along with getterSetter, allow users to provide an expression via the getterSetterContext option.
This expression is evaluated to determine the context that should be used when invoking the ngModel
as a getter/setter function.

For example, <input ng-model="someObject.value" ng-model-options="{ getterSetter: true }"> would
previously invoke 'someObject.value()' from the global context. Now, users can specify context, like
ng-model-options="{ getterSetter: true, getterSetterContext: 'someObject'}", which would invoke
'someObject.value()' using 'someObject' as the calling context.

If getterSetterContext is not provided, fallback to using the current scope as the context.

Closes angular#9394

BREAKING CHANGE: previously, getter/setter functions would always be called from the global context.
This behaviour was unexpected by some users, as described in angular#9394, and is not particularly nice
anyways.  Applications that relied on this behaviour can use `$window` instead of `this` to access
the global object... but they probably shouldn't be storing global state anyways!
@NevilleS NevilleS force-pushed the add-ng-model-context branch from a9fa4d3 to c0747dd Compare November 7, 2014 21:14
@jbedard
Copy link
Contributor

jbedard commented Nov 7, 2014

I'd still hope we can do this without adding to the API, such as jbedard@3392351. But I guess that's not our decision...

@NevilleS
Copy link
Contributor Author

NevilleS commented Nov 7, 2014

@jbedard hmm, not necessarily. That approach is programmatically modifying the ngModel expression to transform it into get/set method invocations. I'd have to think a bit more to see if that would work in all cases, since it makes some assumptions about the ngModel expression (namely that expr + () will transform it into a getter invocation, for example).

@jbedard
Copy link
Contributor

jbedard commented Nov 7, 2014

Ya I'd want to think about that a bit more too. But since expr should always return a function, I'd think adding () to execute it would be fine. Except today getterSetter allows expr to not be a getter/setter which is where the breaking change is (which I think improves the API, imo).

Either way I like the idea of not changing the API. Even doing the regex thing for now - it only improves functionality and does not break any existing use cases (just has known bugs where it doesn't work). Then we can fix it or add more functionality later without having a public API to deal with.

@caitp caitp added this to the 1.3.x milestone Nov 11, 2014
@caitp
Copy link
Contributor

caitp commented Nov 11, 2014

@btford I'm just assigning you because getters/setters was yours so you might have an opinion about whether we want this or not. If you don't want it, just pass it back I guess

@btford
Copy link
Contributor

btford commented Nov 11, 2014

I'll take care of this this week.

On Tue Nov 11 2014 at 11:25:16 AM Caitlin Potter notifications@github.com
wrote:

@btford https://github.com/btford I'm just assigning you because
getters/setters was yours so you might have an opinion about whether we
want this or not. If you don't want it, just pass it back I guess


Reply to this email directly or view it on GitHub
#9865 (comment).

@NevilleS
Copy link
Contributor Author

I haven't come up with any instances where $parse(expr + "()") doesn't generate a valid getter that is called from the context of it's parent, but... something still feels weird about it to me.

@NevilleS
Copy link
Contributor Author

Hey @btford: this isn't quite ready to merge yet (I should update the docs for usage first), but let me know if the proposed API here makes sense.

@btford
Copy link
Contributor

btford commented Nov 20, 2014

Here's my take –> #10136.

btford added a commit to btford/angular.js that referenced this pull request Nov 22, 2014
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 added a commit to btford/angular.js that referenced this pull request Nov 22, 2014
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 added a commit to btford/angular.js that referenced this pull request Nov 22, 2014
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 added a commit to btford/angular.js that referenced this pull request Nov 22, 2014
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 btford closed this in bb4d3b7 Nov 22, 2014
gkalpak pushed a commit to gkalpak/angular.js that referenced this pull request Dec 5, 2014
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`.
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