-
-
Notifications
You must be signed in to change notification settings - Fork 2k
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
feat: Add native support for custom elements. #715
Changes from all commits
3927012
ab756ee
c90d907
3d68b2a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
{ | ||
"presets": [ | ||
["env", { | ||
"loose": true, | ||
"exclude": ["transform-es2015-typeof-symbol"], | ||
"targets": { | ||
"browsers": ["last 2 versions", "IE >= 9"] | ||
} | ||
}] | ||
], | ||
"plugins": [ | ||
"transform-object-rest-spread", | ||
"transform-react-jsx" | ||
] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,12 +3,18 @@ import options from '../options'; | |
|
||
|
||
/** Create an element with the given nodeName. | ||
* @param {String} nodeName | ||
* @param {String|Function} nodeName | ||
* @param {Boolean} [isSvg=false] If `true`, creates an element within the SVG namespace. | ||
* @returns {Element} node | ||
*/ | ||
export function createNode(nodeName, isSvg) { | ||
let node = isSvg ? document.createElementNS('http://www.w3.org/2000/svg', nodeName) : document.createElement(nodeName); | ||
let node; | ||
if (typeof nodeName==='function') { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This now has a branch so that it can create a custom element via newing the constructor. This covers both custom elements and customised built-in elements ( |
||
node = new nodeName(); | ||
nodeName = node.localName; | ||
} else { | ||
node = isSvg ? document.createElementNS('http://www.w3.org/2000/svg', nodeName) : document.createElement(nodeName); | ||
} | ||
node.normalizedNodeName = nodeName; | ||
return node; | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,18 +11,18 @@ export function isSameNodeType(node, vnode, hydrating) { | |
return node.splitText!==undefined; | ||
} | ||
if (typeof vnode.nodeName==='string') { | ||
return !node._componentConstructor && isNamedNode(node, vnode.nodeName); | ||
return !node._componentConstructor && isSameNodeName(node, vnode.nodeName); | ||
} | ||
return hydrating || node._componentConstructor===vnode.nodeName; | ||
return hydrating || node._componentConstructor===vnode.nodeName || node.constructor === vnode.nodeName; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Q: @treshugart - any idea if this will fail prior to CE registration? Sorry for the delays on this PR. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This part won't fail, but https://github.com/developit/preact/pull/715/files/3d68b2af246be137c3be8b43f3ea1be84fe59af0#diff-4935bb00e75b7854383877cbfd9d770fR13 would since we new it there. The worst that can happen at this point, using constructors, is allow it to get to the point of creation to fail because it wasn't registered. One thing to note here, may be that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Something we're playing around in Skate with is auto-defining custom elements that aren't defined in a non-destructive way (extending to avoid registration conflicts). See: https://github.com/skatejs/skatejs/blob/master/packages/renderer-preact/src/index.js#L11. You probably don't want to do that here - at least yet - but that's really the only thing we can do besides error with a helpful message that the custom element hasn't been defined yet. Unfortunately the only good way to check that would be to get some movement on WICG/webcomponents#566. Currently you have to pretty much try / catch, which I'm not sure you want to do in the critical path. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. whoo yeah that's hefty. I'll be keeping an eye on 566... |
||
} | ||
|
||
|
||
/** Check if an Element has a given normalized name. | ||
* @param {Element} node | ||
* @param {String} nodeName | ||
* @param {Element} node | ||
* @param {mixed} nodeName | ||
*/ | ||
export function isNamedNode(node, nodeName) { | ||
return node.normalizedNodeName===nodeName || node.nodeName.toLowerCase()===nodeName.toLowerCase(); | ||
export function isSameNodeName(node, nodeName) { | ||
return node.normalizedNodeName===nodeName || (nodeName.toLowerCase && node.nodeName.toLowerCase()===nodeName.toLowerCase()); | ||
} | ||
|
||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
/** @jsx h */ | ||
|
||
import { h, render } from '../../src/preact'; | ||
|
||
window.customElements && describe('Custom elements', () => { | ||
class CustomElement1 extends HTMLElement {} | ||
class CustomElement2 extends HTMLElement {} | ||
customElements.define('custom-element-1', CustomElement1); | ||
customElements.define('custom-element-2', CustomElement2); | ||
|
||
let refs, refsIndex, root, scratch; | ||
const ref = () => e => e && (refs[refsIndex++] = e); | ||
const tmp = jsx => (root = render(jsx, scratch, root)); | ||
|
||
beforeEach(() => { | ||
refs = []; | ||
refsIndex = 0; | ||
scratch = document.createElement('div'); | ||
document.body.appendChild(scratch); | ||
}); | ||
|
||
afterEach(() => { | ||
scratch.parentNode.removeChild(scratch); | ||
}); | ||
|
||
it('should not be replaced if they are the same', () => { | ||
tmp(<CustomElement1 ref={ref()} />); | ||
tmp(<CustomElement1 ref={ref()} />); | ||
expect(refs[0]).to.equal(refs[1]); | ||
}); | ||
|
||
it('should be replaced if they are different', () => { | ||
tmp(<CustomElement1 ref={ref()} />); | ||
tmp(<CustomElement2 ref={ref()} />); | ||
expect(refs[0]).not.to.equal(refs[1]); | ||
}); | ||
|
||
it('should replace keyed, different children', () => { | ||
tmp( | ||
<div> | ||
<CustomElement1 key="0" ref={ref()} /> | ||
<CustomElement1 key="1" ref={ref()} /> | ||
<CustomElement1 key="2" ref={ref()} /> | ||
</div> | ||
); | ||
tmp( | ||
<div> | ||
<CustomElement2 key="0" ref={ref()} /> | ||
<CustomElement2 key="1" ref={ref()} /> | ||
<CustomElement2 key="2" ref={ref()} /> | ||
</div> | ||
); | ||
expect(refs[0]).to.not.equal(refs[3]); | ||
expect(refs[1]).to.not.equal(refs[4]); | ||
expect(refs[2]).to.not.equal(refs[5]); | ||
}); | ||
|
||
it('should not replace keyed, same children', () => { | ||
tmp( | ||
<div> | ||
<CustomElement1 key="0" ref={ref()} /> | ||
<CustomElement1 key="1" ref={ref()} /> | ||
<CustomElement1 key="2" ref={ref()} /> | ||
</div> | ||
); | ||
tmp( | ||
<div> | ||
<CustomElement1 key="0" ref={ref()} /> | ||
<CustomElement1 key="1" ref={ref()} /> | ||
<CustomElement1 key="2" ref={ref()} /> | ||
</div> | ||
); | ||
expect(refs[0]).to.equal(refs[3]); | ||
expect(refs[1]).to.equal(refs[4]); | ||
expect(refs[2]).to.equal(refs[5]); | ||
}); | ||
|
||
it('should replace un-keyed (plucked), different children', () => { | ||
tmp( | ||
<div> | ||
<CustomElement1 ref={ref()} /> | ||
<CustomElement1 ref={ref()} /> | ||
<CustomElement1 ref={ref()} /> | ||
</div> | ||
); | ||
tmp( | ||
<div> | ||
<CustomElement2 ref={ref()} /> | ||
<CustomElement2 ref={ref()} /> | ||
<CustomElement2 ref={ref()} /> | ||
</div> | ||
); | ||
expect(refs[0]).to.not.equal(refs[3]); | ||
expect(refs[1]).to.not.equal(refs[4]); | ||
expect(refs[2]).to.not.equal(refs[5]); | ||
}); | ||
|
||
it('should not replace un-keyed (pluked), same children', () => { | ||
tmp( | ||
<div> | ||
<CustomElement1 ref={ref()} /> | ||
<CustomElement1 ref={ref()} /> | ||
<CustomElement1 ref={ref()} /> | ||
</div> | ||
); | ||
tmp( | ||
<div> | ||
<CustomElement1 ref={ref()} /> | ||
<CustomElement1 ref={ref()} /> | ||
<CustomElement1 ref={ref()} /> | ||
</div> | ||
); | ||
expect(refs[0]).to.equal(refs[3]); | ||
expect(refs[1]).to.equal(refs[4]); | ||
expect(refs[2]).to.equal(refs[5]); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
/* eslint-disable */ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the ES5 adapter needed to use ES5 "classes" with native custom elements. |
||
|
||
(function () { | ||
'use strict'; | ||
|
||
(()=>{'use strict';if(!window.customElements)return;const a=window.HTMLElement,b=window.customElements.define,c=window.customElements.get,d=new Map,e=new Map;let f=!1,g=!1;window.HTMLElement=function(){if(!f){const j=d.get(this.constructor),k=c.call(window.customElements,j);g=!0;const l=new k;return l}f=!1;},window.HTMLElement.prototype=a.prototype;Object.defineProperty(window,'customElements',{value:window.customElements,configurable:!0,writable:!0}),Object.defineProperty(window.customElements,'define',{value:(j,k)=>{const l=k.prototype,m=class extends a{constructor(){super(),Object.setPrototypeOf(this,l),g||(f=!0,k.call(this)),g=!1;}},n=m.prototype;m.observedAttributes=k.observedAttributes,n.connectedCallback=l.connectedCallback,n.disconnectedCallback=l.disconnectedCallback,n.attributeChangedCallback=l.attributeChangedCallback,n.adoptedCallback=l.adoptedCallback,d.set(k,j),e.set(j,k),b.call(window.customElements,j,m);},configurable:!0,writable:!0}),Object.defineProperty(window.customElements,'get',{value:(j)=>e.get(j),configurable:!0,writable:!0});})(); | ||
|
||
/** | ||
@license | ||
Copyright (c) 2017 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 | ||
*/ | ||
|
||
}()); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -99,6 +99,10 @@ module.exports = function(config) { | |
customLaunchers: sauceLabs ? sauceLabsLaunchers : travisLaunchers, | ||
|
||
files: [ | ||
// We can't load this up front because it's ES2015 and we need it only | ||
// for certain tests that run under those conditions. We also can't load | ||
// it via CDN because { included: false } won't work. | ||
{ pattern: 'custom-elements-es5-adapter.js', included: false }, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unfortunately, I couldn't find a way to get karma to conditionally load stuff from a CDN, so I had to include it. |
||
{ pattern: 'polyfills.js', watched: false }, | ||
{ pattern: '{browser,shared}/**.js', watched: false } | ||
], | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,3 +3,11 @@ import 'core-js/es6/map'; | |
import 'core-js/fn/array/fill'; | ||
import 'core-js/fn/array/from'; | ||
import 'core-js/fn/object/assign'; | ||
|
||
// We add the ES5 adapter because src / test are converted to ES5 and native | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Comment is self-explanatory, but thought I should highlight this as part of the caveats listed in the main description of the PR. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @WebReflection's ideas from babel-plugin-transform-builtin-classes have been incorporated into Babel 7, so at some point this adapter will not be needed. I already use WebComponents.js without native-shim in my projects, transpiling with Babel 6 + babel-plugin-transform-builtin-classes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The result of using babel-plugin-transform-builtin-classes is nice because the transpiled code runs in all browsers (old and new). |
||
// custom elements uses native ES2015. Unfortunately. This needs to be done | ||
// sync so tests that assert this functionality will have the ES5 shim loaded | ||
// by the time they run. | ||
if (window.customElements) { | ||
document.write('<script src="https://unpkg.com/@webcomponents/[email protected]/custom-elements-es5-adapter.js"></script>'); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From #683.