-
Notifications
You must be signed in to change notification settings - Fork 27.4k
feat($compile): add one-way binding to the isolate scope defintion #13928
Conversation
6987dd8
to
bd1f619
Compare
} | ||
|
||
if (definition.collection) { | ||
removeWatch = scope.$watchCollection($parse(attrs[attrName]), onParentValueChange); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couldn't parentGet
be used here (to avoid double parsing) ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we even need to parse this? We are just passing the string to watch, which parses this anyway. Yeah, I think I was confused about what's going oon in the two-way binding
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should parse it to init the child value. However, you can do this
scope.$watchCollection(parentGet, ...)
to not parse twice.
bd1f619
to
ca8cc76
Compare
@@ -4219,6 +4227,299 @@ describe('$compile', function() { | |||
}); | |||
|
|||
|
|||
describe('one-way binding', function() { | |||
it('should update isolate when the identity of origin changes', inject(function() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unecessary inject()
(here and below) ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They are necessary. (many other tests have them). I think it's because the $rootScope etc. "globals" are set up in a module'S return fn.
ca8cc76
to
218b338
Compare
destination[scopeName] = newParentValue; | ||
}); | ||
} else { | ||
removeWatch = scope.$watch(attrs[attrName], function onParentCollectionValueChange(newParentValue) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are the function names for the watch callbacks swapped?
Should't it be onParentValueChange
here and onParentCollectionValueChange
above?
218b338
to
93a857b
Compare
93a857b
to
31d31e4
Compare
owRefAlias: '< owRef', | ||
owOptref: '<?', | ||
owOptrefAlias: '<? owOptref', | ||
owOptreference: '<?', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OOC, what is the purpose of owOptref
+ owOptreference
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Came from a c&p'd test, but owOptreference
doesn't look like it's needed.
Currently, if an object literal or an array literal is passed as the expression for a two-way ( |
c0b6bdc
to
35f5b5b
Compare
|
||
$rootScope.name = 'c'; | ||
$rootScope.$apply(); | ||
expect(componentScope.owRef).toEqual({name: 'c'}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I expected it to remain {name: 'b'}
. Interesting...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aha, parsed expression with inputs
and $$watchDelegates
. Nice 😃
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's why it works? I'm actually confused by this right now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. Parsing {name: name}
the resulting parsed expression has inputs
, so it has a $$watchDelegate
that will re-evaluate the expression whenever the inputs change value (in this case, inputs
is the name
property on $rootScope
).
So, whenever $rootScope.name
changes, the expression will be evaluated and will return a new object, thus the change will be propagated to the child.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this works, but when you are going deeper, you need objectEquality watch - I've added that right now.
describe('optional one-way binding', function() { | ||
it('should update local when origin changes', inject(function() { | ||
compile('<div><span my-component ow-optref="name">'); | ||
expect(componentScope.owOptRef).toBe(undefined); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
owOptRef --> owOptref
Ah, it's too late. This comes from the alias that is on ow-ref. All good. |
A couple of minor comments, but LGTM otherwise, expect for the fact that we should be more detailed on what is updated when and what is the difference between |
destination[scopeName] = parentGet(scope); | ||
|
||
if (definition.collection) { | ||
removeWatch = scope.$watchCollection(parentGet, function parentCollectionValueWatchAction(newParentValue) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do not understand what is the use case to use $watchCollection
. We are not copying things from one side to the other, so I would just drop support for '<*'
, only support '<'
(and its named and '?'
versions) have the minimal viable feature and cut this release.
If, later on, people ask for this feature, then we can take the time to think if this is the way that best fits the needs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with @lgalfaso.
The only difference with <*
is when the parent will be "synced" to the child and this only matters, if the child has set its property to a new instance. Since I can't think of any good usecase for setting the child to a new instance (knowing that at any point the parent could overwrite that value), I believe that <*
will only cause confusion.
Keeping this feature as simple as possible (which means simpler semantics, less corner-cases, less unexpected behaviors) is pretty important imo.
As @lgalfaso said, we can always add stuff/features, but removing isn't that simple. Let's start simple.
(BTW, as @lgalfaso pointed out, there is no *
in ng2, so I think it helps to be consistent.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lgalfaso Well spotted!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just found a use case for this today while using a 2-way binding (and wishing it was a 1-way binding)... when doing things like <element binding="array | orderBy:'foo'">
using a =
binding causes an infinite digest while using =*
works. But =*
or <*
really doesn't seem like a great solution for that, it would be great if there was some other way. Maybe things like filter
and orderby
should somehow return the same instance each time if it hasn't changed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jbedard when you have <element binding="array | orderBy:'foo'">
we are not watching the result of the array after it went thru the filter, but before. Anyhow, if the behavior of filter
and orderBy
should change, then that should be another issue's discussion
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we are not watching the result of the array after it went thru the filter, but before
I don't think that's right. Just doing $rootScope.$watch("a | orderBy"); $rootScope.$apply("a = []")
causes an infinite digest. I believe =*
was created specifically to avoid this. However I think =*
is a bad solution, it would be better if $parse/$watch handled this somehow...
I've
|
* parent scope. If no `attr` name is specified then the attribute name is assumed to be the same as the | ||
* local name. You can also make the binding optional by adding `?`: `<?` or `<?attr`. | ||
* | ||
* For example, given `<dir my-attr="parentModel">` and directive definition of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dir --> my-component (to be consistent with #13921)
LGTM (expect for some minor docs typos/fixes). |
This change allows the developer to bind an isolate scope / controller property to an expression, using a `<` binding, in such a way that if the value of the expression changes, the scope/controller property is updated but not the converse. The binding is implemented as a single simple watch, which can also provide performance benefits over two way bindings. Closes #13928 Closes #13854 Closes #12835 Closes #13900
No description provided.