1 | # Custom Elements (v1) Polyfill [![Build Status](https://travis-ci.org/webcomponents/custom-elements.svg?branch=master)](https://travis-ci.org/webcomponents/custom-elements)
|
2 |
|
3 | A polyfill for the [custom elements](https://html.spec.whatwg.org/multipage/scripting.html#custom-elements)
|
4 | v1 spec.
|
5 |
|
6 | ## Using
|
7 |
|
8 | Include `custom-elements.min.js` at the beginning of your page, *before* any code that
|
9 | manipulates the DOM:
|
10 | ```html
|
11 | <script src="custom-elements.min.js"></script>
|
12 | ```
|
13 |
|
14 | ## Developing
|
15 |
|
16 | 1. Install and build
|
17 |
|
18 | ```
|
19 | npm install
|
20 | npm run build
|
21 | ```
|
22 | (Or, `npm i && gulp`, if [gulp](https://github.com/gulpjs/gulp) is installed globally.)
|
23 |
|
24 | 1. Test
|
25 |
|
26 | ```
|
27 | npm run test
|
28 | ```
|
29 | (Or, [`wct`](https://github.com/Polymer/web-component-tester), if installed
|
30 | globally.)
|
31 |
|
32 | ## Custom element reactions in the DOM and HTML specs
|
33 |
|
34 | API which might trigger custom element reactions in the [DOM](https://dom.spec.whatwg.org/)
|
35 | and [HTML](https://html.spec.whatwg.org/) specifications are marked with the
|
36 | [`CEReactions` extended attribute](https://html.spec.whatwg.org/multipage/scripting.html#cereactions).
|
37 |
|
38 | ## Known Bugs and Limitations
|
39 |
|
40 | - `adoptedCallback` is not supported.
|
41 | - Changing an attribute of a customizable (but uncustomized) element will not
|
42 | cause that element to upgrade.
|
43 | - Only DOM API is patched. Notably, this excludes API from the HTML spec marked
|
44 | with the `CEReactions` extended attribute.
|
45 | - Unpatched API from the DOM spec:
|
46 | - Setters on `Element` for `id`, `className`, and `slot`.
|
47 | - `DOMTokenList` (`element.classList`)
|
48 | - `NamedNodeMap` (`element.attributes`)
|
49 | - `Attr` (`element.attributes.getNamedItem('attr-name')`)
|
50 | - The [custom element reactions stack](https://html.spec.whatwg.org/multipage/scripting.html#custom-element-reactions-stack)
|
51 | is not implemented.
|
52 | - Typically, DOM operations patched in this polyfill gather the list of
|
53 | elements to which a given callback would apply and then iterate that list,
|
54 | calling the callback on each element. This mechanism breaks down if an
|
55 | element's callback performs another DOM operation that manipulates an area
|
56 | of the tree that was captured in the outer operation's list of elements.
|
57 | When this happens, the callbacks from the inner DOM operation will be called
|
58 | *before* those of the outer DOM operation (typically, depending on the patch
|
59 | implementation), as opposed to a spec-compliant implementation where the
|
60 | callbacks are always run in the order they were inserted into each
|
61 | particular element's reaction queue.
|
62 | - Custom elements created by the UA's parser are customized as if they were
|
63 | upgraded, rather than constructed.
|
64 | - These elements are only learned about *after* they have been constructed,
|
65 | and typically after their descendants have been constructed. When these
|
66 | elements are constructed, their children are visible and editable *even
|
67 | though they would not yet exist and manipulating them would throw in a
|
68 | spec-compliant implementation of custom elements!*
|
69 | - The [requirements for custom element constructors](https://html.spec.whatwg.org/multipage/scripting.html#custom-element-conformance)
|
70 | are not enforced.
|
71 | - These requirements are not generally enforcable in user script because of
|
72 | the ability to use the `new` operator on a custom element constructor. This
|
73 | means there is no way to know when a call to a constructor has begun or
|
74 | finished.
|
75 | - Methods of the `ParentNode` and `ChildNode` interfaces do not support
|
76 | `DocumentFragment`s as arguments.
|
77 | - Your custom element constructor's prototype *must* have a property named
|
78 | `constructor` which is that constructor.
|
79 | - By default, for every constructable function `F`, `F.prototype.constructor === F`.
|
80 | If you replace the prototype of your constructor `F`, you must make sure
|
81 | that `F.prototype.constructor === F` remains true. Otherwise, the polyfill
|
82 | will not be able to create or upgrade your custom elements.
|
83 | - The [`:defined` CSS pseudo-class](https://html.spec.whatwg.org/multipage/semantics-other.html#pseudo-classes)
|
84 | is not supported.
|
85 |
|
86 | ### ES5 vs ES2015
|
87 |
|
88 | The custom elements v1 spec is not compatible with ES5 style classes. This means
|
89 | ES2015 code compiled to ES5 will not work with a native implementation of Custom
|
90 | Elements.[0] While it's possible to force the custom elements polyfill to be
|
91 | used to workaround this issue (by setting (`customElements.forcePolyfill = true;`
|
92 | before loading the polyfill), you will not be using the UA's native
|
93 | implementation in that case.
|
94 |
|
95 | Since this is not ideal, we've provided an alternative:
|
96 | [native-shim.js](https://github.com/webcomponents/custom-elements/blob/master/src/native-shim.js).
|
97 | Loading this shim minimally augments the native implementation to be compatible
|
98 | with ES5 code. We are also working on some future refinements to this approach
|
99 | that will improve the implementation and automatically detect if it's needed.
|
100 |
|
101 | [0] The spec requires that an element call the `HTMLElement` constructor.
|
102 | Typically an ES5 style class would do something like `HTMLElement.call(this)` to
|
103 | emulate `super()`. However, `HTMLElement` *must* be called as a constructor and
|
104 | not as a plain function, i.e. with `Reflect.construct(HTMLElement, [], MyCEConstructor)`,
|
105 | or it will throw.
|
106 |
|
107 | ### Parser-created elements in the main document
|
108 |
|
109 | By default, the polyfill uses a `MutationObserver` to learn about and upgrade
|
110 | elements in the main document as they are parsed. This `MutationObserver` is
|
111 | attached to `document` synchronously when the script is run.
|
112 | - If you attach a `MutationObserver` earlier before loading the polyfill, that
|
113 | mutation observer will not see upgraded custom elements.
|
114 | - If you move a node with descendants that have not yet been inserted by the
|
115 | parser out of the main document, those nodes will not be noticed or upgraded
|
116 | (until another action would trigger an upgrade).
|
117 |
|
118 | Note: Using `polyfillWrapFlushCallback` disconnects this `MutationObserver`.
|
119 |
|
120 | ### `customElements.polyfillWrapFlushCallback`
|
121 |
|
122 | tl;dr: The polyfill gets slower as the size of your page and number of custom
|
123 | element definitons increases. You can use `polyfillWrapFlushCallback` to prevent
|
124 | redundant work.
|
125 |
|
126 | To avoid a potential memory leak, the polyfill does not maintain a list of upgrade
|
127 | candidates. This means that calling `customElements.define` causes a synchronous,
|
128 | full-document walk to search for elements with `localName`s matching the new
|
129 | definition. Given that this operation is potentially expensive and, if your page
|
130 | loads many custom element definitions before using any of them, highly redundant,
|
131 | an extra method is added to the `CustomElementRegistry` prototype -
|
132 | `polyfillWrapFlushCallback`.
|
133 |
|
134 | `polyfillWrapFlushCallback` allows you to block the synchronous, full-document
|
135 | upgrade attempts made when calling `define` and perform them later. Call
|
136 | `polyfillWrapFlushCallback` with a function; the next time `customElements.define`
|
137 | is called and a full-document upgrade would happen, your function will be called
|
138 | instead. The only argument to your function is *another* function which, when
|
139 | called, will run the full-document upgrade attempt.
|
140 |
|
141 | For example, if you wanted to delay upgrades until the document's ready state
|
142 | was `'complete'`, you could use the following:
|
143 |
|
144 | ```javascript
|
145 | customElements.polyfillWrapFlushCallback(function(flush) {
|
146 | if (document.readyState === 'complete') {
|
147 | // If the document is already complete, flush synchronously.
|
148 | flush();
|
149 | } else {
|
150 | // Otherwise, wait until it is complete.
|
151 | document.addEventListener('readystatechange', function() {
|
152 | if (document.readyState === 'complete') {
|
153 | flush();
|
154 | }
|
155 | });
|
156 | }
|
157 | });
|
158 | ```
|
159 |
|
160 | Once your wrapper function is called (because the polyfill wants to upgrade the
|
161 | document), it will not be called again until you have triggered the
|
162 | full-document upgrade attempt. If multiple definitions are registered before you
|
163 | trigger upgrades, all of those definitions will apply when you trigger upgrades -
|
164 | don't call the provided function multiple times.
|
165 |
|
166 | Promises returned by `customElements.whenDefined` will not resolve until a
|
167 | full-document upgrade attempt has been performed *after* the given local name
|
168 | has been defined.
|
169 |
|
170 | ```javascript
|
171 | let flush;
|
172 | customElements.polyfillWrapFlushCallback(f => flush = f);
|
173 |
|
174 | const p = customElements.whenDefined('c-e', () => console.log('c-e defined'));
|
175 |
|
176 | customElements.define('c-e', class extends HTMLElement {});
|
177 | // `p` is not yet resolved; `flush` is now a function.
|
178 |
|
179 | flush(); // Resolves `p`.
|
180 | ```
|
181 |
|
182 | You can't remove a callback given to `polyfillWrapFlushCallback`. If the
|
183 | condition your callback was intended to wait on is no longer important, your
|
184 | callback should call the given function synchronously. (See the
|
185 | `document.readyState` example above.)
|
186 |
|
187 | **Calling `polyfillWrapFlushCallback` disconnects the `MutationObserver` watching
|
188 | the main document.** This means that you must delay until at least
|
189 | `document.readyState !== 'loading'` to be sure that all elements in the main
|
190 | document are found (subject to exceptions mentioned in the section above).
|
191 |
|
192 | You can call `polyfillWrapFlushCallback` multiple times, each function given
|
193 | will automatically wrap and delay any previous wrappers:
|
194 |
|
195 | ```javascript
|
196 | customElements.polyfillWrapFlushCallback(function(flush) {
|
197 | console.log('added first');
|
198 | flush();
|
199 | });
|
200 |
|
201 | customElements.polyfillWrapFlushCallback(function(flush) {
|
202 | console.log('added second');
|
203 | setTimeout(() => flush(), 1000);
|
204 | });
|
205 |
|
206 | customElements.define('c-e', class extends HTMLElement {});
|
207 | // 'added second'
|
208 | // ~1s delay
|
209 | // 'added first'
|
210 | // The document is walked to attempt upgrades.
|
211 | ```
|