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 polyfills for select ParentNode APIs. #389

Merged
merged 24 commits into from
Oct 15, 2020
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
57d816f
Add MDN snippets for ParentNode's append and prepend.
bicknellr Oct 1, 2020
6a82c7b
Add test stubs for ParentNode append and prepend.
bicknellr Oct 1, 2020
1c4f39d
Add tests for append.
bicknellr Oct 1, 2020
cc7e026
Add tests for prepend.
bicknellr Oct 1, 2020
5af15ea
Test for append vs prepend behavior.
bicknellr Oct 1, 2020
43aa4a6
IE11 requires an argument for createHTMLDocument.
bicknellr Oct 1, 2020
3358ac1
Write custom append / prepend polyfills because the MDN snippets woul…
bicknellr Oct 1, 2020
1f31671
In Safari 9, the `firstChild` descriptor's `get` and `set` are undefi…
bicknellr Oct 2, 2020
615805e
In Chrome 41, `firstChild` is a per-instance value descriptor.
bicknellr Oct 2, 2020
6a3de6c
Polyfill replaceChildren.
bicknellr Oct 2, 2020
bf56cd0
Shady DOM: Add basic wrappers for append, prepend, and replaceChildren.
bicknellr Oct 2, 2020
b99798a
Shady DOM: Add tests for append, prepend, and replaceChildren.
bicknellr Oct 2, 2020
e1e498f
Import ParentNode polyfills into Shady DOM tests directly. (...)
bicknellr Oct 2, 2020
faf0511
Prevent Closure from renaming `replaceChildren`.
bicknellr Oct 2, 2020
a78100b
The linter doesn't like raw assignments as conditionals.
bicknellr Oct 2, 2020
65134c3
Merge remote-tracking branch 'origin/master' into parent-node
bicknellr Oct 7, 2020
d8c8854
Update changelogs.
bicknellr Oct 7, 2020
d7e72c3
Add ParentNode methods to Wrapper.
bicknellr Oct 8, 2020
4c53900
Implement 'convert nodes into a node' to reduce Shady DOM work and Mu…
bicknellr Oct 8, 2020
ce8b140
Fix a bug where `append`, `prepend`, `replaceChildren` was broken whe…
bicknellr Oct 12, 2020
210e89b
Refactor and fix titles in tests.
bicknellr Oct 12, 2020
4dd308c
Factor out a function to convert a value into a node.
bicknellr Oct 14, 2020
e915283
Merge remote-tracking branch 'origin/master' into parent-node
bicknellr Oct 15, 2020
664abc2
Remove explicit string conversion in `convertIntoANode`; browsers thr…
bicknellr Oct 15, 2020
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
5 changes: 4 additions & 1 deletion packages/shadydom/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

<!-- ## [Unreleased] -->
## [Unreleased]

- Add support for select ParentNode APIs.
([#389](https://github.com/webcomponents/polyfills/pull/389))

## [1.7.4] - 2020-07-20

Expand Down
6 changes: 4 additions & 2 deletions packages/shadydom/src/patch-native.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,10 @@ const ParentNodeAccessors = [

const ParentNodeMethods = [
'querySelector',
'querySelectorAll'
// 'append', 'prepend'
'querySelectorAll',
'append',
'prepend',
'replaceChildren',
Comment on lines +97 to +100
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Was this the right thing to do here? I'm not totally sure what this list is about. There seems to be some stuff later in this file that implies that they're on the wrong prototype in older Safari and that this is just about putting them in the correct spot - should I consider doing this for ChildNode in #390 too?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think the concern was IE/Edge.

It would be good to verify (perhaps just via one-off manual testing rather than in the code itself) that these properties do not exist higher on the prototype chain than where we patch them on any browser. This would cause the patch to fail.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I checked the earliest version of the browsers implementing append (Edge 17, Firefox 49, Chrome 54, Safari 10) and they all have the descriptor on Document, DocumentFragment, and Element prototypes but not the HTMLDocument, HTMLElement, or ShadowRoot (if it exists) prototypes, so they should be alright assuming they haven't regressed and moved them around since.

];

export const addNativePrefixedProperties = () => {
Expand Down
22 changes: 21 additions & 1 deletion packages/shadydom/src/patches/ParentNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,27 @@ export const ParentNodePatches = utils.getOwnPropertyDescriptors({
return children.length;
}
return 0;
}
},

/** @this {Element} */
append(...args) {
this[utils.SHADY_PREFIX + 'insertBefore'](utils.convertNodesIntoANode(...args), null);
},

/** @this {Element} */
prepend(...args) {
this[utils.SHADY_PREFIX + 'insertBefore'](
utils.convertNodesIntoANode(...args), this[utils.SHADY_PREFIX + 'firstChild']);
},

/** @this {Element} */
['replaceChildren'](...args) {
let child;
while ((child = this[utils.SHADY_PREFIX + 'firstChild']) !== null) {
this[utils.SHADY_PREFIX + 'removeChild'](child);
}
this[utils.SHADY_PREFIX + 'insertBefore'](utils.convertNodesIntoANode(...args), null);
},

});

Expand Down
14 changes: 14 additions & 0 deletions packages/shadydom/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,3 +230,17 @@ export const assign = (target, source) => {
export const arrayFrom = (object) => {
return [].slice.call(/** @type {IArrayLike} */(object));
};

// Implements 'convert nodes into a node'.
// https://dom.spec.whatwg.org/#converting-nodes-into-a-node
export const convertNodesIntoANode = (...args) => {
if (args.length === 1) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

What if the only argument is a string? It won't get converted to a node in that case.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Fix and add test?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good catch, should be fixed and tested now.

return (typeof args[0] === 'string' ? document.createTextNode(args[0]) : args[0]);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's factor this out since it's done 2x?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ok, I moved it out. I also found that this expression doesn't exactly match what current browsers do so I updated it also. (AFAICT, the spec text for this doesn't say what happens to non-string, non-node values in this list but all browsers seem to just coerce to a string and make a text node from that string.)

}

const fragment = document.createDocumentFragment();
for (const arg of args) {
fragment.appendChild(typeof arg === 'string' ? document.createTextNode(arg) : arg);
}
return fragment;
};
12 changes: 12 additions & 0 deletions packages/shadydom/src/wrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,18 @@ class Wrapper {
return this.node[utils.SHADY_PREFIX + 'className'] = value;
}

