Skip to content

Commit

Permalink
Merge pull request #5537 from Polymer/scopeSubtree
Browse files Browse the repository at this point in the history
Implement scopeSubtree for ShadyDOM noPatch mode
  • Loading branch information
dfreedm authored May 16, 2019
2 parents 971d32d + 338d420 commit 6be58b0
Show file tree
Hide file tree
Showing 4 changed files with 219 additions and 5 deletions.
10 changes: 6 additions & 4 deletions lib/legacy/legacy-element-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { Debouncer } from '../utils/debounce.js';
import { timeOut, microTask } from '../utils/async.js';
import { get } from '../utils/path.js';
import { wrap } from '../utils/wrap.js';
import { scopeSubtree } from '../utils/scope-subtree.js';

let styleInterface = window.ShadyCSS;

Expand Down Expand Up @@ -701,12 +702,13 @@ export const LegacyElementMixin = dedupingMixin((base) => {
/**
* No-op for backwards compatibility. This should now be handled by
* ShadyCss library.
* @param {*} container Unused
* @param {*} shouldObserve Unused
* @return {void}
* @param {!Element} container Container element to scope
* @param {boolean=} shouldObserve if true, start a mutation observer for added nodes to the container
* @return {?MutationObserver} Returns a new MutationObserver on `container` if `shouldObserve` is true.
* @override
*/
scopeSubtree(container, shouldObserve) { // eslint-disable-line no-unused-vars
scopeSubtree(container, shouldObserve = false) {
return scopeSubtree(container, shouldObserve);
}

/**
Expand Down
78 changes: 78 additions & 0 deletions lib/utils/scope-subtree.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
@license
Copyright (c) 2019 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/

import './boot.js';

const ShadyDOM = window.ShadyDOM;
const ShadyCSS = window.ShadyCSS;

/**
* Ensure that elements in a ShadowDOM container are scoped correctly.
* This function is only needed when ShadyDOM is used and unpatched DOM APIs are used in third party code.
* This can happen in noPatch mode or when specialized APIs like ranges or tables are used to mutate DOM.
*
* @param {!Element} container Container element to scope
* @param {boolean=} shouldObserve if true, start a mutation observer for added nodes to the container
* @return {?MutationObserver} Returns a new MutationObserver on `container` if `shouldObserve` is true.
*/
export function scopeSubtree(container, shouldObserve = false) {
// If using native ShadowDOM, abort
if (!ShadyDOM || !ShadyCSS) {
return null;
}
// ShadyCSS handles DOM mutations when ShadyDOM does not handle scoping itself
if (!ShadyDOM['handlesDynamicScoping']) {
return null;
}
const ScopingShim = ShadyCSS['ScopingShim'];
// if ScopingShim is not available, abort
if (!ScopingShim) {
return null;
}
// capture correct scope for container
const containerScope = ScopingShim['scopeForNode'](container);

const scopify = (node) => {
// NOTE: native qSA does not honor scoped DOM, but it is faster, and the same behavior as Polymer v1
const elements = Array.from(ShadyDOM['nativeMethods']['querySelectorAll'].call(node, '*'));
elements.push(node);
for (let i = 0; i < elements.length; i++) {
const el = elements[i];
const currentScope = ScopingShim['currentScopeForNode'](el);
if (currentScope !== containerScope) {
if (currentScope !== '') {
ScopingShim['unscopeNode'](el, currentScope);
}
ScopingShim['scopeNode'](el, containerScope);
}
}
};

// scope everything in container
scopify(container);

if (shouldObserve) {
const mo = new MutationObserver((mxns) => {
for (let i = 0; i < mxns.length; i++) {
const mxn = mxns[i];
for (let j = 0; j < mxn.addedNodes.length; j++) {
const addedNode = mxn.addedNodes[j];
if (addedNode.nodeType === Node.ELEMENT_NODE) {
scopify(addedNode);
}
}
}
});
mo.observe(container, {childList: true, subtree: true});
return mo;
} else {
return null;
}
}
3 changes: 2 additions & 1 deletion test/runner.html
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@
'unit/html-tag.html',
'unit/legacy-data.html',
// 'unit/multi-style.html'
'unit/class-properties.html'
'unit/class-properties.html',
'unit/styling-scoped-nopatch.html'
];

function combinations(suites, flags) {
Expand Down
133 changes: 133 additions & 0 deletions test/unit/styling-scoped-nopatch.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<!doctype html>
<!--
@license
Copyright (c) 2019 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->
<html>
<head>
<meta charset="utf-8">
<script>
window.ShadyDOM = {force: true, noPatch: true};
</script>
<script src="../../node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js"></script>
<script src="wct-browser-config.js"></script>
<script src="../../node_modules/wct-browser-legacy/browser.js"></script>
</head>
<body>

<script type="module">
import {Polymer, html} from '../../polymer-legacy.js';
Polymer({
is: 'scope-subtree-legacy',
_template: html`
<style>
#container * {
border: 10px solid black;
}
</style>
<div id="container"></div>`
});
</script>

<script type="module">
import {PolymerElement, html} from '../../polymer-element.js';
class ScopeSubtreeElement extends PolymerElement {
static get template() {
return html`
<style>
#container * {
border: 10px solid black;
}
</style>
<div id="container"></div>`;
}
}
customElements.define('scope-subtree-element', ScopeSubtreeElement);
</script>

<test-fixture id="legacy">
<template>
<scope-subtree-legacy></scope-subtree-legacy>
</template>
</test-fixture>

<test-fixture id="element">
<template>
<scope-subtree-element></scope-subtree-element>
</template>
</test-fixture>

<script type="module">
import { scopeSubtree } from '../../lib/utils/scope-subtree.js';

function assertComputed(element, value, property, pseudo) {
var computed = getComputedStyle(element, pseudo);
property = property || 'border-top-width';
if (Array.isArray(value)) {
assert.oneOf(computed[property], value, 'computed style incorrect for ' + property);
} else {
assert.equal(computed[property], value, 'computed style incorrect for ' + property);
}
}

suite('LegacyElement.scopeSubtree', function() {
let el;
setup(function() {
el = fixture('legacy');
});
test('elements are scoped', function() {
const div = document.createElement('div');
const innerDiv = document.createElement('div');
div.appendChild(innerDiv);
el.$.container.appendChild(div);
el.scopeSubtree(el.$.container);
assertComputed(div, '10px');
assertComputed(innerDiv, '10px');
});
test('second argument creates a mutation observer', async function() {
const mo = el.scopeSubtree(el.$.container, true);
assert.instanceOf(mo, MutationObserver);
const div = document.createElement('div');
const innerDiv = document.createElement('div');
div.appendChild(innerDiv);
el.$.container.appendChild(div);
await new Promise((resolve) => setTimeout(resolve, 0));
assertComputed(div, '10px');
assertComputed(innerDiv, '10px');
});
});

suite('PolymerElement and scopeSubtree util', function() {
let el;
setup(function() {
el = fixture('element');
});
test('elements are scoped', function() {
const div = document.createElement('div');
const innerDiv = document.createElement('div');
div.appendChild(innerDiv);
el.$.container.appendChild(div);
scopeSubtree(el.$.container);
assertComputed(div, '10px');
assertComputed(innerDiv, '10px');
});
test('second argument creates a mutation observer', async function() {
const mo = scopeSubtree(el.$.container, true);
assert.instanceOf(mo, MutationObserver);
const div = document.createElement('div');
const innerDiv = document.createElement('div');
div.appendChild(innerDiv);
el.$.container.appendChild(div);
await new Promise((resolve) => setTimeout(resolve, 0));
assertComputed(div, '10px');
assertComputed(innerDiv, '10px');
});
});
</script>
</body>
</html>

0 comments on commit 6be58b0

Please sign in to comment.