Skip to content
This repository has been archived by the owner on Sep 20, 2019. It is now read-only.

Adds attachShadow({shadyUpgradeFragment: documentFragment}) #316

Merged
merged 25 commits into from
Apr 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6261855
Adds `ShadowRoot.upgrade(fragment, host, options)`
Jan 23, 2019
71f9768
[upgrade] Fix rendering with slots
Jan 24, 2019
96be5a4
Avoid childNodes when possible.
Jan 24, 2019
3091902
More avoiding of `childNodes` when possible.
Jan 24, 2019
3da0f04
Remove superfluous check
Jan 25, 2019
eb98d72
[upgrade] Refine argument name
Jan 25, 2019
a0ce84a
Merge branch 'more-flush' into shadowRoot-upgrade
Feb 1, 2019
f542a57
Merge branch 'master' into shadowRoot-upgrade
Feb 6, 2019
a67d835
Merge branch 'master' into shadowRoot-upgrade
Feb 14, 2019
d359957
Adds `ShadyDOM.upgrade(fragment, host, options)`
Feb 14, 2019
4342887
Re-add skipped tests
Feb 14, 2019
003a148
upgarde: avoid optimal path when customElements polyfill is in use
Feb 14, 2019
2b355c2
Avoid `upgrade` on IE
Feb 15, 2019
59cff79
Address review feedback
Feb 20, 2019
87c8e35
Slight simplification based on review
Feb 20, 2019
f70e34e
Allow `ShadyDOM.attachDOM` to work in the customElements polyfill
Feb 21, 2019
ba1a238
Merge branch 'master' into shadowRoot-upgrade
Apr 1, 2019
4eb1c12
Merge branch 'wrap-className' into shadowRoot-upgrade
Apr 1, 2019
3fb6f83
Merge branch 'master' into shadowRoot-upgrade
Apr 3, 2019
9fa8f8a
Ensure scoping updates correctly when ShadyDOM.attachDom is used.
Apr 4, 2019
5d9f909
Fix event removal for platforms (e.g. old Chrome) that don't have eve…
Apr 11, 2019
9dd608d
Simplify native patching slightly and add className
Apr 11, 2019
eb6e08a
Remove `ShadyDOM.attachDom` in favor of `attachShadow({shadyUpgradeFr…
Apr 13, 2019
d585fd4
Fix typo
Apr 13, 2019
5a77147
Lint fix.
Apr 13, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 40 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"@webcomponents/webcomponents-platform": "^1.0.0",
"eslint": "^4.19.1",
"eslint-plugin-html": "^4.0.5",
"google-closure-compiler": "^20180506.0.0",
"google-closure-compiler": "^20190121.0.0",
"gulp": "^4.0.0",
"gulp-rename": "^1.4.0",
"gulp-rollup": "^2.16.2",
Expand Down
99 changes: 60 additions & 39 deletions src/attach-shadow.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,31 @@ class ShadyRoot {
if (token !== ShadyRootConstructionToken) {
throw new TypeError('Illegal constructor');
}
/** @type {boolean} */
this._renderPending;
/** @type {boolean} */
this._hasRendered;
/** @type {?Array<HTMLSlotElement>} */
this._slotList = null;
/** @type {?Object<string, Array<HTMLSlotElement>>} */
this._slotMap;
/** @type {?Array<HTMLSlotElement>} */
this._pendingSlots;
this._init(host, options);
}

_init(host, options) {
// NOTE: set a fake local name so this element can be
// distinguished from a DocumentFragment when patching.
// FF doesn't allow this to be `localName`
/** @type {string} */
this._localName = SHADYROOT_NAME;
// root <=> host
this.host = host;
/** @type {!string|undefined} */
this.mode = options && options.mode;
recordChildNodes(host);
const hostData = ensureShadyDataForNode(host);
recordChildNodes(this.host);
const hostData = ensureShadyDataForNode(this.host);
/** @type {!ShadyRoot} */
hostData.root = this;
hostData.publicRoot = this.mode !== MODE_CLOSED ? this : null;
Expand All @@ -64,21 +79,12 @@ class ShadyRoot {
rootData.firstChild = rootData.lastChild =
rootData.parentNode = rootData.nextSibling =
rootData.previousSibling = null;
rootData.childNodes = [];
// state flags
this._renderPending = false;
this._hasRendered = false;
// marsalled lazily
this._slotList = null;
/** @type {Object<string, Array<HTMLSlotElement>>} */
this._slotMap = null;
this._pendingSlots = null;
// NOTE: optimization flag, only require an asynchronous render
// to record parsed children if flag is not set.
if (utils.settings['preferPerformance']) {
let n;
while ((n = host[utils.NATIVE_PREFIX + 'firstChild'])) {
host[utils.NATIVE_PREFIX + 'removeChild'](n);
while ((n = this.host[utils.NATIVE_PREFIX + 'firstChild'])) {
this.host[utils.NATIVE_PREFIX + 'removeChild'](n);
}
} else {
this._asyncRender();
Expand Down Expand Up @@ -149,13 +155,11 @@ class ShadyRoot {
// if optimization flag is not set.
// on initial render remove any undistributed children.
if (!utils.settings['preferPerformance'] && !this._hasRendered) {
const c$ = this.host[utils.SHADY_PREFIX + 'childNodes'];
for (let i=0, l=c$.length; i < l; i++) {
const child = c$[i];
const data = shadyDataForNode(child);
if (child[utils.NATIVE_PREFIX + 'parentNode'] === this.host &&
(child.localName === 'slot' || !data.assignedSlot)) {
this.host[utils.NATIVE_PREFIX + 'removeChild'](child);
for (let n=this.host[utils.SHADY_PREFIX + 'firstChild']; n; n = n[utils.SHADY_PREFIX + 'nextSibling']) {
const data = shadyDataForNode(n);
if (n[utils.NATIVE_PREFIX + 'parentNode'] === this.host &&
(n.localName === 'slot' || !data.assignedSlot)) {
this.host[utils.NATIVE_PREFIX + 'removeChild'](n);
}
}
}
Expand Down Expand Up @@ -335,20 +339,18 @@ class ShadyRoot {
// Returns the list of nodes which should be rendered inside `node`.
_composeNode(node) {
let children = [];
let c$ = node[utils.SHADY_PREFIX + 'childNodes'];
for (let i = 0; i < c$.length; i++) {
let child = c$[i];
for (let n=node[utils.SHADY_PREFIX + 'firstChild']; n; n = n[utils.SHADY_PREFIX + 'nextSibling']) {
// Note: if we see a slot here, the nodes are guaranteed to need to be
// composed here. This is because if there is redistribution, it has
// already been handled by this point.
if (this._isInsertionPoint(child)) {
let flattenedNodes = shadyDataForNode(child).flattenedNodes;
if (this._isInsertionPoint(n)) {
let flattenedNodes = shadyDataForNode(n).flattenedNodes;
for (let j = 0; j < flattenedNodes.length; j++) {
let distributedNode = flattenedNodes[j];
children.push(distributedNode);
}
} else {
children.push(child);
children.push(n);
}
}
return children;
Expand All @@ -360,7 +362,7 @@ class ShadyRoot {

// Ensures that the rendered node list inside `container` is `children`.
_updateChildNodes(container, children) {
let composed = Array.prototype.slice.call(container[utils.NATIVE_PREFIX + 'childNodes']);
let composed = utils.nativeChildNodesArray(container);
let splices = calculateSplices(children, composed);
// process removals
for (let i=0, d=0, s; (i<splices.length) && (s=splices[i]); i++) {
Expand Down Expand Up @@ -462,7 +464,7 @@ class ShadyRoot {
let nA = listA[i];
let nB = listB[i];
if (nA !== nB) {
let c$ = Array.from(nA[utils.SHADY_PREFIX + 'parentNode'][utils.SHADY_PREFIX + 'childNodes']);
let c$ = utils.childNodesArray(nA[utils.SHADY_PREFIX + 'parentNode']);
return c$.indexOf(nA) - c$.indexOf(nB);
}
}
Expand Down Expand Up @@ -565,7 +567,29 @@ export const attachShadow = (host, options) => {
if (!options) {
throw new Error('Not enough arguments.');
}
return new ShadyRoot(ShadyRootConstructionToken, host, options);
let root;
// Optimization for booting up a shadowRoot from a fragment rather than
// creating one.
if (options['shadyUpgradeFragment'] && utils.canUpgrade()) {
root = options['shadyUpgradeFragment'];
root.__proto__ = ShadowRoot.prototype;
root._init(host, options);
recordChildNodes(root, root);
// Note: qsa is native when used with noPatch.
/** @type {?NodeList<Element>} */
const slotsAdded = root['__noInsertionPoint'] ? null : root.querySelectorAll('slot');
// Reset scoping information so normal scoing rules apply after this.
root['__noInsertionPoint'] = undefined;
// if a slot is added, must render containing root.
if (slotsAdded && slotsAdded.length) {
root._addSlots(slotsAdded);
root._asyncRender();
}
/** @type {ShadowRoot} */(root).host[utils.NATIVE_PREFIX + 'appendChild'](root);
} else {
root = new ShadyRoot(ShadyRootConstructionToken, host, options);
}
return root;
}

// Mitigate connect/disconnect spam by wrapping custom element classes.
Expand All @@ -585,9 +609,9 @@ if (window['customElements'] && utils.settings.inUse && !utils.settings['preferP
for (let i=0; i < r.length; i++) {
const e = r[i][0], value = r[i][1];
if (value) {
e.__shadydom_connectedCallback();
e['__shadydom_connectedCallback']();
} else {
e.__shadydom_disconnectedCallback();
e['__shadydom_disconnectedCallback']();
}
}
}
Expand All @@ -612,7 +636,7 @@ if (window['customElements'] && utils.settings.inUse && !utils.settings['preferP
if (connected || disconnected) {

/** @this {!HTMLElement} */
base.prototype.connectedCallback = base.prototype.__shadydom_connectedCallback = function() {
base.prototype.connectedCallback = base.prototype['__shadydom_connectedCallback'] = function() {
// if rendering defer connected
// otherwise connect only if we haven't already
if (isRendering) {
Expand All @@ -626,7 +650,7 @@ if (window['customElements'] && utils.settings.inUse && !utils.settings['preferP
}

/** @this {!HTMLElement} */
base.prototype.disconnectedCallback = base.prototype.__shadydom_disconnectedCallback = function() {
base.prototype.disconnectedCallback = base.prototype['__shadydom_disconnectedCallback'] = function() {
// if rendering, cancel a pending connection and queue disconnect,
// otherwise disconnect only if a connection has been allowed
if (isRendering) {
Expand All @@ -651,11 +675,9 @@ if (window['customElements'] && utils.settings.inUse && !utils.settings['preferP
}

const define = window['customElements']['define'];
// NOTE: Instead of patching customElements.define,
// re-define on the CustomElementRegistry.prototype.define
// for Safari 10 compatibility (it's flakey otherwise).
Object.defineProperty(window['CustomElementRegistry'].prototype, 'define', {
value: function(name, constructor) {
// Note, it would be better to patch the CustomElementRegistry.prototype, but
// ShadyCSS patches define directly.
window.customElements.define = function(name, constructor) {
Copy link
Contributor

Choose a reason for hiding this comment

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

If we're keeping this change, let's add a comment for why... did something else patch the instance rather than the prototype?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, ShadyCSS patches this directly. Changing is path of least resistance.

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

The "fast" (tearoff) native shim also patches the instance. So nevermind, let's just align on instance patching for now.

const connected = constructor.prototype.connectedCallback;
const disconnected = constructor.prototype.disconnectedCallback;
define.call(window['customElements'], name,
Expand All @@ -666,7 +688,6 @@ if (window['customElements'] && utils.settings.inUse && !utils.settings['preferP
constructor.prototype.connectedCallback = connected;
constructor.prototype.disconnectedCallback = disconnected;
}
});

}

Expand Down
53 changes: 26 additions & 27 deletions src/link-nodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,10 @@ import * as utils from './utils.js';
import {shadyDataForNode, ensureShadyDataForNode} from './shady-data.js';
import {patchInsideElementAccessors, patchOutsideElementAccessors} from './patch-instances.js';

function linkNode(node, container, ref_node) {
function linkNode(node, container, containerData, ref_node) {
patchOutsideElementAccessors(node);
ref_node = ref_node || null;
const nodeData = ensureShadyDataForNode(node);
const containerData = ensureShadyDataForNode(container);
const ref_nodeData = ref_node ? ensureShadyDataForNode(ref_node) : null;
// update ref_node.previousSibling <-> node
nodeData.previousSibling = ref_node ? ref_nodeData.previousSibling :
Expand Down Expand Up @@ -54,17 +53,15 @@ export const recordInsertBefore = (node, container, ref_node) => {
}
// handle document fragments
if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
let c$ = node[utils.SHADY_PREFIX + 'childNodes'];
for (let i=0; i < c$.length; i++) {
linkNode(c$[i], container, ref_node);
// Note, documentFragments should not have logical DOM so there's
// no need update that. It is possible to append a ShadowRoot, but we're
// choosing not to support that.
const first = node[utils.NATIVE_PREFIX + 'firstChild'];
for (let n = first; n; (n = n[utils.NATIVE_PREFIX + 'nextSibling'])) {
linkNode(n, container, containerData, ref_node);
}
// cleanup logical dom in doc fragment.
const nodeData = ensureShadyDataForNode(node);
let resetTo = (nodeData.firstChild !== undefined) ? null : undefined;
nodeData.firstChild = nodeData.lastChild = resetTo;
nodeData.childNodes = resetTo;
kevinpschaaf marked this conversation as resolved.
Show resolved Hide resolved
} else {
linkNode(node, container, ref_node);
linkNode(node, container, containerData, ref_node);
}
}

Expand Down Expand Up @@ -97,23 +94,25 @@ export const recordRemoveChild = (node, container) => {
}

/**
* @param {!Node} node
* @param {!Node|DocumentFragment} node
* @param {!Node|DocumentFragment=} adoptedParent
*/
export const recordChildNodes = (node) => {
export const recordChildNodes = (node, adoptedParent) => {
const nodeData = ensureShadyDataForNode(node);
if (nodeData.firstChild === undefined) {
// remove caching of childNodes
nodeData.childNodes = null;
const first = nodeData.firstChild = node[utils.NATIVE_PREFIX + 'firstChild'] || null;
nodeData.lastChild = node[utils.NATIVE_PREFIX + 'lastChild'] || null;
patchInsideElementAccessors(node);
for (let n = first, previous; n; (n = n[utils.NATIVE_PREFIX + 'nextSibling'])) {
const sd = ensureShadyDataForNode(n);
sd.parentNode = node;
sd.nextSibling = n[utils.NATIVE_PREFIX + 'nextSibling'] || null;
sd.previousSibling = previous || null;
previous = n;
patchOutsideElementAccessors(n);
}
if (!adoptedParent && nodeData.firstChild !== undefined) {
return;
}
// remove caching of childNodes
nodeData.childNodes = null;
const first = nodeData.firstChild = node[utils.NATIVE_PREFIX + 'firstChild'];
nodeData.lastChild = node[utils.NATIVE_PREFIX + 'lastChild'];
patchInsideElementAccessors(node);
for (let n = first, previous; n; (n = n[utils.NATIVE_PREFIX + 'nextSibling'])) {
const sd = ensureShadyDataForNode(n);
sd.parentNode = adoptedParent || node;
sd.nextSibling = n[utils.NATIVE_PREFIX + 'nextSibling'];
sd.previousSibling = previous || null;
previous = n;
patchOutsideElementAccessors(n);
}
}
2 changes: 1 addition & 1 deletion src/patch-events.js
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ export function addEventListener(type, fnOrObj, optionsOrCapture) {
const wrapperFn = function(e) {
// Support `once` option.
if (once) {
this[utils.SHADY_PREFIX + 'removeEventListener'](type, fnOrObj, nativeEventOptions);
this[utils.SHADY_PREFIX + 'removeEventListener'](type, fnOrObj, optionsOrCapture);
Copy link
Contributor

Choose a reason for hiding this comment

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

Not following this change...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The nativeEventOptions should only be passed to native methods. It's important to pass the actual options to the removeEventListener so we can find the wrapper function to remove.

}
if (!e['__target']) {
patchEvent(e);
Expand Down
Loading