Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add scopedQuerySelector helper #7260

Merged
merged 10 commits into from
Feb 2, 2017

Conversation

jridgewell
Copy link
Contributor

@jridgewell jridgewell commented Jan 30, 2017

This fixes (and forbids future) element scoped #querySelector calls. Anytime you select a descendant of a descendant (div div, or, really, * *), your matching against the entire document then filtering to elements inside the scope. This is not what you'd expect.

var parent = document.createElement('div');

var element = document.createElement('span');
parent.appendChild(element);

var a = document.createElement('a');
element.appendChild(a);

// WHAT!?
element.querySelector('div a'); // => a

Instead, we'll use the :scope CSS selector where supported (everywhere but IE), and will quickly mutate and un-mutate the element in IE to accomplish a global, unique scope selector.

scopedQuerySelector(element, 'div a'); // => null

Fixes a selector bug in <amp-analytics>. /cc @avimehta
/cc @tlong2

src/dom.js Outdated
});

// Only IE.
const unique = `i-amphtml-scoped-${Math.random()}`;
Copy link
Contributor

Choose a reason for hiding this comment

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

That includes the '.' (dot). Is it prudent to have it there given selectors 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.

Good catch, changed to use Date.now

* @param {string} selector
* @return {?Element}
*/
export function scopedQuerySelector(root, selector) {
Copy link
Contributor

Choose a reason for hiding this comment

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

We initially decided to hold this off because of https://bugs.chromium.org/p/chromium/issues/detail?id=222028

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This isn't due to :scope, but that there are now two selectors to match against (.selector1 vs .selector1 .selector2).

I'm not requiring scopedQuerySelector where we can avoid it (it's fine if you're doing scope.querySelector('selector')). The only time it's necessary is when there's a descendant selector included (scope.querySelector('parent descendant')), which is already slow.

@@ -136,7 +136,7 @@ export class AmpAdCustom extends AMP.BaseElement {

// Get the parent body of this amp-ad element. It could be the body of
// the main document, or it could be an enclosing iframe.
const body = ancestorElementsByTag(this.element, 'BODY')[0];
const body = closestByTag(this.element, 'BODY');
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm. This doesn't look right. Though I guess not the subject to your PR. Would you mind leaving a TODO here for me to investigate?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll split out of the PR.

@@ -177,7 +181,7 @@ export function getElement(ampdoc, selector, analyticsEl, selectionMethod) {
// Only tag names are supported currently.
foundEl = closestByTag(analyticsEl, selector);
} else if (selectionMethod == 'scope') {
foundEl = analyticsEl.parentElement.querySelector(selector);
foundEl = scopedQuerySelector(analyticsEl.parentElement, selector);
Copy link
Contributor

Choose a reason for hiding this comment

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

There's the same thing in the analytics-root. This code is actually waiting to be killed in preference of that. Can you change there as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

@@ -1362,8 +1362,9 @@ function createBaseCustomElementClass(win) {
* @package @final @this {!Element}
*/
getRealChildren() {
return dom.childElements(this, element =>
!isInternalOrServiceNode(element));
return dom.childElementByTag(this, '*').filter(element => {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is relatively unclear that ByTag can do wildcards... E.g. does ELement.getElementsByTagName do wildcards?

Another question: is querying here really faster than first-child -> siblings walk?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was after code size here, not speed. I'll split out the childElements changes.

Copy link
Contributor Author

@jridgewell jridgewell left a comment

Choose a reason for hiding this comment

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

PTAL.

@@ -177,7 +181,7 @@ export function getElement(ampdoc, selector, analyticsEl, selectionMethod) {
// Only tag names are supported currently.
foundEl = closestByTag(analyticsEl, selector);
} else if (selectionMethod == 'scope') {
foundEl = analyticsEl.parentElement.querySelector(selector);
foundEl = scopedQuerySelector(analyticsEl.parentElement, selector);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

src/dom.js Outdated
});

// Only IE.
const unique = `i-amphtml-scoped-${Date.now()}`;
Copy link
Member

Choose a reason for hiding this comment

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

better to increment an integer. This can very much be invoked twice in the same milli

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's removed immediately, so it'll still be unique. Really, all the class name needs is "i-amphtml-scoped".

src/dom.js Outdated
scopeSelectorSupported = isScopeSelectorSupported(root);
}
if (scopeSelectorSupported) {
return toArray(root./*OK*/querySelectorAll(`:scope ${selector}`));
Copy link
Member

Choose a reason for hiding this comment

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

Do we really need to toArray?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If they call #filter, etc.

Copy link
Member

Choose a reason for hiding this comment

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

Are there existing consumers that do that?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Only one. Updated it to a for loop.

Anytime you select a descendant of a descendant (`div div`, or, really,
`* *`), your matching against the entire document then filtering to
elements inside the scope. This is **not** what you'd expect.

Fixes a selector bug in `<amp-analytics>`.
@jridgewell
Copy link
Contributor Author

Ping @dvoytenko

@jridgewell jridgewell merged commit 57810d9 into ampproject:master Feb 2, 2017
torch2424 pushed a commit to torch2424/amphtml that referenced this pull request Feb 14, 2017
* Eliminate uses of childElement and childElements

* Forbid uses of buggy querySelector

Anytime you select a descendant of a descendant (`div div`, or, really,
`* *`), your matching against the entire document then filtering to
elements inside the scope. This is **not** what you'd expect.

Fixes a selector bug in `<amp-analytics>`.

* Add tests

* Use `Date.now` to avoid `.` class selector conflict

* Merge latest master

* Linting

* Fix types

* Fix merge conflict

* Return NodeList

* Fix tests
mrjoro pushed a commit to mrjoro/amphtml that referenced this pull request Apr 28, 2017
* Eliminate uses of childElement and childElements

* Forbid uses of buggy querySelector

Anytime you select a descendant of a descendant (`div div`, or, really,
`* *`), your matching against the entire document then filtering to
elements inside the scope. This is **not** what you'd expect.

Fixes a selector bug in `<amp-analytics>`.

* Add tests

* Use `Date.now` to avoid `.` class selector conflict

* Merge latest master

* Linting

* Fix types

* Fix merge conflict

* Return NodeList

* Fix tests
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