append(...args) {
return this.node[utils.SHADY_PREFIX + 'append'](...args);
}

prepend(...args) {
return this.node[utils.SHADY_PREFIX + 'prepend'](...args);
}

replaceChildren(...args) {
return this.node[utils.SHADY_PREFIX + 'replaceChildren'](...args);
}

}

const addEventPropertyWrapper = (name) => {
Expand Down
123 changes: 123 additions & 0 deletions packages/tests/shadydom/shady.html
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,129 @@
assert.strictEqual(getComposedHTML(host), '<b></b>');
});

test('append - mutate host', function() {
ShadyDOM.wrapIfNeeded(host).innerHTML = '<a>Hello</a>';
ShadyDOM.wrapIfNeeded(ShadyDOM.wrapIfNeeded(host).shadowRoot).innerHTML = '<slot></slot>';
ShadyDOM.flush();
assert.strictEqual(getComposedHTML(host), '<a>Hello</a>');
host.append(document.createElement('b'), 'c', document.createElement('d'));
ShadyDOM.flush();
assert.strictEqual(getComposedHTML(host), '<a>Hello</a><b></b>c<d></d>');
});

test('append - mutate host - single string', function() {
ShadyDOM.wrapIfNeeded(host).innerHTML = '<a>Hello</a>';
ShadyDOM.wrapIfNeeded(ShadyDOM.wrapIfNeeded(host).shadowRoot).innerHTML = '<slot></slot>';
ShadyDOM.flush();
assert.strictEqual(getComposedHTML(host), '<a>Hello</a>');
host.append('b');
ShadyDOM.flush();
assert.strictEqual(getComposedHTML(host), '<a>Hello</a>b');
});

test('append - mutate shadow', function() {
ShadyDOM.wrapIfNeeded(host).innerHTML = '<a>Hello</a>';
ShadyDOM.wrapIfNeeded(ShadyDOM.wrapIfNeeded(host).shadowRoot).innerHTML = '<slot></slot>';
ShadyDOM.flush();
assert.strictEqual(getComposedHTML(host), '<a>Hello</a>');
ShadyDOM.wrapIfNeeded(ShadyDOM.wrapIfNeeded(host).shadowRoot).append(
document.createElement('b'), 'c', document.createElement('d'));
ShadyDOM.flush();
assert.strictEqual(getComposedHTML(host), '<a>Hello</a><b></b>c<d></d>');
});

test('append - mutate shadow - single string', function() {
ShadyDOM.wrapIfNeeded(host).innerHTML = '<a>Hello</a>';
ShadyDOM.wrapIfNeeded(ShadyDOM.wrapIfNeeded(host).shadowRoot).innerHTML = '<slot></slot>';
ShadyDOM.flush();
assert.strictEqual(getComposedHTML(host), '<a>Hello</a>');
ShadyDOM.wrapIfNeeded(ShadyDOM.wrapIfNeeded(host).shadowRoot).append('b');
ShadyDOM.flush();
assert.strictEqual(getComposedHTML(host), '<a>Hello</a>b');
});

