Skip to content

Add the ability to add and remove bindings in subtrees rooted at a specified node#7439

Merged
dreamofabear merged 27 commits intoampproject:masterfrom
kmh287:#7376_mustache_binding
Feb 16, 2017
Merged

Add the ability to add and remove bindings in subtrees rooted at a specified node#7439
dreamofabear merged 27 commits intoampproject:masterfrom
kmh287:#7376_mustache_binding

Conversation

@kmh287
Copy link
Copy Markdown
Contributor

@kmh287 kmh287 commented Feb 8, 2017

  • Adds the ability to add and remove bindings in subtrees rooted at a specific node
  • Adds tests for the new behavior

/to @choumx

@dreamofabear dreamofabear self-requested a review February 9, 2017 17:52
this.evaluator_ = new BindEvaluator();
const parseErrors = this.evaluator_.setBindings(bindings);
this.evaluator_ = this.evaluator_ || new BindEvaluator();
const parseErrors = this.evaluator_.setBindings(this.bindings_);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

}
}
if (elements.length == 0) {
delete this.expressionToElements_[expression];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

We should also remove obsolete expressions from the evaluator.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

* @return {Promise}
*/
removeBindingsForSubtree(rootElement) {
this.scanPromise_ = this.scanSubtree_(rootElement).then(results => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

const currentElement = this.boundElements_[j];
if (currentElement
.element
.isEqualNode(boundElementToRemove.element)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

We want to match the exact node, not just equivalent nodes. Otherwise, we may remove bindings from elements outside of the subtree.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Will exactly the same node always be represented by the same object? i.e. will === work?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

this.scanPromise_ = this.scanSubtree_(rootElement).then(results => {
const {boundElements, bindings, expressionToElements} = results;

// TODO(kmh287): Discuss strategies for speedup
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

You can probably just walk the ancestor list of each bound element and check whether rootElement is one of them.

* @param {!Element} rootElement
* @return {Promise}
*/
addBindingsForSubtree(rootElement) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nit: s/Subtree/Node and s/rootElement/node

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

* @param {!Element} rootElement
* @return {Promise}
*/
addBindingsForSubtree(rootElement) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

* @return {!Promise}
*/
function onBindReady(callback) {
function onBindReady() {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

👍

});

it('should scan for bindings when ampdoc is ready', () => {
it('should scan for bindings when ampdoc is ready', done => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remove unused done here and below.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Now that onBindReady() returns a promise, it looks like we need this to signal the tests are done after the promises resolve.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Shouldn't be necessary. Grab me if it's not working.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Mocha understand promise return values, so there's no need for done.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

* @return {!Element}
*/
function createElementWithBinding(binding) {
const fakeDiv = env.win.document.createElement('div');
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why this change? Also, is this div actually "fake"?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The old version only ever created one element with a binding. I needed to change it to make one per call.

@kmh287 kmh287 mentioned this pull request Feb 13, 2017
});

it('should scan for bindings when ampdoc is ready', () => {
it('should scan for bindings when ampdoc is ready', done => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Mocha understand promise return values, so there's no need for done.

* @param {!Element} potentialParent
* @return {boolean}
*/
isAncestorOf_(node, potentialParent) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nodes have a super efficient #contains to do this:

isAncestorOf_(node, potentialParent) {
  return potentialParent.contains(node);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Awesome. Done.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is no longer called, right? Please delete.

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--) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We have a filterSplice helper for this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

for (let i = 0; i < expressionStrings.length; i++) {
expressionsToRemove[expressionStrings[i]] = undefined;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ditto filterSplice

const deletedExpressions = [];
for (const expression in this.expressionToElements_) {
const elements = this.expressionToElements_[expression];
for (let j = elements.length - 1; j >= 0; j--) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ditto.

* @param {!Element} potentialParent
* @return {boolean}
*/
isAncestorOf_(node, potentialParent) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is no longer called, right? Please delete.

@kmh287
Copy link
Copy Markdown
Contributor Author

kmh287 commented Feb 14, 2017

@choumx I've merged in the web worker. PTAL

Copy link
Copy Markdown

@dreamofabear dreamofabear left a comment

Choose a reason for hiding this comment

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

Looks good, mostly nits.

*/
removeBindingsForNode_(node) {
return new Promise(resolve => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nit: Extra line.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

removeBindingsForNode_(node) {
return new Promise(resolve => {

// Eliminate bound elements that have node as an ancestor
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nit: Periods at end of comments to be consistent.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

expressionsToRemove[expressionStrings[i]] = undefined;
}

this.parsedBindings_ = filterSplice(this.parsedBindings_, binding => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nit: "expressionString" or "string" to disambiguate from BindExpression object.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

}

this.parsedBindings_ = filterSplice(this.parsedBindings_, binding => {
const expression = binding.expression.expressionString;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

hasOwnProperty not necessary for pure map objects created by Object.create(null). Set map values to true above and check expressionsToRemove[expressionString].

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

});

it('should allow callers to add bindings multiple times', () => {
const bindingDef = {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

https://engdoc.corp.google.com/eng/doc/devguide/coding/testing/unit-testing-best-practices.shtml?cl=head#constants

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

}
}

// Remove the bindings from the evaluator
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ditto.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

/**
* Removes all parsed bindings for the provided expressions.
* @param {!Array<string>} expressionStrings
*/
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nit: removeBindingsWithExpressionStrings.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.


import {BindEvaluator, BindingDef} from '../bind-evaluator';

describe('BindEvaluator', () => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Choose a reason for hiding this comment

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

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', () => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Recommend also verifying invocation of corresponding BindEvaluator methods.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You want ! this. #filterSplice (and #filter) keep the items that return true and remove the items that return false.

Copy link
Copy Markdown
Contributor Author

@kmh287 kmh287 Feb 14, 2017

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Lol.

if (evaluator_) {
returnValue =
evaluator_
.removeBindingsWithExpressionStrings.apply(evaluator_, args);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Move evaluator_.removeBindingsWithExpressionStrings to a local var to fix this formatting.

@kmh287
Copy link
Copy Markdown
Contributor Author

kmh287 commented Feb 16, 2017

Thank you for the thorough review

@dreamofabear dreamofabear merged commit d822f3a into ampproject:master Feb 16, 2017
mrjoro pushed a commit to mrjoro/amphtml that referenced this pull request Apr 28, 2017
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants