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

ngModelOption's getterSetter does not work with Factory objects #9394

Closed
lightswitch05 opened this issue Oct 2, 2014 · 15 comments
Closed

ngModelOption's getterSetter does not work with Factory objects #9394

lightswitch05 opened this issue Oct 2, 2014 · 15 comments

Comments

@lightswitch05
Copy link

Overview of the Issue

When using ng-model-options="{ getterSetter: true }", the getter & setter methods do not run within the correct scope if defined in a factory. This causes this to be undefined within the getter & setter method - preventing anything from being set or retrieved. (TypeError: Cannot read property 'x' of undefined)

Motivation for or Use Case

While some getter & setter methods should be defined within the controller & scope - more complex objects should contain their own getter and setter methods defined within their factories for a better separation of concerns.

Angular Version

I've tried this with 1.3.0-rc.4 & 1.3.0-rc.3

Browsers and Operating System

This appears to be Angular.js specific and not a browser problem. However, I am using chrome 37.0.2062.124 m on windows 7 64bit.

Reproduce the Error

Here is a plunker that has getter & setter methods defined within a object from a factory that does not work: http://plnkr.co/edit/RmoegMYEgF9s7s50KQNj?p=preview

Here is another version that has the object defined within the controller (no factory) that works fine: http://plnkr.co/edit/4f8i384nsRwCoaQtPoxx?p=preview

@lightswitch05 lightswitch05 changed the title ngModelOption's getterSetter do not work with Factory objects ngModelOption's getterSetter does not work with Factory objects Oct 2, 2014
@caitp
Copy link
Contributor

caitp commented Oct 2, 2014

This is kind of a dupe of #8552 (sort of).

So, the parser isn't the one invoking the function, and we have no way to get the context of the function :(

It would be good to make this work nicely everywhere though

@jtymes
Copy link
Contributor

jtymes commented Oct 2, 2014

I was working with @lightswitch05 on this, and if it's not currently possible to get the context, maybe the docs should be updated to reflect that.

I volunteer @lightswitch05 to update the docs 😄

@lightswitch05
Copy link
Author

@caitp I propose this as a solution:

getterSetter currently accepts a boolean if getter & setters are to be used. Thats fine - but how about allowing a context to be sent instead of true

thoughts?

@caitp
Copy link
Contributor

caitp commented Oct 2, 2014

I don't think that's particularly useful --- I would rather just change the parser so that it can return the context so that we can get it when we need it.

@caitp
Copy link
Contributor

caitp commented Oct 2, 2014

so I don't know how big of a problem this is, but I guess it couldn't hurt to fix it --- I just don't think it's a huge priority.

Maybe I'll write a patch for this on the weekend and see what people think

@lightswitch05
Copy link
Author

I'm currently working around this by storing a reference to this and using non-prototypical getter & setter methods: http://plnkr.co/edit/nk35FTVECZiC8Uzj8aWY?p=preview

@NevilleS
Copy link
Contributor

NevilleS commented Oct 3, 2014

Something like this could work, although I'm not sure how many other things it would break...

In getterFn(path, options, fullExp) (angular.js:11601):

    var code = '';
    forEach(pathKeys, function(key, index) {
      ensureSafeMemberName(key, fullExp);
      code += 'var p = s;\n';
      code += 'if(s == null) return undefined;\n';
      if (index == 0) {
        // but if we are first then we check locals first, and if so read it first
        code += 's=((l&&l.hasOwnProperty("' + key + '"))?l:s)' + '.' + key + ';\n';
      } else {
        // we simply dereference 's' on any .dot notation
        code += 'p = s;\n'
        code += 's=s.' + key + ';\n'
      }
    });
    code += "if (typeof s === 'function' && p != null) return s.bind(p);\n"
    code += 'return s;';

@caitp
Copy link
Contributor

caitp commented Oct 3, 2014

would be a performance killer, not the right way to do this

@caitp
Copy link
Contributor

caitp commented Oct 3, 2014