test('prepend - mutate host', function() {
ShadyDOM.wrapIfNeeded(host).innerHTML = '<a>Hello</a>';
ShadyDOM.wrapIfNeeded(ShadyDOM.wrapIfNeeded(host).shadowRoot).innerHTML = '<slot></slot>';
ShadyDOM.flush();
assert.strictEqual(getComposedHTML(host), '<a>Hello</a>');
host.prepend(document.createElement('b'), 'c', document.createElement('d'));
ShadyDOM.flush();
assert.strictEqual(getComposedHTML(host), '<b></b>c<d></d><a>Hello</a>');
});

test('prepend - mutate host - single string', function() {
ShadyDOM.wrapIfNeeded(host).innerHTML = '<a>Hello</a>';
ShadyDOM.wrapIfNeeded(ShadyDOM.wrapIfNeeded(host).shadowRoot).innerHTML = '<slot></slot>';
ShadyDOM.flush();
assert.strictEqual(getComposedHTML(host), '<a>Hello</a>');
host.prepend('b');
ShadyDOM.flush();
assert.strictEqual(getComposedHTML(host), 'b<a>Hello</a>');
});

test('prepend - mutate shadow', function() {
ShadyDOM.wrapIfNeeded(host).innerHTML = '<a>Hello</a>';
ShadyDOM.wrapIfNeeded(ShadyDOM.wrapIfNeeded(host).shadowRoot).innerHTML = '<slot></slot>';
ShadyDOM.flush();
assert.strictEqual(getComposedHTML(host), '<a>Hello</a>');
ShadyDOM.wrapIfNeeded(ShadyDOM.wrapIfNeeded(host).shadowRoot).prepend(
document.createElement('b'), 'c', document.createElement('d'));
ShadyDOM.flush();
assert.strictEqual(getComposedHTML(host), '<b></b>c<d></d><a>Hello</a>');
});

test('prepend - mutate shadow - single string', function() {
ShadyDOM.wrapIfNeeded(host).innerHTML = '<a>Hello</a>';
ShadyDOM.wrapIfNeeded(ShadyDOM.wrapIfNeeded(host).shadowRoot).innerHTML = '<slot></slot>';
ShadyDOM.flush();
assert.strictEqual(getComposedHTML(host), '<a>Hello</a>');
ShadyDOM.wrapIfNeeded(ShadyDOM.wrapIfNeeded(host).shadowRoot).prepend('b');
ShadyDOM.flush();
assert.strictEqual(getComposedHTML(host), 'b<a>Hello</a>');
});

test('replaceChildren - mutate host', function() {
ShadyDOM.wrapIfNeeded(host).innerHTML = '<a>Hello</a>';
ShadyDOM.wrapIfNeeded(ShadyDOM.wrapIfNeeded(host).shadowRoot).innerHTML = '<slot></slot>';
ShadyDOM.flush();
assert.strictEqual(getComposedHTML(host), '<a>Hello</a>');
host.replaceChildren(document.createElement('b'), 'c', document.createElement('d'));
ShadyDOM.flush();
assert.strictEqual(getComposedHTML(host), '<b></b>c<d></d>');
});

test('replaceChildren - mutate host - single string', function() {
ShadyDOM.wrapIfNeeded(host).innerHTML = '<a>Hello</a>';
ShadyDOM.wrapIfNeeded(ShadyDOM.wrapIfNeeded(host).shadowRoot).innerHTML = '<slot></slot>';
ShadyDOM.flush();
assert.strictEqual(getComposedHTML(host), '<a>Hello</a>');
host.replaceChildren('b');
ShadyDOM.flush();
assert.strictEqual(getComposedHTML(host), 'b');
});

test('replaceChildren - mutate shadow', function() {
ShadyDOM.wrapIfNeeded(host).innerHTML = '<a>Hello</a>';
ShadyDOM.wrapIfNeeded(ShadyDOM.wrapIfNeeded(host).shadowRoot).innerHTML = '<slot></slot>';
ShadyDOM.flush();
assert.strictEqual(getComposedHTML(host), '<a>Hello</a>');
ShadyDOM.wrapIfNeeded(ShadyDOM.wrapIfNeeded(host).shadowRoot).replaceChildren(
document.createElement('b'), 'c', document.createElement('d'));
ShadyDOM.flush();
assert.strictEqual(getComposedHTML(host), '<b></b>c<d></d>');
});

