Add the ability to add and remove bindings in subtrees rooted at a specified node#7439
Conversation
extensions/amp-bind/0.1/bind-impl.js
Outdated
| this.evaluator_ = new BindEvaluator(); | ||
| const parseErrors = this.evaluator_.setBindings(bindings); | ||
| this.evaluator_ = this.evaluator_ || new BindEvaluator(); | ||
| const parseErrors = this.evaluator_.setBindings(this.bindings_); |
There was a problem hiding this comment.
This will cause the evaluator to re-parse every expression, and expression parsing currently is the slowest operation in amp-bind. Instead, we should support incrementally add of expressions.
You can probably avoid the this.bindings_ variable, which saves work when removing bindings from a subtree.
extensions/amp-bind/0.1/bind-impl.js
Outdated
| } | ||
| } | ||
| if (elements.length == 0) { | ||
| delete this.expressionToElements_[expression]; |
There was a problem hiding this comment.
We should also remove obsolete expressions from the evaluator.
extensions/amp-bind/0.1/bind-impl.js
Outdated
| * @return {Promise} | ||
| */ | ||
| removeBindingsForSubtree(rootElement) { | ||
| this.scanPromise_ = this.scanSubtree_(rootElement).then(results => { |
There was a problem hiding this comment.
I don't think we need to scan the subtree here since we should already have the bindings/expression strings that we're about to remove.
extensions/amp-bind/0.1/bind-impl.js
Outdated
| const currentElement = this.boundElements_[j]; | ||
| if (currentElement | ||
| .element | ||
| .isEqualNode(boundElementToRemove.element)) { |
There was a problem hiding this comment.
We want to match the exact node, not just equivalent nodes. Otherwise, we may remove bindings from elements outside of the subtree.
There was a problem hiding this comment.
Will exactly the same node always be represented by the same object? i.e. will === work?
There was a problem hiding this comment.
extensions/amp-bind/0.1/bind-impl.js
Outdated
| this.scanPromise_ = this.scanSubtree_(rootElement).then(results => { | ||
| const {boundElements, bindings, expressionToElements} = results; | ||
|
|
||
| // TODO(kmh287): Discuss strategies for speedup |
There was a problem hiding this comment.
You can probably just walk the ancestor list of each bound element and check whether rootElement is one of them.
extensions/amp-bind/0.1/bind-impl.js
Outdated
| * @param {!Element} rootElement | ||
| * @return {Promise} | ||
| */ | ||
| addBindingsForSubtree(rootElement) { |
There was a problem hiding this comment.
Nit: s/Subtree/Node and s/rootElement/node
extensions/amp-bind/0.1/bind-impl.js
Outdated
| * @param {!Element} rootElement | ||
| * @return {Promise} | ||
| */ | ||
| addBindingsForSubtree(rootElement) { |
There was a problem hiding this comment.
Mark all private methods with @private attribute and trailing underscore. This allows Closure compiler to rename the method during minimization and other optimizations like DCE.
| * @return {!Promise} | ||
| */ | ||
| function onBindReady(callback) { | ||
| function onBindReady() { |
| }); | ||
|
|
||
| it('should scan for bindings when ampdoc is ready', () => { | ||
| it('should scan for bindings when ampdoc is ready', done => { |
There was a problem hiding this comment.
Remove unused done here and below.
There was a problem hiding this comment.
Now that onBindReady() returns a promise, it looks like we need this to signal the tests are done after the promises resolve.
There was a problem hiding this comment.
Shouldn't be necessary. Grab me if it's not working.
There was a problem hiding this comment.
Mocha understand promise return values, so there's no need for done.
| * @return {!Element} | ||
| */ | ||
| function createElementWithBinding(binding) { | ||
| const fakeDiv = env.win.document.createElement('div'); |
There was a problem hiding this comment.
Why this change? Also, is this div actually "fake"?
There was a problem hiding this comment.
The old version only ever created one element with a binding. I needed to change it to make one per call.
| }); | ||
|
|
||
| it('should scan for bindings when ampdoc is ready', () => { | ||
| it('should scan for bindings when ampdoc is ready', done => { |
There was a problem hiding this comment.
Mocha understand promise return values, so there's no need for done.
extensions/amp-bind/0.1/bind-impl.js
Outdated
| * @param {!Element} potentialParent | ||
| * @return {boolean} | ||
| */ | ||
| isAncestorOf_(node, potentialParent) { |
There was a problem hiding this comment.
Nodes have a super efficient #contains to do this:
isAncestorOf_(node, potentialParent) {
return potentialParent.contains(node);
}There was a problem hiding this comment.
This is no longer called, right? Please delete.
extensions/amp-bind/0.1/bind-impl.js
Outdated
| removeBindingsForNode_(node) { | ||
| return new Promise(resolve => { | ||
| // Eliminate bound elements that have node as an ancestor | ||
| for (let i = this.boundElements_.length - 1; i >= 0; i--) { |
| for (let i = 0; i < expressionStrings.length; i++) { | ||
| expressionsToRemove[expressionStrings[i]] = undefined; | ||
| } | ||
|
|
extensions/amp-bind/0.1/bind-impl.js
Outdated
| const deletedExpressions = []; | ||
| for (const expression in this.expressionToElements_) { | ||
| const elements = this.expressionToElements_[expression]; | ||
| for (let j = elements.length - 1; j >= 0; j--) { |
extensions/amp-bind/0.1/bind-impl.js
Outdated
| * @param {!Element} potentialParent | ||
| * @return {boolean} | ||
| */ | ||
| isAncestorOf_(node, potentialParent) { |
There was a problem hiding this comment.
This is no longer called, right? Please delete.
|
@choumx I've merged in the web worker. PTAL |
extensions/amp-bind/0.1/bind-impl.js
Outdated
| */ | ||
| removeBindingsForNode_(node) { | ||
| return new Promise(resolve => { | ||
|
|
extensions/amp-bind/0.1/bind-impl.js
Outdated
| removeBindingsForNode_(node) { | ||
| return new Promise(resolve => { | ||
|
|
||
| // Eliminate bound elements that have node as an ancestor |
There was a problem hiding this comment.
Nit: Periods at end of comments to be consistent.
| expressionsToRemove[expressionStrings[i]] = undefined; | ||
| } | ||
|
|
||
| this.parsedBindings_ = filterSplice(this.parsedBindings_, binding => { |
There was a problem hiding this comment.
Nit: "expressionString" or "string" to disambiguate from BindExpression object.
| } | ||
|
|
||
| this.parsedBindings_ = filterSplice(this.parsedBindings_, binding => { | ||
| const expression = binding.expression.expressionString; |
There was a problem hiding this comment.
hasOwnProperty not necessary for pure map objects created by Object.create(null). Set map values to true above and check expressionsToRemove[expressionString].
| }); | ||
|
|
||
| it('should allow callers to add bindings multiple times', () => { | ||
| const bindingDef = { |
There was a problem hiding this comment.
Nit: Drop "Def", which is only needed as a naming convention for @typedef. Though since you only reference each var once here, inlining would be most readable.
extensions/amp-bind/0.1/bind-impl.js
Outdated
| } | ||
| } | ||
|
|
||
| // Remove the bindings from the evaluator |
| /** | ||
| * Removes all parsed bindings for the provided expressions. | ||
| * @param {!Array<string>} expressionStrings | ||
| */ |
There was a problem hiding this comment.
Nit: removeBindingsWithExpressionStrings.
|
|
||
| import {BindEvaluator, BindingDef} from '../bind-evaluator'; | ||
|
|
||
| describe('BindEvaluator', () => { |
There was a problem hiding this comment.
Thanks for adding tests to this class!
| return onBindReady().then(() => { | ||
| expect(bind.boundElements_.length).to.equal(5); | ||
| return bind | ||
| .removeBindingsForNode_(env.win.document.getElementById('parent')); |
There was a problem hiding this comment.
Nit: Move env.win.document to a local var and this can fit on one line.
| }); | ||
| }); | ||
|
|
||
| it('should have same state after removing + re-adding a subtree', () => { |
There was a problem hiding this comment.
Recommend also verifying invocation of corresponding BindEvaluator methods.
There was a problem hiding this comment.
IMO the method invocations on the evaluator are more of an implementation detail of the Bind object.
| } | ||
|
|
||
| this.parsedBindings_ = filterSplice(this.parsedBindings_, binding => { | ||
| const expressionString = binding.expression.expressionString; |
There was a problem hiding this comment.
You want ! this. #filterSplice (and #filter) keep the items that return true and remove the items that return false.
There was a problem hiding this comment.
Thanks for catching this.
Actually, this worked as intended because it's wrong twice. Assigning to this.parsedBindings_ assigned the elements that were filtered out.
Done.
src/web-worker/web-worker.js
Outdated
| if (evaluator_) { | ||
| returnValue = | ||
| evaluator_ | ||
| .removeBindingsWithExpressionStrings.apply(evaluator_, args); |
There was a problem hiding this comment.
Move evaluator_.removeBindingsWithExpressionStrings to a local var to fix this formatting.
|
Thank you for the thorough review |
…ecified node (ampproject#7439) * first pass at adding/removing subtrees * tests passing * Add ability to add and remove bindings in the subtree rooted at a specific node * lint fixes * merge conflict * ci issues * some cleanup * pr comments * pr comments, new method of removing bindings for node and its children * fix some lint warnings * test and lint fixes * update comments * pr comments * lint errors * pr comments * cleanup * lint errors * pr comments * pr comments and lint warnings * typo * more lint warnings * pr comment * pr comment
/to @choumx