I mean, it is technically the best way to solve the problem, but also would kill performance :(

@NevilleS
Copy link
Contributor

NevilleS commented Oct 3, 2014

Yeah 😄

Maybe you could reparse the ngModel expression at call time with a special argument to the lexer...

@caitp
Copy link
Contributor

caitp commented Oct 3, 2014

in my view, it would be easy to provide a separate "getter" function (lets call it context()) which would always return the containing object, or $scope. It would basically just shave off the last chained identifier in an expression.

Obviously this sucks because getting the context (and calling a function using that context) would be the responsibility of the developer, but I don't really see a better way of doing this that doesn't suck the life out of applications.

Alternatively we could just call this behaviour unsupported and never handle it, but I don't think that would make people happy :( WDYT @btford

@NevilleS
Copy link
Contributor

NevilleS commented Oct 3, 2014

You could do something hacky in NgModelController like:

  var parsedNgModel = $parse($attr.ngModel),
      pendingDebounce = null,
      ctrl = this,
      parsedNgModelParent = null;
  if (ctrl.$options.getterSetter) {
    parsedNgModelParent = parseParent($attr.ngModel);
  }

  var ngModelGet = function ngModelGet() {
    var modelValue = parsedNgModel($scope);
    if (ctrl.$options && ctrl.$options.getterSetter && isFunction(modelValue)) {
      modelValue = modelValue.call(parsedNgModelParent);
    }
    return modelValue;
  };

That way you can still have the nice benefit of the lexer providing the context (not the developer), but you limit the performance impact to a second, optional call to $parse.

Still pretty gross 😒

@btford
Copy link
Contributor

btford commented Oct 3, 2014

I think this reasonably should be supported.

@Katulus
Copy link

Katulus commented Oct 24, 2014

I have a scenario when this really is a blocker - when you use TypeScript. I use TypeScript to implement all of my application logic including controllers, models etc. TypeScript generates methods using prototype. None of getters/setters then work with AngularJS due to this issue.

When you mention that it would be a performance killer, have you actually tried how much?

I made a small test: http://plnkr.co/ABCvAb6FrYO2PNB7J7ic

Approach with bind() is slower as we expect. In IE11 it's about two times slower than current approach. In Chrome it's about 10 times slower. The test just runs each version one million times in a loop so it's kind of artificial but shows the difference. For that 1 million calls it makes about 1 second in Chrome.

Now the question is, if you run full AngularJS processing for single getter, what is the relative slowdown? If whole processing executed 1000 times today takes let's say 1 second and with this change would take 1.1 second, I would go for it. Today you generate code for the getter as a string and let runtime parse it and execute it. That sounds like a much bigger performance hit than calling bind() (though I haven't tested that).

Current behavior that prevents you to use object methods correctly makes getter/setter option kind of useless.

@NevilleS
Copy link
Contributor

Hmm. Something like this would be more explicit, and let you specify the caller for your getter function if you wanted...

  var parsedNgModel = $parse($attr.ngModel),
      parsedNgModelContext = $parse($attr.ngModelContext),
      pendingDebounce = null,
      ctrl = this;

  var ngModelGet = function ngModelGet() {
    var modelValue = parsedNgModel($scope);
    if (ctrl.$options && ctrl.$options.getterSetter && isFunction(modelValue)) {
      var context = parsedNgModelContext($scope) || this;
      modelValue = modelValue.call(context);
    }
    return modelValue;
  };

  var ngModelSet = function ngModelSet(newValue) {
    var getterSetter;
    if (ctrl.$options && ctrl.$options.getterSetter &&
        isFunction(getterSetter = parsedNgModel($scope))) {
      var context = parsedNgModelContext($scope) || this;
      getterSetter.call(context, ctrl.$modelValue);
    } else {
      parsedNgModel.assign($scope, ctrl.$modelValue);
    }
  };

That would make something like this work as you were hoping, where this is bound to your factory instance when the getter/setter method is called:

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

I think that's what @caitp was originally proposing, and this doesn't require touching $parse at all.

NevilleS added a commit to NevilleS/angular.js that referenced this issue Oct 31, 2014
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 angular#9394
NevilleS added a commit to NevilleS/angular.js that referenced this issue Nov 2, 2014
When using a getter/setter function for ngModel, attempt to determine the appropriate context by
parsing the ngModel expression. If the context cannot be found, fallback to using the current scope.
For example, '<input ng-model="someObject.value" ng-model-options="{ getterSetter: true }">' will
use 'someObject' as the calling context.

Non-assignable ngModel expressions will always fallback to using the current scope as the context.
For example, '<input ng-model="someObject.getValueFn()" ng-model-options="{ getterSetter: true }">'
will invoke the result of 'someObject.getValueFn()' from the current scope.

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 added a commit to NevilleS/angular.js that referenced this issue Nov 2, 2014
When using a getter/setter function for ngModel, attempt to determine the appropriate context by
parsing the ngModel expression. If the context cannot be found, fallback to using the current scope.
For example, '<input ng-model="someObject.value" ng-model-options="{ getterSetter: true }">' will
use 'someObject' as the calling context.

Non-assignable ngModel expressions will always fallback to using the current scope as the context.
For example, '<input ng-model="someObject.getValueFn()" ng-model-options="{ getterSetter: true }">'
will invoke the result of 'someObject.getValueFn()' from the current scope.

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 added a commit to NevilleS/angular.js that referenced this issue Nov 7, 2014
…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 added a commit to NevilleS/angular.js that referenced this issue Nov 7, 2014
…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!
btford added a commit to btford/angular.js that referenced this issue Nov 20, 2014
Closes angular#9394

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 issue Nov 20, 2014
Closes angular#9394

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 issue Nov 20, 2014
Closes angular#9394

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`.
jbedard added a commit to jbedard/angular.js that referenced this issue Nov 20, 2014
Closes angular#9394

BREAKING CHANGE:
 - the ngModelController getterSetter property is read once at link time and can not change afterwards
 - previously ngModel invoked the getterSetter in the global context
 - the ngModel getterSetter expression must work when `()` (a method invokation) is appended to it
btford added a commit to btford/angular.js that referenced this issue Nov 22, 2014
Closes angular#9394

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 issue Nov 22, 2014
Closes angular#9394

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 issue 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 issue 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 issue 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 issue 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 as completed in bb4d3b7 Nov 22, 2014
gkalpak pushed a commit to gkalpak/angular.js that referenced this issue 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.