test('replaceChildren - mutate shadow - single string', function() {
ShadyDOM.wrapIfNeeded(host).innerHTML = '<a>Hello</a>';
ShadyDOM.wrapIfNeeded(ShadyDOM.wrapIfNeeded(host).shadowRoot).innerHTML = '<slot></slot>';
ShadyDOM.flush();
assert.strictEqual(getComposedHTML(host), '<a>Hello</a>');
ShadyDOM.wrapIfNeeded(ShadyDOM.wrapIfNeeded(host).shadowRoot).replaceChildren('b');
ShadyDOM.flush();
assert.strictEqual(getComposedHTML(host), 'b');
});

test('querySelectorAll .shadowRoot', function() {
ShadyDOM.wrapIfNeeded(host).innerHTML = '<div id="main"></div>';
ShadyDOM.wrapIfNeeded(ShadyDOM.wrapIfNeeded(host).shadowRoot).innerHTML = '<slot></slot><span id="main"></span>' +
Expand Down
127 changes: 127 additions & 0 deletions packages/tests/webcomponentsjs_/parent-node/append.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<!doctype html>
<!--
@license
Copyright (c) 2020 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>
<title>ParentNode append</title>
<script>
for (const item of [
Document.prototype,
DocumentFragment.prototype,
Element.prototype,
]) {
delete item.append;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why is this needed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This and the others are here to remove the built-in implementations so that we can test it in browsers that actually do support it. I could also remove it if you think it's better to just run the tests against the built-in implementation, WDYT?

Copy link
Collaborator

Choose a reason for hiding this comment

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

OK, I missed the fact that this isn't a shadydom test.

}
</script>
<script src="../../node_modules/@webcomponents/webcomponentsjs/bundles/webcomponents-pf_dom.js"></script>
<script src="../wct-config.js"></script>
<script src="../../node_modules/wct-browser-legacy/browser.js"></script>
</head>
<body>
<script>
suite('ParentNode append', function() {
suite('Document', function() {
test('Appends an element', () => {
const doc = document.implementation.createHTMLDocument('');
while (doc.firstChild) {
doc.removeChild(doc.firstChild);
}

const child = document.createElement('html');
doc.append(child);

assert(doc.childNodes.length === 1);
assert(doc.childNodes[0] === child);
});
});

suite('DocumentFragment', function() {
test('Appends an element', () => {
const container = document.createDocumentFragment();
container.appendChild(document.createElement('div'));

const child = document.createElement('div');
container.append(child);

assert(child.parentNode === container);
assert(container.childNodes.length === 2);
assert(container.childNodes[1] === child);
});

test('Appends a text node', () => {
const container = document.createDocumentFragment();
container.appendChild(document.createElement('div'));

container.append('This is some text.');

assert(container.childNodes.length === 2);
assert(container.childNodes[1].nodeType === Node.TEXT_NODE);
assert(container.childNodes[1].textContent === 'This is some text.');
});

test('Appends multiple types of nodes', () => {
const container = document.createDocumentFragment();
container.appendChild(document.createElement('div'));

const child1 = document.createElement('div');
const child3 = document.createElement('div');
container.append(child1, 'This is some text.', child3);

assert(container.childNodes.length === 4);
assert(container.childNodes[1] === child1);
assert(container.childNodes[2].nodeType === Node.TEXT_NODE);
assert(container.childNodes[2].textContent === 'This is some text.');
assert(container.childNodes[3] === child3);
});
});

suite('Element', function() {
test('Appends an element', () => {
const container = document.createElement('div');
container.appendChild(document.createElement('div'));

const child = document.createElement('div');
container.append(child);

assert(child.parentNode === container);
assert(container.childNodes.length === 2);
assert(container.childNodes[1] === child);
});

test('Appends a text node', () => {
const container = document.createElement('div');
container.appendChild(document.createElement('div'));

container.append('This is some text.');

assert(container.childNodes.length === 2);
assert(container.childNodes[1].nodeType === Node.TEXT_NODE);
assert(container.childNodes[1].textContent === 'This is some text.');
});

test('Appends multiple types of nodes', () => {
const container = document.createElement('div');
container.appendChild(document.createElement('div'));

const child1 = document.createElement('div');
const child3 = document.createElement('div');
container.append(child1, 'This is some text.', child3);

assert(container.childNodes.length === 4);
assert(container.childNodes[1] === child1);
assert(container.childNodes[2].nodeType === Node.TEXT_NODE);
assert(container.childNodes[2].textContent === 'This is some text.');
assert(container.childNodes[3] === child3);
});
});
});
</script>
</body>
</html>
9 changes: 9 additions & 0 deletions packages/tests/webcomponentsjs_/parent-node/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!doctype html>
<script src="../../../../node_modules/wct-browser-legacy/browser.js"></script>
<script>
WCT.loadSuites([
'./append.html',
'./prepend.html',
'./replace-children.html',
]);
</script>
Loading