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

feat($compile): add one-way binding to the isolate scope defintion #13928

Closed
wants to merge 6 commits into from

Conversation

Narretz
Copy link
Contributor

@Narretz Narretz commented Feb 2, 2016

No description provided.

}

if (definition.collection) {
removeWatch = scope.$watchCollection($parse(attrs[attrName]), onParentValueChange);
Copy link
Member

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) ?

Copy link
Contributor Author

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

Copy link
Contributor

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.

@Narretz Narretz force-pushed the feat-compile-oneway branch from bd1f619 to ca8cc76 Compare February 2, 2016 13:34
@@ -4219,6 +4227,299 @@ describe('$compile', function() {
});


describe('one-way binding', function() {
it('should update isolate when the identity of origin changes', inject(function() {
Copy link
Member

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) ?

Copy link
Contributor Author

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.

@Narretz Narretz force-pushed the feat-compile-oneway branch from ca8cc76 to 218b338 Compare February 2, 2016 14:14
destination[scopeName] = newParentValue;
});
} else {
removeWatch = scope.$watch(attrs[attrName], function onParentCollectionValueChange(newParentValue) {
Copy link
Contributor

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?

@Narretz Narretz force-pushed the feat-compile-oneway branch from 218b338 to 93a857b Compare February 2, 2016 15:45
@Narretz Narretz force-pushed the feat-compile-oneway branch from 93a857b to 31d31e4 Compare February 2, 2016 15:51
owRefAlias: '< owRef',
owOptref: '<?',
owOptrefAlias: '<? owOptref',
owOptreference: '<?',
Copy link
Member

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 ?

Copy link
Contributor Author

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.

@thorn0
Copy link
Contributor

thorn0 commented Feb 2, 2016

Currently, if an object literal or an array literal is passed as the expression for a two-way (=) binding, the set up watch is deep (uses angular.equals). The implementation in this PR doesn't do this with the new one-way bindings. IMO it'd more consistent if one-way bindings behaved exactly like two-way bindings, just without probability for the parent to get modified by them (with the known caveats).

@Narretz Narretz force-pushed the feat-compile-oneway branch from c0b6bdc to 35f5b5b Compare February 2, 2016 19:22

$rootScope.name = 'c';
$rootScope.$apply();
expect(componentScope.owRef).toEqual({name: 'c'});
Copy link
Member

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...

Copy link
Member

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 😃

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

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);
Copy link
Member

Choose a reason for hiding this comment

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

owOptRef --> owOptref

@Narretz
Copy link
Contributor Author

Narretz commented Feb 2, 2016

Hm, something strange is going on. The watchAction functions fire twice every time there's an apply. I don't think this should happen. Maybe that's also why the literals work even though they shouldn't.

Ah, it's too late. This comes from the alias that is on ow-ref. All good.

@gkalpak
Copy link
Member

gkalpak commented Feb 2, 2016

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 * and no *.

destination[scopeName] = parentGet(scope);

if (definition.collection) {
removeWatch = scope.$watchCollection(parentGet, function parentCollectionValueWatchAction(newParentValue) {
Copy link
Contributor

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.

Copy link
Member

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.)

Copy link
Contributor

Choose a reason for hiding this comment

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

@lgalfaso Well spotted!

Copy link
Collaborator

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?

Copy link
Contributor

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

Copy link
Collaborator

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...

@Narretz
Copy link
Contributor Author

Narretz commented Feb 3, 2016

I've

  • removed the collectionWatch functionality
  • updated the description to incorporate changes made by @thorn0
  • added watching literals by object equality

* 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
Copy link
Member

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)

@gkalpak
Copy link
Member

gkalpak commented Feb 3, 2016

LGTM (expect for some minor docs typos/fixes).
After this gets merged, it might be a goood idea to add an example in the Directives guide.

@petebacondarwin petebacondarwin self-assigned this Feb 3, 2016
petebacondarwin pushed a commit that referenced this pull request Feb 3, 2016
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
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.

9 participants