Surprising behavior for the first argument to $watch

2,145 views
Skip to first unread message

Luke Bayes

unread,
May 10, 2012, 2:08:52 PM5/10/12
to ang...@googlegroups.com
I know why it's happening, and the docs mostly explain it (at least for function and string), but I'm admittedly still surprised.

We've been trying to develop some patterns for working with Angular, and one of them has evolved into trying to create custom abstractions (directives) for each logical area of our application.

Unfortunately, as we work with directives, we're struggling with sending values across the directive boundary and into its controller (or linker) instance.

I've seen (and posted) some examples where $attrs.propertyName is sent directly to a $watch expression and thought that was kind of a recommended thing to do.

In my imagination, the first argument to $watch would branch on whether the input was a function or a non-function value, but it turns out, the branch is a bit more complicated.

a) Function: Call the function and compare the results, calling the second function when the results are different.
b) Numeric value: if (!isNan(parseInt(value)), then call the second function when the numeric values are different, with numericized values.
c) Object: If the type is a non-string Object, then compare the object with whatever rules exist today (=== , or $$id, or some such thing).
d) String: Evaluate the string against $scope, calling the second function when $scope[stringValue] are different.

There may be other type inferences here, we were just surprised by the difference between two apparently primitive values (numeric and string).

The documentation does explain the difference between function and string inputs to $watch, I guess we began by sending values that weren't either and it behaved pretty well until the value happened to be a string.


Just sharing here because we were pretty surprised by the results, and figure it could save someone else a little time.

I know there are a variety of alternative patterns that can be sent to the 'scope' parameter when declaring a directive. I suspect it's one of those that we should use instead of creating $watchers?


lb.


Suller Andras

unread,
May 12, 2012, 12:21:04 AM5/12/12
to ang...@googlegroups.com
Hi lb,

From the docs[1]:
"watchExpression – {(function()|string)} – Expression that is
evaluated on each $digest cycle. A change in the return value triggers
a call to the listener.
string: Evaluated as expression
function(scope): called with current scope as a parameter."

I believe there is no point to call $watch with a numeric parameter,
since it will never change. If you write "scope.$watch(attrs.count,
...)", $watch will get the actual value of attrs.count, but it doesn't
know where is it coming from, so it will not be able to re-evaluate
later.
You didn't change any attribute value in your fiddle, but I would be
surprised if the $watch would notice the change.

However, what you wrote is practically copies the attribute values to
the directive's scope. You can do this with:

scope: {direction: 'attribute', count: 'attribute'},

Hope this helps.

Andras

[1]: http://docs.angularjs.org/api/angular.module.ng.$rootScope.Scope

Vojta Jína

unread,
May 21, 2012, 2:16:22 AM5/21/12
to ang...@googlegroups.com
Hey Luke,

it's simple. The first argument to $watch function can be only
FUNCTION OR STRING.
If it's a string, it is parsed (see $parse) into a function. (To be
precise, even numbers/objects are parsed, but to noop function).

Then, angular calls this function and if result of the current call
does not equal previous one, the second argument (function) is called.
So, strings are evaluated as ANGULAR EXPRESSION.

Angular expressions:
'123' -> 123
'true' -> true
'false' -> false
'"some"' -> "some"

'prop' -> will be looked up on current scope


Now, back to watching attributes.
<div something="property"> -> $attr.something === 'property'
scope.$watch($attr.something, function(){});
You are passing a string "property", treated as angular expression ->
will be looked up at scope.property.

<div something="123"> -> $attr.something === '123'
scope.$watch($attr.something, function(){});
You are passing a string "123", treated as angular expression -> will
be number 123 (so it will never change, the watcher will get called
only once)

Anyway, watching attributes only makes sense, if you interpolate them,
eg. <div something="prop-{{id}}">, then, you want to use $observe:
$attr.$observe('something', function() {});

Looking into your fiddle - none of these watchers makes sense to me.
If you want to get "vertical" value, just read $attr.direction, why
would you watch it ? It can't change, unless you interpolate it (use
double curlies). If you want to get proper types (say boolean/number)
instead of strings, just evaluate it once scope.$eval($attr.count),
but again, no reason for watch, as it will never change the value.


Do you have any idea, how to make it simpler ?

V.
> --
> You received this message because you are subscribed to the Google Groups
> "AngularJS" group.
> To view this discussion on the web visit
> https://groups.google.com/d/msg/angular/-/B6J0aohnXq4J.
> To post to this group, send email to ang...@googlegroups.com.
> To unsubscribe from this group, send email to
> angular+u...@googlegroups.com.
> For more options, visit this group at
> http://groups.google.com/group/angular?hl=en.
Reply all
Reply to author
Forward
0 new messages