From 8b525a7ff92793f84bb9f8639a556e6c60839ad5 Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Wed, 25 Jul 2018 22:41:54 -0400 Subject: [PATCH 01/26] tmp --- build-system/tasks/update-packages.js | 2 +- src/polyfills.js | 5 +- src/polyfills/custom-element.js | 523 ++++++++++++++++++++++++++ src/service/extensions-impl.js | 4 +- 4 files changed, 528 insertions(+), 6 deletions(-) create mode 100644 src/polyfills/custom-element.js diff --git a/build-system/tasks/update-packages.js b/build-system/tasks/update-packages.js index 550a50988c15..16fbd8f57ac2 100644 --- a/build-system/tasks/update-packages.js +++ b/build-system/tasks/update-packages.js @@ -142,7 +142,7 @@ function updatePackages() { runYarnCheck(); } patchWebAnimations(); - patchRegisterElement(); + // patchRegisterElement(); } gulp.task( diff --git a/src/polyfills.js b/src/polyfills.js index 771a63654b09..454e2ab6e0a3 100644 --- a/src/polyfills.js +++ b/src/polyfills.js @@ -24,10 +24,9 @@ import {install as installObjectAssign} from './polyfills/object-assign'; import {install as installPromise} from './polyfills/promise'; // Importing the document-register-element module has the side effect // of installing the custom elements polyfill if necessary. -import {installCustomElements} from - 'document-register-element/build/document-register-element.patched'; +import {install as installCustomElements} from './polyfills/custom-element'; -installCustomElements(self, 'auto'); +installCustomElements(self, class {}); installDOMTokenListToggle(self); installMathSign(self); installObjectAssign(self); diff --git a/src/polyfills/custom-element.js b/src/polyfills/custom-element.js new file mode 100644 index 000000000000..1eb969704f4a --- /dev/null +++ b/src/polyfills/custom-element.js @@ -0,0 +1,523 @@ +/** + * Copyright 2018 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @typedef {{ + * promise: !Promise, + * resolve: function(), + * }} + */ +let DeferredDef; + +/** + * @typedef {!Function} + */ +let CustomElementConstructorDef; + +/** + * @typedef {{ + * name: string, + * ctor: !CustomElementConstructorDef, + * observedAttributes: !Array, + * connectedCallback: (function()|null), + * disconnectedCallback: (function()|null), + * }} + */ +let CustomElementDef; + +/** + * Validates the custom element's name. + * This intentionally ignores "valid" higher Unicode Code Points. + * https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name + */ +const VALID_NAME = /^[a-z][a-z0-9._]*-[a-z0-9._-]*$/; +const INVALID_NAMES = [ + 'annotation-xml', + 'color-profile', + 'font-face', + 'font-face-src', + 'font-face-uri', + 'font-face-format', + 'font-face-name', + 'missing-glyph', +]; + +/** + * Asserts that the custom element name conforms to the spec. + * @param {string} name + */ +function assertValidName(name) { + if (!VALID_NAME.test(name) || INVALID_NAMES.indexOf(name) >= 0) { + throw new SyntaxError(`invalid custom element name "${name}"`); + } +} + +/** + * Does win have a full Custom Elements registry? + * @param {!Window} win + * @return {boolean} + */ +function hasCustomElements(win) { + const {customElements} = win; + + return !!( + customElements && + customElements.define && + customElements.get && + customElements.whenDefined); +} + +class CustomElementRegistry { + /** + * @param {!Registry} registry + */ + constructor(registry) { + /** @const @private */ + this.registry_ = registry; + + /** + * @type {Object} + * @private + * @const + */ + this.whens_ = Object.create(null); + } + + /** + * @param {string} name + * @param {!CustomElementConstructorDef} ctor + * @param {Object=} options + */ + define(name, ctor, options) { + if (options) { + throw new Error('Extending native custom elements is not supported'); + } + + assertValidName(name); + + if (this.registry_.getByName(name) || + this.registry_.getByConstructor(ctor)) { + throw new Error('duplicate definition'); + } + + const proto = ctor.prototype; + const lifecycleCallbacks = { + 'connectedCallback': null, + 'disconnectedCallback': null, + // 'adoptedCallback': null, + // 'attributeChangedCallback': null, + }; + + for (const callbackName in lifecycleCallbacks) { + const callback = proto[callbackName]; + if (callback) { + lifecycleCallbacks[callbackName] = /** @type {function()} */(callback); + } + } + + const observedAttributes = (lifecycleCallbacks['attributeChangedCallback'] + && ctor['observedAttributes']) || []; + + this.registry_.add(name, ctor, lifecycleCallbacks, + observedAttributes); + + this.registry_.upgrade(null, name); + + const whens = this.whens_; + const when = whens[name]; + if (when) { + when.resolve(); + delete whens[name]; + } + } + + /** + * @param {string} name + * @return {!CustomElementConstructorDef|undefined} + */ + get(name) { + const def = this.registry_.getByName(name); + if (def) { + return def.ctor; + } + } + + /** + * @param {string} name + * @return {!Promise} + */ + whenDefined(name) { + assertValidName(name); + + if (this.registry_.getByName(name)) { + return Promise.resolve(); + } + + const whens = this.whens_; + const when = whens[name]; + if (when) { + return when.promise; + } + + let resolve; + const promise = new /*OK*/Promise(res => resolve = res); + whens[name] = { + promise, + resolve, + }; + + return promise; + } + + /** + * @param {!Node} root + */ + upgrade(root) { + this.registry_.upgrade(root); + } +} + +class Registry { + /** + * @param {!Document} doc + */ + constructor(doc) { + /** + * @private @const + */ + this.doc_ = doc; + + /** + * @type {Object} + * @private + * @const + */ + this.definitions_ = Object.create(null); + + /** + * A up-to-date DOM selector for all custom elements. + * @type {string} + */ + this.query_ = ''; + + /** + * The currently upgrading element. + * @private {Element} + */ + this.current_ = null; + + const observer = new MutationObserver(records => { + this.handleRecords_(records); + }); + observer.observe(doc, { + childList: true, + subtree: true, + }); + } + + /** + * @return {Element} + */ + current() { + const current = this.current_; + this.current_ = null; + return current; + } + + /** + * @param {string} name + * @return {CustomElementDef|undefined} + */ + getByName(name) { + const definition = this.definitions_[name]; + if (definition) { + return definition; + } + } + + /** + * @param {CustomElementConstructorDef} ctor + * @return {CustomElementDef|undefined} + */ + getByConstructor(ctor) { + const definitions = this.definitions_; + + for (const name in definitions) { + const def = definitions[name]; + if (def.ctor === ctor) { + return def; + } + } + } + + /** + * @param {string} name + * @param {!CustomElementConstructorDef} ctor + * @param {!Object} lifecycleCallbacks + * @param {!Array} observedAttributes + */ + add(name, ctor, lifecycleCallbacks, observedAttributes) { + if (this.query_) { + this.query_ += ','; + } + this.query_ += name; + + this.definitions_[name] = { + name, + ctor, + observedAttributes, + connectedCallback: lifecycleCallbacks['connectedCallback'], + disconnectedCallback: lifecycleCallbacks['disconnectedCallback'], + // lifecycleCallbacks['adoptedCallback'], + // lifecycleCallbacks['attributeChangedCallback'], + }; + } + + /** + * @param {Node} root + * @param {string=} opt_query + */ + upgrade(root, opt_query) { + const query = opt_query || this.query_; + const upgradeCandidates = this.queryAll_(root, query); + + for (let i = 0; i < upgradeCandidates.length; i++) { + this.upgradeSelf(upgradeCandidates[i]); + } + } + + /** + * @param {!Node} node + */ + upgradeSelf(node) { + const def = this.getByName(node.localName); + if (!def) { + return; + } + + this.upgradeSelf_(/** @type {!Element} */(node), def); + } + + /** + * @param {Node} root + * @param {string} query + * @return {!Array|!NodeList} + */ + queryAll_(root, query) { + if (!root) { + root = this.doc_; + } else if (!query || !root.querySelectorAll) { + // Nothing to do... + return []; + } + + return root.querySelectorAll(query); + } + + /** + * @param {!Element} node + * @param {!CustomElementDef} def + */ + upgradeSelf_(node, def) { + const {ctor} = def; + if (node instanceof ctor) { + return; + } + + this.current_ = node; + // Despite how it looks, this is not a useless construction. + // HTMLElementPolyfill (the base class of all custom elements) will return + // the current node, allowing the custom element's subclass constructor to + // run on the node. The node itself is already constructed, so the return + // value is just the node. + const el = new ctor(); + if (el !== node) { + throw new Error('Constructor illegally returned a different instance.'); + } + } + + /** + * @param {!Node} node + */ + connectedCallback_(node) { + const def = this.getByName(node.localName); + if (!def) { + return; + } + this.upgradeSelf_(/** @type {!Element} */(node), def); + if (node.connectedCallback) { + node.connectedCallback(); + } + } + /** + * @param {!Node} node + */ + disconnectedCallback_(node) { + if (node.disconnectedCallback) { + node.disconnectedCallback(); + } + } + + /** + * @param {!Array} records + */ + handleRecords_(records) { + for (let i = 0; i < records.length; i++) { + const record = records[i]; + const {addedNodes, removedNodes} = record; + for (let i = 0; i < addedNodes.length; i++) { + const node = addedNodes[i]; + const connectedCandidates = this.queryAll_(node, this.query_); + this.connectedCallback_(node); + for (let i = 0; i < connectedCandidates.length; i++) { + this.connectedCallback_(connectedCandidates[i]); + } + } + + for (let i = 0; i < removedNodes.length; i++) { + const node = removedNodes[i]; + const disconnectedCandidates = this.queryAll_(node, this.query_); + this.disconnectedCallback_(node); + for (let i = 0; i < disconnectedCandidates.length; i++) { + this.disconnectedCallback_(disconnectedCandidates[i]); + } + } + } + } +} + +/** + * Does the polyfilling. + * @param {!Window} win + */ +function polyfill(win) { + const {HTMLElement, Element, Node, Document, document} = win; + const {createElement, cloneNode, importNode} = document; + + const registry = new Registry(document); + const customElements = new CustomElementRegistry(registry); + + // Object.getOwnPropertyDescriptor(window, 'customElements') + // {get: ƒ, set: undefined, enumerable: true, configurable: true} + Object.defineProperty(win, 'customElements', { + enumerable: true, + configurable: true, + // writable: false, + value: customElements, + }); + + // Object.getOwnPropertyDescriptor(Document.prototype, 'createElement') + // {value: ƒ, writable: true, enumerable: true, configurable: true} + Document.prototype.createElement = function createElementPolyfill(name) { + const def = registry.getByName(name); + if (def) { + return new def.ctor(); + } + return createElement.apply(this, arguments); + }; + + // Object.getOwnPropertyDescriptor(Document.prototype, 'importNode') + // {value: ƒ, writable: true, enumerable: true, configurable: true} + Document.prototype.importNode = function importNodePolyfill() { + const imported = importNode.apply(this, arguments); + if (imported) { + registry.upgradeSelf(imported); + registry.upgrade(imported); + } + return imported; + }; + + // Object.getOwnPropertyDescriptor(Node.prototype, 'cloneNode') + // {value: ƒ, writable: true, enumerable: true, configurable: true} + Node.prototype.cloneNode = function cloneNodePolyfill() { + const cloned = cloneNode.apply(this, arguments); + registry.upgradeSelf(cloned); + registry.upgrade(cloned); + return cloned; + }; + + // Object.getOwnPropertyDescriptor(Element.prototype, 'innerHTML') + // {get: ƒ, set: ƒ, enumerable: true, configurable: true} + const innerHTMLDesc = Object.getOwnPropertyDescriptor(Element.prototype, + 'innerHTML'); + const innerHTMLSetter = innerHTMLDesc.set; + innerHTMLDesc.set = function(html) { + innerHTMLSetter.call(this, html); + registry.upgrade(this); + }; + Object.defineProperty(Element.prototype, 'innerHTML', innerHTMLDesc); + + /** + * You can't use the real HTMLElement constructor, because you can't subclass + * it without using native classes. So, mock its approximation using + * createElement. + */ + function HTMLElementPolyfill() { + const {constructor} = this; + + let el = registry.current(); + if (!el) { + const def = registry.getByConstructor(constructor); + el = createElement.call(document, def.name); + } + Object.setPrototypeOf(this, constructor.prototype); + return el; + } + HTMLElementPolyfill.prototype = Object.create(HTMLElement.prototype, { + constructor: { + // enumerable: false, + configurable: true, + writable: true, + value: HTMLElementPolyfill, + }, + }); + + // Object.getOwnPropertyDescriptor(window, 'HTMLElement') + // {value: ƒ, writable: true, enumerable: false, configurable: true} + win.HTMLElement = HTMLElementPolyfill; +} + +/** + * Polyfills Custom Elements v1 API + * @param {!Window} win + * @param {!Function} ctor + */ +export function install(win, ctor) { + let install = true; + if (hasCustomElements(win)) { + // If ctor is constructable without new, it's a function. That means it was + // compiled down, and we need to force the polyfill because all you cannot + // extend HTMLElement without native classes. + try { + // "Construct" ctor using ES5 idioms + const instance = Object.create(ctor.prototype); + // Call ctor without new + ctor.call(instance); + + // If this succeeds, we're in a transpiled environment + install = true; + } catch (e) { + + // The ctor threw when we constructed is via ES5, so it's a real class. + // We're ok to not install the polyfill. + install = false; + } + } + + if (install) { + polyfill(win); + } +} diff --git a/src/service/extensions-impl.js b/src/service/extensions-impl.js index f368293b018e..b3c396202893 100644 --- a/src/service/extensions-impl.js +++ b/src/service/extensions-impl.js @@ -32,8 +32,8 @@ import { import {cssText} from '../../build/css'; import {dev, rethrowAsync} from '../log'; import {getMode} from '../mode'; -import {installCustomElements} from - 'document-register-element/build/document-register-element.patched'; +// import {installCustomElements} from + // 'document-register-element/build/document-register-element.patched'; import { install as installDOMTokenListToggle, } from '../polyfills/domtokenlist-toggle'; From 94d836ac82e91e1bfdabf536296233e0f3d6af01 Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Wed, 25 Jul 2018 23:02:33 -0400 Subject: [PATCH 02/26] Fixes --- src/polyfills/custom-element.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/polyfills/custom-element.js b/src/polyfills/custom-element.js index 1eb969704f4a..139ff7378cdd 100644 --- a/src/polyfills/custom-element.js +++ b/src/polyfills/custom-element.js @@ -295,7 +295,12 @@ class Registry { const upgradeCandidates = this.queryAll_(root, query); for (let i = 0; i < upgradeCandidates.length; i++) { - this.upgradeSelf(upgradeCandidates[i]); + const candidate = upgradeCandidates[i]; + if (root) { + this.upgradeSelf(candidate); + } else { + this.connectedCallback_(candidate); + } } } @@ -473,7 +478,7 @@ function polyfill(win) { const def = registry.getByConstructor(constructor); el = createElement.call(document, def.name); } - Object.setPrototypeOf(this, constructor.prototype); + Object.setPrototypeOf(el, constructor.prototype); return el; } HTMLElementPolyfill.prototype = Object.create(HTMLElement.prototype, { From f09eebfd016a51eebd316241f1cba864d27bae92 Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Wed, 25 Jul 2018 23:57:00 -0400 Subject: [PATCH 03/26] Wrap HTMLElement if Reflect.construct is available --- src/polyfills/custom-element.js | 52 ++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/src/polyfills/custom-element.js b/src/polyfills/custom-element.js index 139ff7378cdd..a719e924e7a8 100644 --- a/src/polyfills/custom-element.js +++ b/src/polyfills/custom-element.js @@ -80,6 +80,16 @@ function hasCustomElements(win) { customElements.whenDefined); } +/** + * Was HTMLElement already patched this window? + * @param {!Window} win + * @return {boolean} + */ +function isPatched(win) { + const tag = win.HTMLElement.toString(); + return tag.indexOf('[native code]') === -1; +} + class CustomElementRegistry { /** * @param {!Registry} registry @@ -495,13 +505,45 @@ function polyfill(win) { win.HTMLElement = HTMLElementPolyfill; } +/** + * Wraps HTMLElement in a Reflect.construct constructor, so that transpiled + * classes can `_this = superClass.call(this)` during their construction. + * @param {!Window} win + */ +function wrapHTMLElement(win) { + const {HTMLElement} = win; + /** + */ + function HTMLElementPolyfill() { + return Reflect.construct(HTMLElement, [], this.constructor); + } + HTMLElementPolyfill.prototype = Object.create(HTMLElement.prototype, { + constructor: { + // enumerable: false, + configurable: true, + writable: true, + value: HTMLElementPolyfill, + }, + }); + + // Object.getOwnPropertyDescriptor(window, 'HTMLElement') + // {value: ƒ, writable: true, enumerable: false, configurable: true} + win.HTMLElement = HTMLElementPolyfill; +} + /** * Polyfills Custom Elements v1 API * @param {!Window} win * @param {!Function} ctor */ export function install(win, ctor) { + if (isPatched(win)) { + return; + } + let install = true; + let installWrapper = false; + if (hasCustomElements(win)) { // If ctor is constructable without new, it's a function. That means it was // compiled down, and we need to force the polyfill because all you cannot @@ -509,11 +551,11 @@ export function install(win, ctor) { try { // "Construct" ctor using ES5 idioms const instance = Object.create(ctor.prototype); - // Call ctor without new ctor.call(instance); - // If this succeeds, we're in a transpiled environment - install = true; + // If that succeeded, we're in a transpiled environment + // Let's find out if we can wrap HTMLElement and avoid a full patch. + installWrapper = typeof Reflect !== 'undefined' && !!Reflect.construct; } catch (e) { // The ctor threw when we constructed is via ES5, so it's a real class. @@ -522,7 +564,9 @@ export function install(win, ctor) { } } - if (install) { + if (installWrapper) { + wrapHTMLElement(win); + } else if (install) { polyfill(win); } } From c6f0c7e24bd3c5bc343e62048017b6115eb32589 Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Thu, 26 Jul 2018 20:47:36 -0400 Subject: [PATCH 04/26] Completely isolate code from globals --- src/polyfills/custom-element.js | 97 ++++++++++++++++++++------------- 1 file changed, 58 insertions(+), 39 deletions(-) diff --git a/src/polyfills/custom-element.js b/src/polyfills/custom-element.js index a719e924e7a8..a5cc66478a1e 100644 --- a/src/polyfills/custom-element.js +++ b/src/polyfills/custom-element.js @@ -57,9 +57,10 @@ const INVALID_NAMES = [ /** * Asserts that the custom element name conforms to the spec. + * @param {!Function} SyntaxError * @param {string} name */ -function assertValidName(name) { +function assertValidName(SyntaxError, name) { if (!VALID_NAME.test(name) || INVALID_NAMES.indexOf(name) >= 0) { throw new SyntaxError(`invalid custom element name "${name}"`); } @@ -92,31 +93,41 @@ function isPatched(win) { class CustomElementRegistry { /** + * @param {!Window} win * @param {!Registry} registry */ - constructor(registry) { - /** @const @private */ + constructor(win, registry) { + /** + * @const @private + */ + this.win_ = win; + + /** + * @const @private + */ this.registry_ = registry; /** - * @type {Object} + * @type {!Object} * @private * @const */ - this.whens_ = Object.create(null); + this.whens_ = this.win_.Object.create(null); } /** * @param {string} name * @param {!CustomElementConstructorDef} ctor - * @param {Object=} options + * @param {!Object=} options */ define(name, ctor, options) { + const {Error, SyntaxError} = this.win_; + if (options) { throw new Error('Extending native custom elements is not supported'); } - assertValidName(name); + assertValidName(SyntaxError, name); if (this.registry_.getByName(name) || this.registry_.getByConstructor(ctor)) { @@ -170,7 +181,8 @@ class CustomElementRegistry { * @return {!Promise} */ whenDefined(name) { - assertValidName(name); + const {Promise, SyntaxError} = this.win_; + assertValidName(SyntaxError, name); if (this.registry_.getByName(name)) { return Promise.resolve(); @@ -202,20 +214,20 @@ class CustomElementRegistry { class Registry { /** - * @param {!Document} doc + * @param {!Window} win */ - constructor(doc) { + constructor(win) { /** * @private @const */ - this.doc_ = doc; + this.win_ = win; /** - * @type {Object} + * @type {!Object} * @private * @const */ - this.definitions_ = Object.create(null); + this.definitions_ = win.Object.create(null); /** * A up-to-date DOM selector for all custom elements. @@ -229,10 +241,10 @@ class Registry { */ this.current_ = null; - const observer = new MutationObserver(records => { + const observer = new win.MutationObserver(records => { this.handleRecords_(records); }); - observer.observe(doc, { + observer.observe(win.document, { childList: true, subtree: true, }); @@ -333,7 +345,7 @@ class Registry { */ queryAll_(root, query) { if (!root) { - root = this.doc_; + root = this.win_.document; } else if (!query || !root.querySelectorAll) { // Nothing to do... return []; @@ -360,7 +372,8 @@ class Registry { // value is just the node. const el = new ctor(); if (el !== node) { - throw new Error('Constructor illegally returned a different instance.'); + throw new this.win_.Error( + 'Constructor illegally returned a different instance.'); } } @@ -419,11 +432,11 @@ class Registry { * @param {!Window} win */ function polyfill(win) { - const {HTMLElement, Element, Node, Document, document} = win; + const {HTMLElement, Element, Node, Document, Object, document} = win; const {createElement, cloneNode, importNode} = document; - const registry = new Registry(document); - const customElements = new CustomElementRegistry(registry); + const registry = new Registry(win); + const customElements = new CustomElementRegistry(win, registry); // Object.getOwnPropertyDescriptor(window, 'customElements') // {get: ƒ, set: undefined, enumerable: true, configurable: true} @@ -491,14 +504,7 @@ function polyfill(win) { Object.setPrototypeOf(el, constructor.prototype); return el; } - HTMLElementPolyfill.prototype = Object.create(HTMLElement.prototype, { - constructor: { - // enumerable: false, - configurable: true, - writable: true, - value: HTMLElementPolyfill, - }, - }); + subClass(Object, HTMLElement, HTMLElementPolyfill); // Object.getOwnPropertyDescriptor(window, 'HTMLElement') // {value: ƒ, writable: true, enumerable: false, configurable: true} @@ -511,24 +517,37 @@ function polyfill(win) { * @param {!Window} win */ function wrapHTMLElement(win) { - const {HTMLElement} = win; + const {HTMLElement, Reflect, Object} = win; /** */ - function HTMLElementPolyfill() { - return Reflect.construct(HTMLElement, [], this.constructor); + function HTMLElementWrapper() { + return Reflect.construct(HTMLElement, [], + /** @type {!HTMLElement} */(this).constructor); } - HTMLElementPolyfill.prototype = Object.create(HTMLElement.prototype, { + subClass(Object, HTMLElement, HTMLElementWrapper); + + // Object.getOwnPropertyDescriptor(window, 'HTMLElement') + // {value: ƒ, writable: true, enumerable: false, configurable: true} + win.HTMLElement = HTMLElementWrapper; +} + +/** + * Setups up prototype inheritance + * @param {!Object} Object + * @param {!Function} superClass + * @param {!Function} subClass + */ +function subClass(Object, superClass, subClass) { + // Object.getOwnPropertyDescriptor(superClass.prototype, 'constructor') + // {value: ƒ, writable: true, enumerable: false, configurable: true} + subClass.prototype = Object.create(superClass.prototype, { constructor: { // enumerable: false, configurable: true, writable: true, - value: HTMLElementPolyfill, + value: subClass, }, }); - - // Object.getOwnPropertyDescriptor(window, 'HTMLElement') - // {value: ƒ, writable: true, enumerable: false, configurable: true} - win.HTMLElement = HTMLElementPolyfill; } /** @@ -550,12 +569,12 @@ export function install(win, ctor) { // extend HTMLElement without native classes. try { // "Construct" ctor using ES5 idioms - const instance = Object.create(ctor.prototype); + const instance = win.Object.create(ctor.prototype); ctor.call(instance); // If that succeeded, we're in a transpiled environment // Let's find out if we can wrap HTMLElement and avoid a full patch. - installWrapper = typeof Reflect !== 'undefined' && !!Reflect.construct; + installWrapper = !!(win.Reflect && win.Reflect.construct); } catch (e) { // The ctor threw when we constructed is via ES5, so it's a real class. From ad2c0ebb7dab021dbbf435930f2360ae96d29b20 Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Thu, 26 Jul 2018 20:50:18 -0400 Subject: [PATCH 05/26] Install in child win --- src/polyfills.js | 4 +--- src/service/extensions-impl.js | 5 ++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/polyfills.js b/src/polyfills.js index 454e2ab6e0a3..f3a83746a5e3 100644 --- a/src/polyfills.js +++ b/src/polyfills.js @@ -15,6 +15,7 @@ */ import {install as installArrayIncludes} from './polyfills/array-includes'; +import {install as installCustomElements} from './polyfills/custom-element'; import { install as installDOMTokenListToggle, } from './polyfills/domtokenlist-toggle'; @@ -22,9 +23,6 @@ import {install as installDocContains} from './polyfills/document-contains'; import {install as installMathSign} from './polyfills/math-sign'; import {install as installObjectAssign} from './polyfills/object-assign'; import {install as installPromise} from './polyfills/promise'; -// Importing the document-register-element module has the side effect -// of installing the custom elements polyfill if necessary. -import {install as installCustomElements} from './polyfills/custom-element'; installCustomElements(self, class {}); installDOMTokenListToggle(self); diff --git a/src/service/extensions-impl.js b/src/service/extensions-impl.js index b3c396202893..7ee6b92c2e93 100644 --- a/src/service/extensions-impl.js +++ b/src/service/extensions-impl.js @@ -32,8 +32,7 @@ import { import {cssText} from '../../build/css'; import {dev, rethrowAsync} from '../log'; import {getMode} from '../mode'; -// import {installCustomElements} from - // 'document-register-element/build/document-register-element.patched'; +import {install as installCustomElements} from './polyfills/custom-element'; import { install as installDOMTokenListToggle, } from '../polyfills/domtokenlist-toggle'; @@ -665,7 +664,7 @@ export function stubLegacyElements(win) { function installPolyfillsInChildWindow(childWin) { installDocContains(childWin); installDOMTokenListToggle(childWin); - installCustomElements(childWin, 'auto'); + installCustomElements(childWin); } From 68cf92f2708d1ee23e1dba3bbb36e67dc855ccc2 Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Thu, 26 Jul 2018 20:53:36 -0400 Subject: [PATCH 06/26] Fix --- src/polyfills/custom-element.js | 5 +++-- src/service/extensions-impl.js | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/polyfills/custom-element.js b/src/polyfills/custom-element.js index a5cc66478a1e..3c86ee4c8e48 100644 --- a/src/polyfills/custom-element.js +++ b/src/polyfills/custom-element.js @@ -560,6 +560,7 @@ export function install(win, ctor) { return; } + const {Object, Reflect} = win; let install = true; let installWrapper = false; @@ -569,12 +570,12 @@ export function install(win, ctor) { // extend HTMLElement without native classes. try { // "Construct" ctor using ES5 idioms - const instance = win.Object.create(ctor.prototype); + const instance = Object.create(ctor.prototype); ctor.call(instance); // If that succeeded, we're in a transpiled environment // Let's find out if we can wrap HTMLElement and avoid a full patch. - installWrapper = !!(win.Reflect && win.Reflect.construct); + installWrapper = !!(Reflect && Reflect.construct); } catch (e) { // The ctor threw when we constructed is via ES5, so it's a real class. diff --git a/src/service/extensions-impl.js b/src/service/extensions-impl.js index 7ee6b92c2e93..8c55313a5983 100644 --- a/src/service/extensions-impl.js +++ b/src/service/extensions-impl.js @@ -664,7 +664,7 @@ export function stubLegacyElements(win) { function installPolyfillsInChildWindow(childWin) { installDocContains(childWin); installDOMTokenListToggle(childWin); - installCustomElements(childWin); + installCustomElements(childWin, class {}); } From 01d7530023091d3ebc89a4839903f7c7ca881fe0 Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Thu, 26 Jul 2018 23:28:45 -0400 Subject: [PATCH 07/26] Remove document-register-element --- build-system/dep-check-config.js | 2 ++ build-system/tasks/compile.js | 5 ++--- build-system/tasks/presubmit-checks.js | 4 ---- build-system/tasks/update-packages.js | 2 +- package.json | 1 - renovate.json | 1 - src/custom-element.js | 18 +++--------------- testing/describes.js | 5 ++--- testing/iframe.js | 5 ++--- yarn.lock | 4 ---- 10 files changed, 12 insertions(+), 35 deletions(-) diff --git a/build-system/dep-check-config.js b/build-system/dep-check-config.js index 68621aab83e1..7a82b4c0678f 100644 --- a/build-system/dep-check-config.js +++ b/build-system/dep-check-config.js @@ -291,6 +291,8 @@ exports.rules = [ 'src/polyfills.js->src/polyfills/object-assign.js', 'src/polyfills.js->src/polyfills/promise.js', 'src/polyfills.js->src/polyfills/array-includes.js', + 'src/polyfills.js->src/polyfills/custom-element.js', + 'src/service/extensions-impl.js->src/polyfills/custom-element.js', 'src/service/extensions-impl.js->src/polyfills/document-contains.js', 'src/service/extensions-impl.js->src/polyfills/domtokenlist-toggle.js', ], diff --git a/build-system/tasks/compile.js b/build-system/tasks/compile.js index cba7737b26c7..27f9e507fc7b 100644 --- a/build-system/tasks/compile.js +++ b/build-system/tasks/compile.js @@ -75,7 +75,6 @@ function cleanupBuildDir() { fs.mkdirsSync('build/cc'); rimraf.sync('build/fake-module'); rimraf.sync('build/patched-module'); - fs.mkdirsSync('build/patched-module/document-register-element/build'); fs.mkdirsSync('build/fake-module/third_party/babel'); fs.mkdirsSync('build/fake-module/src/polyfills/'); fs.mkdirsSync('build/fake-polyfills/src/polyfills'); @@ -247,8 +246,6 @@ function compile(entryModuleFilenames, outputDir, 'node_modules/promise-pjs/promise.js', 'node_modules/web-animations-js/web-animations.install.js', 'node_modules/web-activities/activity-ports.js', - 'node_modules/document-register-element/build/' + - 'document-register-element.patched.js', // 'node_modules/core-js/modules/**.js', // Not sure what these files are, but they seem to duplicate code // one level below and confuse the compiler. @@ -295,6 +292,8 @@ function compile(entryModuleFilenames, outputDir, '!build/fake-module/src/polyfills/**/*.js', '!build/fake-polyfills/src/polyfills.js', '!src/polyfills/*.js', + // TODO(prateekbh): I don't understand how to add + // src/polyfills/custom-element.js to the _needed_ polyfills. 'build/fake-polyfills/**/*.js'); polyfillsShadowList.forEach(polyfillFile => { fs.writeFileSync('build/fake-polyfills/src/polyfills/' + polyfillFile, diff --git a/build-system/tasks/presubmit-checks.js b/build-system/tasks/presubmit-checks.js index d28bae38053c..f96013a84076 100644 --- a/build-system/tasks/presubmit-checks.js +++ b/build-system/tasks/presubmit-checks.js @@ -75,10 +75,6 @@ const forbiddenTerms = { 'https://medium.com/gulpjs/gulp-util-ca3b1f9f9ac5 ' + 'for a list of alternatives.', }, - 'document-register-element.node': { - message: 'Use `document-register-element.patched` instead', - whitelist: ['build-system/tasks/update-packages.js'], - }, 'sinon\\.(spy|stub|mock)\\(': { message: 'Use a sandbox instead to avoid repeated `#restore` calls', }, diff --git a/build-system/tasks/update-packages.js b/build-system/tasks/update-packages.js index 16fbd8f57ac2..550a50988c15 100644 --- a/build-system/tasks/update-packages.js +++ b/build-system/tasks/update-packages.js @@ -142,7 +142,7 @@ function updatePackages() { runYarnCheck(); } patchWebAnimations(); - // patchRegisterElement(); + patchRegisterElement(); } gulp.task( diff --git a/package.json b/package.json index 73feed4e8106..5add27308cd4 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ }, "dependencies": { "babel-polyfill": "6.26.0", - "document-register-element": "1.5.0", "dompurify": "1.0.5", "promise-pjs": "1.1.3", "web-activities": "1.13.0", diff --git a/renovate.json b/renovate.json index c5f4cbd701a9..f160e04bdb45 100644 --- a/renovate.json +++ b/renovate.json @@ -5,7 +5,6 @@ "statusCheckVerify": true, "ignoreDeps": [ "babel-polyfill", - "document-register-element", "dompurify", "promise-pjs", "web-activities", diff --git a/src/custom-element.js b/src/custom-element.js index 45883d80ed38..62962df01df3 100644 --- a/src/custom-element.js +++ b/src/custom-element.js @@ -103,14 +103,6 @@ export function createCustomElementClass(win, name) { const baseCustomElement = createBaseCustomElementClass(win); /** @extends {HTMLElement} */ class CustomAmpElement extends baseCustomElement { - /** - * @see https://github.com/WebReflection/document-register-element#v1-caveat - * @suppress {checkTypes} - * @param {HTMLElement} self - */ - constructor(self) { - return super(self); - } /** * The name of the custom element. * @return {string} @@ -137,14 +129,10 @@ function createBaseCustomElementClass(win) { /** @abstract @extends {HTMLElement} */ class BaseCustomElement extends htmlElement { /** - * @see https://github.com/WebReflection/document-register-element#v1-caveat - * @suppress {checkTypes} - * @param {HTMLElement} self */ - constructor(self) { - self = super(self); - self.createdCallback(); - return self; + constructor() { + super(); + this.createdCallback(); } /** diff --git a/testing/describes.js b/testing/describes.js index 0a53827d0a4a..649f1e252d65 100644 --- a/testing/describes.js +++ b/testing/describes.js @@ -104,8 +104,7 @@ import { installBuiltinElements, installExtensionsService, } from '../src/service/extensions-impl'; -import {installCustomElements} from - 'document-register-element/build/document-register-element.patched'; +import {installCustomElements} from '../src/service/ampdoc-impl'; import {installDocService} from '../src/service/ampdoc-impl'; import {installFriendlyIframeEmbed} from '../src/friendly-iframe-embed'; import { @@ -587,7 +586,7 @@ class RealWinFixture { get: () => customElements, }); } else { - installCustomElements(win); + installCustomElements(win, class {}); } // Intercept event listeners diff --git a/testing/iframe.js b/testing/iframe.js index 8c5465b430f3..56e2db8893d9 100644 --- a/testing/iframe.js +++ b/testing/iframe.js @@ -25,8 +25,7 @@ import { installAmpdocServices, installRuntimeServices, } from '../src/runtime'; -import {installCustomElements} from - 'document-register-element/build/document-register-element.patched'; +import {installCustomElements} from '../src/polyfills/custom-element'; import {installDocService} from '../src/service/ampdoc-impl'; import {installExtensionsService} from '../src/service/extensions-impl'; import {installStylesLegacy} from '../src/style-installer'; @@ -232,7 +231,7 @@ export function createIframePromise(opt_runtimeOff, opt_beforeLayoutCallback) { Services.ampdocServiceFor(iframe.contentWindow).getAmpDoc(); installExtensionsService(iframe.contentWindow); installRuntimeServices(iframe.contentWindow); - installCustomElements(iframe.contentWindow); + installCustomElements(iframe.contentWindow, class {}); installAmpdocServices(ampdoc); Services.resourcesForDoc(ampdoc).ampInitComplete(); // Act like no other elements were loaded by default. diff --git a/yarn.lock b/yarn.lock index c682af84e7d5..2331bb71d457 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3721,10 +3721,6 @@ doctrine@2.1.0, doctrine@^2.1.0: dependencies: esutils "^2.0.2" -document-register-element@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/document-register-element/-/document-register-element-1.5.0.tgz#a91bc20afd9340d50cdb6a493afaf10932c12e00" - dom-serialize@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b" From 70a70ffe310f254e719856d44585931cfe71b17b Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Thu, 26 Jul 2018 23:41:24 -0400 Subject: [PATCH 08/26] Rename file --- build-system/dep-check-config.js | 4 ++-- build-system/tasks/compile.js | 2 +- build-system/tasks/presubmit-checks.js | 1 + src/polyfills.js | 2 +- src/polyfills/{custom-element.js => custom-elements.js} | 0 src/service/extensions-impl.js | 2 +- testing/iframe.js | 2 +- 7 files changed, 7 insertions(+), 6 deletions(-) rename src/polyfills/{custom-element.js => custom-elements.js} (100%) diff --git a/build-system/dep-check-config.js b/build-system/dep-check-config.js index 7a82b4c0678f..f55c6f41e0e1 100644 --- a/build-system/dep-check-config.js +++ b/build-system/dep-check-config.js @@ -291,8 +291,8 @@ exports.rules = [ 'src/polyfills.js->src/polyfills/object-assign.js', 'src/polyfills.js->src/polyfills/promise.js', 'src/polyfills.js->src/polyfills/array-includes.js', - 'src/polyfills.js->src/polyfills/custom-element.js', - 'src/service/extensions-impl.js->src/polyfills/custom-element.js', + 'src/polyfills.js->src/polyfills/custom-elements.js', + 'src/service/extensions-impl.js->src/polyfills/custom-elements.js', 'src/service/extensions-impl.js->src/polyfills/document-contains.js', 'src/service/extensions-impl.js->src/polyfills/domtokenlist-toggle.js', ], diff --git a/build-system/tasks/compile.js b/build-system/tasks/compile.js index 27f9e507fc7b..fba97644f966 100644 --- a/build-system/tasks/compile.js +++ b/build-system/tasks/compile.js @@ -293,7 +293,7 @@ function compile(entryModuleFilenames, outputDir, '!build/fake-polyfills/src/polyfills.js', '!src/polyfills/*.js', // TODO(prateekbh): I don't understand how to add - // src/polyfills/custom-element.js to the _needed_ polyfills. + // src/polyfills/custom-elements.js to the _needed_ polyfills. 'build/fake-polyfills/**/*.js'); polyfillsShadowList.forEach(polyfillFile => { fs.writeFileSync('build/fake-polyfills/src/polyfills/' + polyfillFile, diff --git a/build-system/tasks/presubmit-checks.js b/build-system/tasks/presubmit-checks.js index f96013a84076..6f9651d90d0e 100644 --- a/build-system/tasks/presubmit-checks.js +++ b/build-system/tasks/presubmit-checks.js @@ -541,6 +541,7 @@ const forbiddenTerms = { whitelist: [ 'src/log.js', // Has actual implementation of assertElement. 'dist.3p/current/integration.js', // Includes the previous. + 'src/polyfills/custom-elements.js', ], }, 'startupChunk\\(': { diff --git a/src/polyfills.js b/src/polyfills.js index f3a83746a5e3..d7c20e2e7bd6 100644 --- a/src/polyfills.js +++ b/src/polyfills.js @@ -15,7 +15,7 @@ */ import {install as installArrayIncludes} from './polyfills/array-includes'; -import {install as installCustomElements} from './polyfills/custom-element'; +import {install as installCustomElements} from './polyfills/custom-elements'; import { install as installDOMTokenListToggle, } from './polyfills/domtokenlist-toggle'; diff --git a/src/polyfills/custom-element.js b/src/polyfills/custom-elements.js similarity index 100% rename from src/polyfills/custom-element.js rename to src/polyfills/custom-elements.js diff --git a/src/service/extensions-impl.js b/src/service/extensions-impl.js index 8c55313a5983..7a42a498894f 100644 --- a/src/service/extensions-impl.js +++ b/src/service/extensions-impl.js @@ -32,7 +32,7 @@ import { import {cssText} from '../../build/css'; import {dev, rethrowAsync} from '../log'; import {getMode} from '../mode'; -import {install as installCustomElements} from './polyfills/custom-element'; +import {install as installCustomElements} from '../polyfills/custom-elements'; import { install as installDOMTokenListToggle, } from '../polyfills/domtokenlist-toggle'; diff --git a/testing/iframe.js b/testing/iframe.js index 56e2db8893d9..074d6bf4b0ed 100644 --- a/testing/iframe.js +++ b/testing/iframe.js @@ -25,7 +25,7 @@ import { installAmpdocServices, installRuntimeServices, } from '../src/runtime'; -import {installCustomElements} from '../src/polyfills/custom-element'; +import {installCustomElements} from '../src/polyfills/custom-elements'; import {installDocService} from '../src/service/ampdoc-impl'; import {installExtensionsService} from '../src/service/extensions-impl'; import {installStylesLegacy} from '../src/style-installer'; From dc51611bdbd57bfde66d86d1e37b8e1274111f58 Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Thu, 26 Jul 2018 23:56:42 -0400 Subject: [PATCH 09/26] Fix types --- src/polyfills/custom-elements.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/polyfills/custom-elements.js b/src/polyfills/custom-elements.js index 3c86ee4c8e48..338ffc2a5de4 100644 --- a/src/polyfills/custom-elements.js +++ b/src/polyfills/custom-elements.js @@ -242,7 +242,9 @@ class Registry { this.current_ = null; const observer = new win.MutationObserver(records => { - this.handleRecords_(records); + if (records) { + this.handleRecords_(records); + } }); observer.observe(win.document, { childList: true, @@ -405,6 +407,10 @@ class Registry { handleRecords_(records) { for (let i = 0; i < records.length; i++) { const record = records[i]; + if (!record) { + continue; + } + const {addedNodes, removedNodes} = record; for (let i = 0; i < addedNodes.length; i++) { const node = addedNodes[i]; @@ -521,8 +527,9 @@ function wrapHTMLElement(win) { /** */ function HTMLElementWrapper() { - return Reflect.construct(HTMLElement, [], - /** @type {!HTMLElement} */(this).constructor); + const ctor = /** @type {function(...?):?|undefined} */( + /** @type {!HTMLElement} */(this).constructor); + return Reflect.construct(HTMLElement, [], ctor); } subClass(Object, HTMLElement, HTMLElementWrapper); From f821d00f5d1a738b392293d25e33e63d7f2aed06 Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Mon, 30 Jul 2018 19:22:56 -0400 Subject: [PATCH 10/26] Experimentally use new custom elements polyfill --- build-system/tasks/compile.js | 3 +++ build-system/tasks/presubmit-checks.js | 4 ++++ package.json | 1 + renovate.json | 1 + src/custom-element.js | 18 +++++++++++++++--- src/polyfills.js | 9 ++++++++- src/service/extensions-impl.js | 14 +++++++++++--- testing/describes.js | 2 +- testing/iframe.js | 2 +- yarn.lock | 4 ++++ 10 files changed, 49 insertions(+), 9 deletions(-) diff --git a/build-system/tasks/compile.js b/build-system/tasks/compile.js index fba97644f966..3c33193d37e2 100644 --- a/build-system/tasks/compile.js +++ b/build-system/tasks/compile.js @@ -75,6 +75,7 @@ function cleanupBuildDir() { fs.mkdirsSync('build/cc'); rimraf.sync('build/fake-module'); rimraf.sync('build/patched-module'); + fs.mkdirsSync('build/patched-module/document-register-element/build'); fs.mkdirsSync('build/fake-module/third_party/babel'); fs.mkdirsSync('build/fake-module/src/polyfills/'); fs.mkdirsSync('build/fake-polyfills/src/polyfills'); @@ -246,6 +247,8 @@ function compile(entryModuleFilenames, outputDir, 'node_modules/promise-pjs/promise.js', 'node_modules/web-animations-js/web-animations.install.js', 'node_modules/web-activities/activity-ports.js', + 'node_modules/document-register-element/build/' + + 'document-register-element.patched.js', // 'node_modules/core-js/modules/**.js', // Not sure what these files are, but they seem to duplicate code // one level below and confuse the compiler. diff --git a/build-system/tasks/presubmit-checks.js b/build-system/tasks/presubmit-checks.js index 6f9651d90d0e..398f583ab72c 100644 --- a/build-system/tasks/presubmit-checks.js +++ b/build-system/tasks/presubmit-checks.js @@ -75,6 +75,10 @@ const forbiddenTerms = { 'https://medium.com/gulpjs/gulp-util-ca3b1f9f9ac5 ' + 'for a list of alternatives.', }, + 'document-register-element.node': { + message: 'Use `document-register-element.patched` instead', + whitelist: ['build-system/tasks/update-packages.js'], + }, 'sinon\\.(spy|stub|mock)\\(': { message: 'Use a sandbox instead to avoid repeated `#restore` calls', }, diff --git a/package.json b/package.json index 5add27308cd4..73feed4e8106 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ }, "dependencies": { "babel-polyfill": "6.26.0", + "document-register-element": "1.5.0", "dompurify": "1.0.5", "promise-pjs": "1.1.3", "web-activities": "1.13.0", diff --git a/renovate.json b/renovate.json index f160e04bdb45..c5f4cbd701a9 100644 --- a/renovate.json +++ b/renovate.json @@ -5,6 +5,7 @@ "statusCheckVerify": true, "ignoreDeps": [ "babel-polyfill", + "document-register-element", "dompurify", "promise-pjs", "web-activities", diff --git a/src/custom-element.js b/src/custom-element.js index 62962df01df3..45883d80ed38 100644 --- a/src/custom-element.js +++ b/src/custom-element.js @@ -103,6 +103,14 @@ export function createCustomElementClass(win, name) { const baseCustomElement = createBaseCustomElementClass(win); /** @extends {HTMLElement} */ class CustomAmpElement extends baseCustomElement { + /** + * @see https://github.com/WebReflection/document-register-element#v1-caveat + * @suppress {checkTypes} + * @param {HTMLElement} self + */ + constructor(self) { + return super(self); + } /** * The name of the custom element. * @return {string} @@ -129,10 +137,14 @@ function createBaseCustomElementClass(win) { /** @abstract @extends {HTMLElement} */ class BaseCustomElement extends htmlElement { /** + * @see https://github.com/WebReflection/document-register-element#v1-caveat + * @suppress {checkTypes} + * @param {HTMLElement} self */ - constructor() { - super(); - this.createdCallback(); + constructor(self) { + self = super(self); + self.createdCallback(); + return self; } /** diff --git a/src/polyfills.js b/src/polyfills.js index d7c20e2e7bd6..8ac614f54cab 100644 --- a/src/polyfills.js +++ b/src/polyfills.js @@ -23,8 +23,15 @@ import {install as installDocContains} from './polyfills/document-contains'; import {install as installMathSign} from './polyfills/math-sign'; import {install as installObjectAssign} from './polyfills/object-assign'; import {install as installPromise} from './polyfills/promise'; +import {isExperimentOn} from './experiments'; +import {installCustomElements as registerElement} from + 'document-register-element/build/document-register-element.patched'; -installCustomElements(self, class {}); +if (isExperimentOn(self, 'custom-elements-v1')) { + installCustomElements(self, class {}); +} else { + registerElement(self, 'auto'); +} installDOMTokenListToggle(self); installMathSign(self); installObjectAssign(self); diff --git a/src/service/extensions-impl.js b/src/service/extensions-impl.js index 7a42a498894f..afe402cf9a99 100644 --- a/src/service/extensions-impl.js +++ b/src/service/extensions-impl.js @@ -41,7 +41,10 @@ import {installImg} from '../../builtins/amp-img'; import {installLayout} from '../../builtins/amp-layout'; import {installPixel} from '../../builtins/amp-pixel'; import {installStylesForDoc, installStylesLegacy} from '../style-installer'; +import {isExperimentOn} from '../experiments'; import {map} from '../utils/object'; +import {installCustomElements as registerElement} from + 'document-register-element/build/document-register-element.patched'; import {startsWith} from '../string'; import {toWin} from '../types'; @@ -419,7 +422,7 @@ export class Extensions { setParentWindow(childWin, parentWin); // Install necessary polyfills. - installPolyfillsInChildWindow(childWin); + installPolyfillsInChildWindow(parentWin, childWin); // Install runtime styles. installStylesLegacy(childWin.document, cssText, /* callback */ null, @@ -659,12 +662,17 @@ export function stubLegacyElements(win) { /** * Install polyfills in the child window (friendly iframe). + * @param {!Window} parentWin * @param {!Window} childWin */ -function installPolyfillsInChildWindow(childWin) { +function installPolyfillsInChildWindow(parentWin, childWin) { installDocContains(childWin); installDOMTokenListToggle(childWin); - installCustomElements(childWin, class {}); + if (isExperimentOn(parentWin, 'custom-elements-v1')) { + installCustomElements(childWin, class {}); + } else { + registerElement(childWin, 'auto'); + } } diff --git a/testing/describes.js b/testing/describes.js index 649f1e252d65..969b5046a364 100644 --- a/testing/describes.js +++ b/testing/describes.js @@ -104,7 +104,7 @@ import { installBuiltinElements, installExtensionsService, } from '../src/service/extensions-impl'; -import {installCustomElements} from '../src/service/ampdoc-impl'; +import {install as installCustomElements} from '../src/service/ampdoc-impl'; import {installDocService} from '../src/service/ampdoc-impl'; import {installFriendlyIframeEmbed} from '../src/friendly-iframe-embed'; import { diff --git a/testing/iframe.js b/testing/iframe.js index 074d6bf4b0ed..4087271920ef 100644 --- a/testing/iframe.js +++ b/testing/iframe.js @@ -25,7 +25,7 @@ import { installAmpdocServices, installRuntimeServices, } from '../src/runtime'; -import {installCustomElements} from '../src/polyfills/custom-elements'; +import {install as installCustomElements} from '../src/polyfills/custom-elements'; import {installDocService} from '../src/service/ampdoc-impl'; import {installExtensionsService} from '../src/service/extensions-impl'; import {installStylesLegacy} from '../src/style-installer'; diff --git a/yarn.lock b/yarn.lock index 2331bb71d457..c682af84e7d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3721,6 +3721,10 @@ doctrine@2.1.0, doctrine@^2.1.0: dependencies: esutils "^2.0.2" +document-register-element@1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/document-register-element/-/document-register-element-1.5.0.tgz#a91bc20afd9340d50cdb6a493afaf10932c12e00" + dom-serialize@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b" From 35ebc6d8f2ec7294735f6464e99f7583bec3f328 Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Mon, 30 Jul 2018 19:26:13 -0400 Subject: [PATCH 11/26] Register custom-elements-v1 experiment --- tools/experiments/experiments.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tools/experiments/experiments.js b/tools/experiments/experiments.js index 0f6468853487..42122bab2742 100644 --- a/tools/experiments/experiments.js +++ b/tools/experiments/experiments.js @@ -327,6 +327,12 @@ const EXPERIMENTS = [ spec: 'https://github.com/ampproject/amphtml/pull/17229', cleanupIssue: 'https://github.com/ampproject/amphtml/issues/17230', }, + { + id: 'custom-elements-v1', + name: 'Enable a new custom elements v1 polyfill', + spec: 'https://github.com/ampproject/amphtml/pull/17205', + cleanupIssue: 'https://github.com/ampproject/amphtml/issues/17243', + }, ]; if (getMode().localDev) { From b5072fc095490863da4012a4a44831503312f3ed Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Tue, 31 Jul 2018 14:41:59 -0400 Subject: [PATCH 12/26] Fix comment --- src/polyfills/custom-elements.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/polyfills/custom-elements.js b/src/polyfills/custom-elements.js index 338ffc2a5de4..7e0daf440927 100644 --- a/src/polyfills/custom-elements.js +++ b/src/polyfills/custom-elements.js @@ -82,7 +82,7 @@ function hasCustomElements(win) { } /** - * Was HTMLElement already patched this window? + * Was HTMLElement already patched for this window? * @param {!Window} win * @return {boolean} */ From 16ac0930178160249ac846b11bb9766f28662ec3 Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Tue, 31 Jul 2018 14:45:12 -0400 Subject: [PATCH 13/26] Include custom elements polyfill with module build --- build-system/tasks/compile.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/build-system/tasks/compile.js b/build-system/tasks/compile.js index 3c33193d37e2..6128bd2ae852 100644 --- a/build-system/tasks/compile.js +++ b/build-system/tasks/compile.js @@ -294,11 +294,9 @@ function compile(entryModuleFilenames, outputDir, '!build/fake-module/src/polyfills.js', '!build/fake-module/src/polyfills/**/*.js', '!build/fake-polyfills/src/polyfills.js', - '!src/polyfills/*.js', - // TODO(prateekbh): I don't understand how to add - // src/polyfills/custom-elements.js to the _needed_ polyfills. 'build/fake-polyfills/**/*.js'); polyfillsShadowList.forEach(polyfillFile => { + srcs.push(`!src/polyfills/${polyfillFile}.js`); fs.writeFileSync('build/fake-polyfills/src/polyfills/' + polyfillFile, 'export function install() {}'); }); From 6b7a68722e2c12e79992ce96c136b13d8c195fe9 Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Tue, 31 Jul 2018 16:36:29 -0400 Subject: [PATCH 14/26] Fix build system --- build-system/tasks/compile.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/build-system/tasks/compile.js b/build-system/tasks/compile.js index 6128bd2ae852..44e637089689 100644 --- a/build-system/tasks/compile.js +++ b/build-system/tasks/compile.js @@ -282,21 +282,19 @@ function compile(entryModuleFilenames, outputDir, // once. Since all files automatically wait for the main binary to load // this works fine. if (options.includeOnlyESMLevelPolyfills) { - const polyfillsShadowList = [ - 'array-includes.js', - 'document-contains.js', - 'domtokenlist-toggle.js', - 'math-sign.js', - 'object-assign.js', - 'promise.js', - ]; + const polyfills = fs.readdirSync('src/polyfills'); + const polyfillsShadowList = polyfills.filter(p => { + // custom-elements polyfill must be included. + return p !== 'custom-elements.js'; + }); srcs.push( '!build/fake-module/src/polyfills.js', '!build/fake-module/src/polyfills/**/*.js', '!build/fake-polyfills/src/polyfills.js', + 'src/polyfills/custom-elements.js', 'build/fake-polyfills/**/*.js'); polyfillsShadowList.forEach(polyfillFile => { - srcs.push(`!src/polyfills/${polyfillFile}.js`); + srcs.push(`!src/polyfills/${polyfillFile}`); fs.writeFileSync('build/fake-polyfills/src/polyfills/' + polyfillFile, 'export function install() {}'); }); From ba171aa1e29c9eda82d8d3ba3b4a5814fa1dec51 Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Tue, 31 Jul 2018 17:23:45 -0400 Subject: [PATCH 15/26] Bump build size --- build-system/tasks/bundle-size.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-system/tasks/bundle-size.js b/build-system/tasks/bundle-size.js index 660915db4e58..590bab259fed 100644 --- a/build-system/tasks/bundle-size.js +++ b/build-system/tasks/bundle-size.js @@ -22,7 +22,7 @@ const log = require('fancy-log'); const {getStdout} = require('../exec'); const runtimeFile = './dist/v0.js'; -const maxSize = '79.28KB'; +const maxSize = '80.60KB'; const {green, red, cyan, yellow} = colors; From eaf7422109e5212a45018412ab28437c468c4f02 Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Tue, 31 Jul 2018 18:18:07 -0400 Subject: [PATCH 16/26] tmp --- src/polyfills/custom-elements.js | 135 ++++++++++++++++++++++++------- 1 file changed, 104 insertions(+), 31 deletions(-) diff --git a/src/polyfills/custom-elements.js b/src/polyfills/custom-elements.js index 7e0daf440927..d1e761d02121 100644 --- a/src/polyfills/custom-elements.js +++ b/src/polyfills/custom-elements.js @@ -31,7 +31,6 @@ let CustomElementConstructorDef; * @typedef {{ * name: string, * ctor: !CustomElementConstructorDef, - * observedAttributes: !Array, * connectedCallback: (function()|null), * disconnectedCallback: (function()|null), * }} @@ -116,6 +115,8 @@ class CustomElementRegistry { } /** + * Register the custom element. + * * @param {string} name * @param {!CustomElementConstructorDef} ctor * @param {!Object=} options @@ -135,11 +136,11 @@ class CustomElementRegistry { } const proto = ctor.prototype; + + // TODO(jridgewell): Support adoptedCallback and attributeChangedCallback const lifecycleCallbacks = { 'connectedCallback': null, 'disconnectedCallback': null, - // 'adoptedCallback': null, - // 'attributeChangedCallback': null, }; for (const callbackName in lifecycleCallbacks) { @@ -149,14 +150,11 @@ class CustomElementRegistry { } } - const observedAttributes = (lifecycleCallbacks['attributeChangedCallback'] - && ctor['observedAttributes']) || []; - - this.registry_.add(name, ctor, lifecycleCallbacks, - observedAttributes); - - this.registry_.upgrade(null, name); + // TODO(jridgewell): If attributeChangedCallback, gather observedAttributes + this.registry_.add(name, ctor, lifecycleCallbacks); + // If anyone is waiting for this custom element to be defined, resolve + // their promise. const whens = this.whens_; const when = whens[name]; if (when) { @@ -166,6 +164,8 @@ class CustomElementRegistry { } /** + * Get the constructor of the (already defined) custom element. + * * @param {string} name * @return {!CustomElementConstructorDef|undefined} */ @@ -177,6 +177,9 @@ class CustomElementRegistry { } /** + * Returns a promise that waits until the custom element is defined. + * If the custom element is already defined, returns a resolved promise. + * * @param {string} name * @return {!Promise} */ @@ -205,6 +208,8 @@ class CustomElementRegistry { } /** + * Upgrade all custom elements inside root. + * * @param {!Node} root */ upgrade(root) { @@ -212,6 +217,11 @@ class CustomElementRegistry { } } +/** + * This internal APIs necessary to run the CustomElementRegistry. + * Since Registry is never exposed externally, all methods are actually + * available on the instance. + */ class Registry { /** * @param {!Window} win @@ -222,6 +232,11 @@ class Registry { */ this.win_ = win; + /** + * @private @const + */ + this.doc_ = win.document; + /** * @type {!Object} * @private @@ -241,18 +256,32 @@ class Registry { */ this.current_ = null; + // Mutation Observers are conveniently available in every browser we care + // about. When a node is connected to the root document, all custom + // elements (including that node iteself) will be upgraded and call + // connectedCallback. When a node is disconnectedCallback from the root + // document, all custom elements will call disconnectedCallback. const observer = new win.MutationObserver(records => { if (records) { this.handleRecords_(records); } }); - observer.observe(win.document, { + observer.observe(this.doc_, { childList: true, subtree: true, }); } /** + * The currently-being-upgraded custom element. + * + * When an already created (through the DOM parsing APIs, or innerHTML) + * custom element node is being upgraded, we can't just create a new node + * (it's illegal in the spec). But we still need to run the custom element's + * constructor code on the node. We avoid this conundrum by running the + * constructor while returning this current node in the HTMLElement + * class constructor (the base class of all custom elements). + * * @return {Element} */ current() { @@ -262,6 +291,8 @@ class Registry { } /** + * Finds the custom element definition by name. + * * @param {string} name * @return {CustomElementDef|undefined} */ @@ -273,6 +304,8 @@ class Registry { } /** + * Finds the custom element definition by constructor instance. + * * @param {CustomElementConstructorDef} ctor * @return {CustomElementDef|undefined} */ @@ -288,35 +321,46 @@ class Registry { } /** + * Registers the custom element definition, and upgrades all elements by that + * name in the root document. + * * @param {string} name * @param {!CustomElementConstructorDef} ctor * @param {!Object} lifecycleCallbacks - * @param {!Array} observedAttributes */ - add(name, ctor, lifecycleCallbacks, observedAttributes) { - if (this.query_) { - this.query_ += ','; - } - this.query_ += name; - + add(name, ctor, lifecycleCallbacks) { + // TODO(jridgewell): Record adoptedCallback, attributeChangedCallback, and + // observedAttributes. this.definitions_[name] = { name, ctor, - observedAttributes, connectedCallback: lifecycleCallbacks['connectedCallback'], disconnectedCallback: lifecycleCallbacks['disconnectedCallback'], - // lifecycleCallbacks['adoptedCallback'], - // lifecycleCallbacks['attributeChangedCallback'], }; + + if (this.query_) { + this.query_ += ','; + } + this.query_ += name; + + this.upgrade(this.doc_, name); } /** - * @param {Node} root + * TODO + * @param {!Node} root * @param {string=} opt_query */ upgrade(root, opt_query) { + // root was undefined, opt_query defined, comes from define + // root defined, opt_query undefined, User call (don't connect) + // root defined, opt_query undefined, importNode (don't connect) + // root defined, opt_query undefined, cloneNode (don't connect) + // root defined, opt_query undefined, innerHTML (don't connect) const query = opt_query || this.query_; const upgradeCandidates = this.queryAll_(root, query); + // TODO + const newlyDefined = !!opt_query; for (let i = 0; i < upgradeCandidates.length; i++) { const candidate = upgradeCandidates[i]; @@ -329,6 +373,9 @@ class Registry { } /** + * Upgrades the custom element node, if a custom element has been registered + * by this name. + * * @param {!Node} node */ upgradeSelf(node) { @@ -341,14 +388,12 @@ class Registry { } /** - * @param {Node} root + * @param {!Node} root * @param {string} query * @return {!Array|!NodeList} */ queryAll_(root, query) { - if (!root) { - root = this.win_.document; - } else if (!query || !root.querySelectorAll) { + if (!query || !root.querySelectorAll) { // Nothing to do... return []; } @@ -357,6 +402,8 @@ class Registry { } /** + * Upgrades the (already created via DOM parsing) custom element. + * * @param {!Element} node * @param {!CustomElementDef} def */ @@ -366,13 +413,14 @@ class Registry { return; } - this.current_ = node; // Despite how it looks, this is not a useless construction. // HTMLElementPolyfill (the base class of all custom elements) will return // the current node, allowing the custom element's subclass constructor to // run on the node. The node itself is already constructed, so the return // value is just the node. + this.current_ = node; const el = new ctor(); + if (el !== node) { throw new this.win_.Error( 'Constructor illegally returned a different instance.'); @@ -380,6 +428,10 @@ class Registry { } /** + * Fires connectedCallback on the custom element, if it has one. + * This also upgrades the custom element, since it may not have been + * accessible via the root document before (a detached DOM tree). + * * @param {!Node} node */ connectedCallback_(node) { @@ -392,7 +444,10 @@ class Registry { node.connectedCallback(); } } + /** + * Fires disconnectedCallback on the custom element, if it has one. + * * @param {!Node} node */ disconnectedCallback_(node) { @@ -402,6 +457,11 @@ class Registry { } /** + * Handle all the Mutation Observer's Mutation Records. + * All added custom elements will be upgraded (if not already) and call + * connectedCallback. All removed custom elements will call + * disconnectedCallback. + * * @param {!Array} records */ handleRecords_(records) { @@ -520,6 +580,7 @@ function polyfill(win) { /** * Wraps HTMLElement in a Reflect.construct constructor, so that transpiled * classes can `_this = superClass.call(this)` during their construction. + * * @param {!Window} win */ function wrapHTMLElement(win) { @@ -540,6 +601,7 @@ function wrapHTMLElement(win) { /** * Setups up prototype inheritance + * * @param {!Object} Object * @param {!Function} superClass * @param {!Function} subClass @@ -558,7 +620,17 @@ function subClass(Object, superClass, subClass) { } /** - * Polyfills Custom Elements v1 API + * Polyfills Custom Elements v1 API. This has 4 modes: + * + * 1. Custom elements v1 already supported, using native classes + * 2. Custom elements v1 already supported, using transpiled classes + * 3. Custom elements v1 not supported, using native classes + * 4. Custom elements v1 not supported, using transpiled classes + * + * In mode 1, nothing is done. In mode 2, a minimal polyfill is used to support + * extending the HTMLElement base class. In mode 3 and 4, a full polyfill is + * done. + * * @param {!Window} win * @param {!Function} ctor */ @@ -567,15 +639,16 @@ export function install(win, ctor) { return; } - const {Object, Reflect} = win; let install = true; let installWrapper = false; if (hasCustomElements(win)) { // If ctor is constructable without new, it's a function. That means it was - // compiled down, and we need to force the polyfill because all you cannot - // extend HTMLElement without native classes. + // compiled down, and we need to do the minimal polyfill because all you + // cannot extend HTMLElement without native classes. try { + const {Object, Reflect} = win; + // "Construct" ctor using ES5 idioms const instance = Object.create(ctor.prototype); ctor.call(instance); From 873f0b000c983cc09422f1024de8eccde5e607f4 Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Tue, 31 Jul 2018 18:59:25 -0400 Subject: [PATCH 17/26] Copious comments --- src/polyfills/custom-elements.js | 84 +++++++++++++++++++++++--------- 1 file changed, 61 insertions(+), 23 deletions(-) diff --git a/src/polyfills/custom-elements.js b/src/polyfills/custom-elements.js index d1e761d02121..7dad61a84949 100644 --- a/src/polyfills/custom-elements.js +++ b/src/polyfills/custom-elements.js @@ -56,6 +56,7 @@ const INVALID_NAMES = [ /** * Asserts that the custom element name conforms to the spec. + * * @param {!Function} SyntaxError * @param {string} name */ @@ -67,6 +68,7 @@ function assertValidName(SyntaxError, name) { /** * Does win have a full Custom Elements registry? + * * @param {!Window} win * @return {boolean} */ @@ -82,6 +84,7 @@ function hasCustomElements(win) { /** * Was HTMLElement already patched for this window? + * * @param {!Window} win * @return {boolean} */ @@ -90,6 +93,9 @@ function isPatched(win) { return tag.indexOf('[native code]') === -1; } +/** + * The public Custom Elements API. + */ class CustomElementRegistry { /** * @param {!Window} win @@ -347,27 +353,28 @@ class Registry { } /** - * TODO + * Upgrades custom elements descendants of root (but not including root). + * + * When called with an opt_query, it both upgrades and connects the custom + * elements (this is used during the custom element define algorithm). + * * @param {!Node} root * @param {string=} opt_query */ upgrade(root, opt_query) { - // root was undefined, opt_query defined, comes from define - // root defined, opt_query undefined, User call (don't connect) - // root defined, opt_query undefined, importNode (don't connect) - // root defined, opt_query undefined, cloneNode (don't connect) - // root defined, opt_query undefined, innerHTML (don't connect) + // Only CustomElementRegistry.p.define provides a query (the newly defined + // custom element). In this case, we are both upgrading _and_ connecting + // the custom elements. + const newlyDefined = !!opt_query; const query = opt_query || this.query_; const upgradeCandidates = this.queryAll_(root, query); - // TODO - const newlyDefined = !!opt_query; for (let i = 0; i < upgradeCandidates.length; i++) { const candidate = upgradeCandidates[i]; - if (root) { - this.upgradeSelf(candidate); - } else { + if (newlyDefined) { this.connectedCallback_(candidate); + } else { + this.upgradeSelf(candidate); } } } @@ -504,6 +511,7 @@ function polyfill(win) { const registry = new Registry(win); const customElements = new CustomElementRegistry(win, registry); + // Expose the custom element registry. // Object.getOwnPropertyDescriptor(window, 'customElements') // {get: ƒ, set: undefined, enumerable: true, configurable: true} Object.defineProperty(win, 'customElements', { @@ -513,8 +521,9 @@ function polyfill(win) { value: customElements, }); - // Object.getOwnPropertyDescriptor(Document.prototype, 'createElement') - // {value: ƒ, writable: true, enumerable: true, configurable: true} + // Patch createElement to immediately upgrade the custom element. + // This has the added benefit that it avoids the "already created but needs + // constructor code run" chicken-and-egg problem. Document.prototype.createElement = function createElementPolyfill(name) { const def = registry.getByName(name); if (def) { @@ -523,8 +532,8 @@ function polyfill(win) { return createElement.apply(this, arguments); }; - // Object.getOwnPropertyDescriptor(Document.prototype, 'importNode') - // {value: ƒ, writable: true, enumerable: true, configurable: true} + // Patch importNode to immediately upgrade custom elements. + // TODO(jridgewell): Can fire adoptedCallback for cross doc imports. Document.prototype.importNode = function importNodePolyfill() { const imported = importNode.apply(this, arguments); if (imported) { @@ -534,8 +543,7 @@ function polyfill(win) { return imported; }; - // Object.getOwnPropertyDescriptor(Node.prototype, 'cloneNode') - // {value: ƒ, writable: true, enumerable: true, configurable: true} + // Patch cloneNode to immediately upgrade custom elements. Node.prototype.cloneNode = function cloneNodePolyfill() { const cloned = cloneNode.apply(this, arguments); registry.upgradeSelf(cloned); @@ -543,8 +551,9 @@ function polyfill(win) { return cloned; }; - // Object.getOwnPropertyDescriptor(Element.prototype, 'innerHTML') - // {get: ƒ, set: ƒ, enumerable: true, configurable: true} + // Patch the innerHTML setter to immediately upgrade custom elements. + // Note, this could technically fire connectedCallbacks if this node was + // connected, but we leave that to the Mutation Observer. const innerHTMLDesc = Object.getOwnPropertyDescriptor(Element.prototype, 'innerHTML'); const innerHTMLSetter = innerHTMLDesc.set; @@ -562,18 +571,41 @@ function polyfill(win) { function HTMLElementPolyfill() { const {constructor} = this; + // If we're upgrading an already created custom element, we can't create + // another new node (by the spec, it must be the same node). let el = registry.current(); + + // If there's not a already created custom element, we're being invoked via + // `new`ing the constructor. + // + // Technically, we could get here via createElement, but we patched that. + // If it the custom element was registered, the patch turned it into a + // `new` call. + // If it was not registered, the native createElement is used. And if + // native createElement is being used and we got to this code, we're really + // in an infinite loop (a native createElement call just below) so we've + // got bigger problems. + // + // So just take my word we got here via `new`. if (!el) { + // The custom element definition is an invariant. If the custom element + // is registered, everything works. If it's not, it throws in the member + // property access (only defined custom elements can be directly + // constructed via `new`). const def = registry.getByConstructor(constructor); el = createElement.call(document, def.name); } + + // Finally, if the node was already constructed, we need to reset it's + // prototype to the custom element prototype. And if it wasn't already + // constructed, we created a new node via native createElement, and we need + // to reset it's prototype. Basically always reset the prototype. Object.setPrototypeOf(el, constructor.prototype); return el; } subClass(Object, HTMLElement, HTMLElementPolyfill); - // Object.getOwnPropertyDescriptor(window, 'HTMLElement') - // {value: ƒ, writable: true, enumerable: false, configurable: true} + // Expose the polyfilled HTMLElement constructor for everyone to extend from. win.HTMLElement = HTMLElementPolyfill; } @@ -581,6 +613,9 @@ function polyfill(win) { * Wraps HTMLElement in a Reflect.construct constructor, so that transpiled * classes can `_this = superClass.call(this)` during their construction. * + * This is only used when Custom Elements v1 is already available _and_ we're + * using transpiled classes (which use ES5 construction idioms). + * * @param {!Window} win */ function wrapHTMLElement(win) { @@ -590,12 +625,15 @@ function wrapHTMLElement(win) { function HTMLElementWrapper() { const ctor = /** @type {function(...?):?|undefined} */( /** @type {!HTMLElement} */(this).constructor); + + // Reflect.construct allows us to construct a new HTMLElement without using + // `new` (which will always fail because native HTMLElement is a restricted + // constructor). return Reflect.construct(HTMLElement, [], ctor); } subClass(Object, HTMLElement, HTMLElementWrapper); - // Object.getOwnPropertyDescriptor(window, 'HTMLElement') - // {value: ƒ, writable: true, enumerable: false, configurable: true} + // Expose the wrapped HTMLElement constructor for everyone to extend from. win.HTMLElement = HTMLElementWrapper; } From 81da1984df6f15a2ba3a61a26bb33a89c550b448 Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Tue, 31 Jul 2018 19:04:40 -0400 Subject: [PATCH 18/26] Use CustomElementsV1 in all tests --- src/polyfills.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/polyfills.js b/src/polyfills.js index 8ac614f54cab..af3a79b906f4 100644 --- a/src/polyfills.js +++ b/src/polyfills.js @@ -14,6 +14,7 @@ * limitations under the License. */ +import {getMode} from './mode'; import {install as installArrayIncludes} from './polyfills/array-includes'; import {install as installCustomElements} from './polyfills/custom-elements'; import { @@ -27,7 +28,7 @@ import {isExperimentOn} from './experiments'; import {installCustomElements as registerElement} from 'document-register-element/build/document-register-element.patched'; -if (isExperimentOn(self, 'custom-elements-v1')) { +if (isExperimentOn(self, 'custom-elements-v1') || getMode().test) { installCustomElements(self, class {}); } else { registerElement(self, 'auto'); From e3eeafea47784a520b8fc0b925d750262663903d Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Tue, 31 Jul 2018 20:49:28 -0400 Subject: [PATCH 19/26] Defer MutationObserver start --- src/polyfills/custom-elements.js | 122 ++++++++++++++++++------------- 1 file changed, 70 insertions(+), 52 deletions(-) diff --git a/src/polyfills/custom-elements.js b/src/polyfills/custom-elements.js index 7dad61a84949..e7a58ac1ea84 100644 --- a/src/polyfills/custom-elements.js +++ b/src/polyfills/custom-elements.js @@ -128,36 +128,7 @@ class CustomElementRegistry { * @param {!Object=} options */ define(name, ctor, options) { - const {Error, SyntaxError} = this.win_; - - if (options) { - throw new Error('Extending native custom elements is not supported'); - } - - assertValidName(SyntaxError, name); - - if (this.registry_.getByName(name) || - this.registry_.getByConstructor(ctor)) { - throw new Error('duplicate definition'); - } - - const proto = ctor.prototype; - - // TODO(jridgewell): Support adoptedCallback and attributeChangedCallback - const lifecycleCallbacks = { - 'connectedCallback': null, - 'disconnectedCallback': null, - }; - - for (const callbackName in lifecycleCallbacks) { - const callback = proto[callbackName]; - if (callback) { - lifecycleCallbacks[callbackName] = /** @type {function()} */(callback); - } - } - - // TODO(jridgewell): If attributeChangedCallback, gather observedAttributes - this.registry_.add(name, ctor, lifecycleCallbacks); + this.registry_.define(name, ctor, options); // If anyone is waiting for this custom element to be defined, resolve // their promise. @@ -261,21 +232,6 @@ class Registry { * @private {Element} */ this.current_ = null; - - // Mutation Observers are conveniently available in every browser we care - // about. When a node is connected to the root document, all custom - // elements (including that node iteself) will be upgraded and call - // connectedCallback. When a node is disconnectedCallback from the root - // document, all custom elements will call disconnectedCallback. - const observer = new win.MutationObserver(records => { - if (records) { - this.handleRecords_(records); - } - }); - observer.observe(this.doc_, { - childList: true, - subtree: true, - }); } /** @@ -332,9 +288,38 @@ class Registry { * * @param {string} name * @param {!CustomElementConstructorDef} ctor - * @param {!Object} lifecycleCallbacks + * @param {!Object|undefined} options */ - add(name, ctor, lifecycleCallbacks) { + define(name, ctor, options) { + const {Error, SyntaxError} = this.win_; + + if (options) { + throw new Error('Extending native custom elements is not supported'); + } + + assertValidName(SyntaxError, name); + + if (this.registry_.getByName(name) || + this.registry_.getByConstructor(ctor)) { + throw new Error('duplicate definition'); + } + + const proto = ctor.prototype; + + // TODO(jridgewell): Support adoptedCallback and attributeChangedCallback + const lifecycleCallbacks = { + 'connectedCallback': null, + 'disconnectedCallback': null, + }; + + for (const callbackName in lifecycleCallbacks) { + const callback = proto[callbackName]; + if (callback) { + lifecycleCallbacks[callbackName] = /** @type {function()} */(callback); + } + } + + // TODO(jridgewell): If attributeChangedCallback, gather observedAttributes // TODO(jridgewell): Record adoptedCallback, attributeChangedCallback, and // observedAttributes. this.definitions_[name] = { @@ -344,11 +329,7 @@ class Registry { disconnectedCallback: lifecycleCallbacks['disconnectedCallback'], }; - if (this.query_) { - this.query_ += ','; - } - this.query_ += name; - + this.observe_(name); this.upgrade(this.doc_, name); } @@ -447,6 +428,8 @@ class Registry { return; } this.upgradeSelf_(/** @type {!Element} */(node), def); + // TODO(jridgewell): It may be appropriate to adoptCallback, if the node + // used to be in another doc. if (node.connectedCallback) { node.connectedCallback(); } @@ -463,6 +446,41 @@ class Registry { } } + /** + * Records name as a registered custom element to observe. + * + * Starts the Mutation Observer if this is the first registered custom + * element. This is deferred until the first custom element is defined to + * speed up initial rendering of the page. + * + * Mutation Observers are conveniently available in every browser we care + * about. When a node is connected to the root document, all custom + * elements (including that node iteself) will be upgraded and call + * connectedCallback. When a node is disconnectedCallback from the root + * document, all custom elements will call disconnectedCallback. + * + * @param {string} name + */ + observe_(name) { + if (this.query_) { + this.query_ += `,${name}`; + return; + } + + this.query_ = name; + + // The first registered name starts the mutation observer. + const observer = new this.win_.MutationObserver(records => { + if (records) { + this.handleRecords_(records); + } + }); + observer.observe(this.doc_, { + childList: true, + subtree: true, + }); + } + /** * Handle all the Mutation Observer's Mutation Records. * All added custom elements will be upgraded (if not already) and call From 72ab68f12a71b36bacb4f6ecea53139cefb51c7f Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Tue, 31 Jul 2018 20:51:14 -0400 Subject: [PATCH 20/26] Remove unused code Though I was collecting `connectedCallback` and `disconnectedCallback`, I wan't actually using these values. I was calling them directly off the prototye intead. While not spec compliant, it's good enough. It also saves bytes. --- src/polyfills/custom-elements.js | 23 ++--------------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/src/polyfills/custom-elements.js b/src/polyfills/custom-elements.js index e7a58ac1ea84..688fcf6b40cd 100644 --- a/src/polyfills/custom-elements.js +++ b/src/polyfills/custom-elements.js @@ -31,8 +31,6 @@ let CustomElementConstructorDef; * @typedef {{ * name: string, * ctor: !CustomElementConstructorDef, - * connectedCallback: (function()|null), - * disconnectedCallback: (function()|null), * }} */ let CustomElementDef; @@ -304,29 +302,12 @@ class Registry { throw new Error('duplicate definition'); } - const proto = ctor.prototype; - - // TODO(jridgewell): Support adoptedCallback and attributeChangedCallback - const lifecycleCallbacks = { - 'connectedCallback': null, - 'disconnectedCallback': null, - }; - - for (const callbackName in lifecycleCallbacks) { - const callback = proto[callbackName]; - if (callback) { - lifecycleCallbacks[callbackName] = /** @type {function()} */(callback); - } - } - + // TODO(jridgewell): Record connectedCallback, disconnectedCallback, + // adoptedCallback, attributeChangedCallback, and observedAttributes. // TODO(jridgewell): If attributeChangedCallback, gather observedAttributes - // TODO(jridgewell): Record adoptedCallback, attributeChangedCallback, and - // observedAttributes. this.definitions_[name] = { name, ctor, - connectedCallback: lifecycleCallbacks['connectedCallback'], - disconnectedCallback: lifecycleCallbacks['disconnectedCallback'], }; this.observe_(name); From 50660bbd7fadada69380e2b28d2ee5783881591e Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Tue, 31 Jul 2018 20:53:04 -0400 Subject: [PATCH 21/26] Add todos --- src/polyfills/custom-elements.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/polyfills/custom-elements.js b/src/polyfills/custom-elements.js index 688fcf6b40cd..59576ce09e30 100644 --- a/src/polyfills/custom-elements.js +++ b/src/polyfills/custom-elements.js @@ -297,8 +297,8 @@ class Registry { assertValidName(SyntaxError, name); - if (this.registry_.getByName(name) || - this.registry_.getByConstructor(ctor)) { + if (this.getByName(name) || + this.getByConstructor(ctor)) { throw new Error('duplicate definition'); } @@ -411,6 +411,8 @@ class Registry { this.upgradeSelf_(/** @type {!Element} */(node), def); // TODO(jridgewell): It may be appropriate to adoptCallback, if the node // used to be in another doc. + // TODO(jridgewell): I should be calling the definitions connectedCallback + // with node as the context. if (node.connectedCallback) { node.connectedCallback(); } @@ -422,6 +424,8 @@ class Registry { * @param {!Node} node */ disconnectedCallback_(node) { + // TODO(jridgewell): I should be calling the definitions connectedCallback + // with node as the context. if (node.disconnectedCallback) { node.disconnectedCallback(); } From 6a7477b78c932663bcee006b0988b284df9e5931 Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Wed, 1 Aug 2018 14:22:45 -0400 Subject: [PATCH 22/26] Fix describes --- testing/describes.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/testing/describes.js b/testing/describes.js index 969b5046a364..afb9ff907117 100644 --- a/testing/describes.js +++ b/testing/describes.js @@ -104,7 +104,9 @@ import { installBuiltinElements, installExtensionsService, } from '../src/service/extensions-impl'; -import {install as installCustomElements} from '../src/service/ampdoc-impl'; +import { + install as installCustomElements, +} from '../src/polyfills/custom-elements'; import {installDocService} from '../src/service/ampdoc-impl'; import {installFriendlyIframeEmbed} from '../src/friendly-iframe-embed'; import { From ecab91f4bc384f2cd7a53255eb3e1ac3be8a23c0 Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Wed, 1 Aug 2018 16:21:28 -0400 Subject: [PATCH 23/26] Update form-proxy inheritance to account for polyfill --- extensions/amp-form/0.1/form-proxy.js | 29 ++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/extensions/amp-form/0.1/form-proxy.js b/extensions/amp-form/0.1/form-proxy.js index be393820edda..77ca3c3475af 100644 --- a/extensions/amp-form/0.1/form-proxy.js +++ b/extensions/amp-form/0.1/form-proxy.js @@ -88,28 +88,39 @@ function createFormProxyConstr(win) { } const FormProxyProto = FormProxy.prototype; + const {Object} = win; + const ObjectProto = Object.prototype; // Hierarchy: // Node <== Element <== HTMLElement <== HTMLFormElement // EventTarget <== HTMLFormElement - const inheritance = [ + const baseClasses = [ win.HTMLFormElement, - win.HTMLElement, - win.Element, - win.Node, win.EventTarget, ]; - inheritance.forEach(function(klass) { - const prototype = klass && klass.prototype; - for (const name in prototype) { - const property = win.Object.getOwnPropertyDescriptor(prototype, name); + const inheritance = baseClasses.reduce((all, klass) => { + let proto = klass && klass.prototype; + while (proto && proto !== ObjectProto) { + if (all.indexOf(proto) >= 0) { + break; + } + all.push(proto); + proto = Object.getPrototypeOf(proto); + } + + return all; + }, []); + + inheritance.forEach(proto => { + for (const name in proto) { + const property = win.Object.getOwnPropertyDescriptor(proto, name); if (!property || // Exclude constants. name.toUpperCase() == name || // Exclude on-events. startsWith(name, 'on') || // Exclude properties that already been created. - win.Object.prototype.hasOwnProperty.call(FormProxyProto, name) || + ObjectProto.hasOwnProperty.call(FormProxyProto, name) || // Exclude some properties. Currently only used for testing. (blacklistedProperties && blacklistedProperties.includes(name))) { continue; From f46cee015bc7901531c19a484c821325759df31c Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Wed, 1 Aug 2018 16:48:47 -0400 Subject: [PATCH 24/26] Fix "dangerous use of global this" --- extensions/amp-form/0.1/form-proxy.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/extensions/amp-form/0.1/form-proxy.js b/extensions/amp-form/0.1/form-proxy.js index 77ca3c3475af..cc2508ede9fc 100644 --- a/extensions/amp-form/0.1/form-proxy.js +++ b/extensions/amp-form/0.1/form-proxy.js @@ -129,19 +129,19 @@ function createFormProxyConstr(win) { // A method call. Call the original prototype method via `call`. const method = property.value; FormProxyProto[name] = function() { - return method.apply(this.form_, arguments); + return method.apply(/** @type {!FormProxy} */(this).form_, arguments); }; } else { // A read/write property. Call the original prototype getter/setter. const spec = {}; if (property.get) { spec.get = function() { - return property.get.call(this.form_); + return property.get.call(/** @type {!FormProxy} */(this).form_); }; } if (property.set) { - spec.set = function(value) { - return property.set.call(this.form_, value); + spec.set = function(v) { + return property.set.call(/** @type {!FormProxy} */(this).form_, v); }; } win.Object.defineProperty(FormProxyProto, name, spec); From 036cf0dc262a24290665edfbdf5de3010654f5a7 Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Wed, 1 Aug 2018 18:21:17 -0400 Subject: [PATCH 25/26] Review comments --- src/polyfills.js | 6 +++--- src/polyfills/custom-elements.js | 24 ++++++++++++------------ src/service/extensions-impl.js | 6 +++--- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/polyfills.js b/src/polyfills.js index af3a79b906f4..7b574c8b929e 100644 --- a/src/polyfills.js +++ b/src/polyfills.js @@ -24,14 +24,14 @@ import {install as installDocContains} from './polyfills/document-contains'; import {install as installMathSign} from './polyfills/math-sign'; import {install as installObjectAssign} from './polyfills/object-assign'; import {install as installPromise} from './polyfills/promise'; -import {isExperimentOn} from './experiments'; -import {installCustomElements as registerElement} from +import {installCustomElements as installRegisterElement} from 'document-register-element/build/document-register-element.patched'; +import {isExperimentOn} from './experiments'; if (isExperimentOn(self, 'custom-elements-v1') || getMode().test) { installCustomElements(self, class {}); } else { - registerElement(self, 'auto'); + installRegisterElement(self, 'auto'); } installDOMTokenListToggle(self); installMathSign(self); diff --git a/src/polyfills/custom-elements.js b/src/polyfills/custom-elements.js index 59576ce09e30..52b7769587e6 100644 --- a/src/polyfills/custom-elements.js +++ b/src/polyfills/custom-elements.js @@ -115,7 +115,7 @@ class CustomElementRegistry { * @private * @const */ - this.whens_ = this.win_.Object.create(null); + this.pendingDefines_ = this.win_.Object.create(null); } /** @@ -130,11 +130,11 @@ class CustomElementRegistry { // If anyone is waiting for this custom element to be defined, resolve // their promise. - const whens = this.whens_; - const when = whens[name]; - if (when) { - when.resolve(); - delete whens[name]; + const pending = this.pendingDefines_; + const deferred = pending[name]; + if (deferred) { + deferred.resolve(); + delete pending[name]; } } @@ -166,15 +166,15 @@ class CustomElementRegistry { return Promise.resolve(); } - const whens = this.whens_; - const when = whens[name]; - if (when) { - return when.promise; + const pending = this.pendingDefines_; + const deferred = pending[name]; + if (deferred) { + return deferred.promise; } let resolve; const promise = new /*OK*/Promise(res => resolve = res); - whens[name] = { + pending[name] = { promise, resolve, }; @@ -299,7 +299,7 @@ class Registry { if (this.getByName(name) || this.getByConstructor(ctor)) { - throw new Error('duplicate definition'); + throw new Error(`duplicate definition "${name}"`); } // TODO(jridgewell): Record connectedCallback, disconnectedCallback, diff --git a/src/service/extensions-impl.js b/src/service/extensions-impl.js index afe402cf9a99..cd7ba627309a 100644 --- a/src/service/extensions-impl.js +++ b/src/service/extensions-impl.js @@ -40,11 +40,11 @@ import {install as installDocContains} from '../polyfills/document-contains'; import {installImg} from '../../builtins/amp-img'; import {installLayout} from '../../builtins/amp-layout'; import {installPixel} from '../../builtins/amp-pixel'; +import {installCustomElements as installRegisterElement} from + 'document-register-element/build/document-register-element.patched'; import {installStylesForDoc, installStylesLegacy} from '../style-installer'; import {isExperimentOn} from '../experiments'; import {map} from '../utils/object'; -import {installCustomElements as registerElement} from - 'document-register-element/build/document-register-element.patched'; import {startsWith} from '../string'; import {toWin} from '../types'; @@ -671,7 +671,7 @@ function installPolyfillsInChildWindow(parentWin, childWin) { if (isExperimentOn(parentWin, 'custom-elements-v1')) { installCustomElements(childWin, class {}); } else { - registerElement(childWin, 'auto'); + installRegisterElement(childWin, 'auto'); } } From 4d47bff50e7c8ac9dbba9a7695494cb651e152f4 Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Thu, 2 Aug 2018 01:28:01 -0400 Subject: [PATCH 26/26] Use V1 in tests --- src/custom-element.js | 8 ++-- test/functional/test-base-element.js | 8 ++-- test/functional/test-custom-element.js | 37 +++++++++---------- test/functional/test-dom.js | 8 ++-- test/functional/test-intersection-observer.js | 8 ++-- testing/describes.js | 4 +- 6 files changed, 33 insertions(+), 40 deletions(-) diff --git a/src/custom-element.js b/src/custom-element.js index 45883d80ed38..1dc1f47d0507 100644 --- a/src/custom-element.js +++ b/src/custom-element.js @@ -1756,11 +1756,11 @@ function isInternalOrServiceNode(node) { * @param {function(new:./base-element.BaseElement, !Element)=} opt_implementationClass For testing only. * @return {!Object} Prototype of element. */ -export function createAmpElementProtoForTesting( +export function createAmpElementForTesting( win, name, opt_implementationClass) { - const ElementProto = createCustomElementClass(win, name).prototype; + const Element = createCustomElementClass(win, name); if (getMode().test && opt_implementationClass) { - ElementProto.implementationClassForTesting = opt_implementationClass; + Element.prototype.implementationClassForTesting = opt_implementationClass; } - return ElementProto; + return Element; } diff --git a/test/functional/test-base-element.js b/test/functional/test-base-element.js index d25ab8b84de7..36712e318e55 100644 --- a/test/functional/test-base-element.js +++ b/test/functional/test-base-element.js @@ -18,7 +18,7 @@ import {BaseElement} from '../../src/base-element'; import {LayoutPriority} from '../../src/layout'; import {Resource} from '../../src/service/resource'; import {Services} from '../../src/services'; -import {createAmpElementProtoForTesting} from '../../src/custom-element'; +import {createAmpElementForTesting} from '../../src/custom-element'; import {layoutRectLtwh} from '../../src/layout-rect'; import {listenOncePromise} from '../../src/event-helper'; import {toggleExperiment} from '../../src/experiments'; @@ -32,10 +32,8 @@ describes.realWin('BaseElement', {amp: true}, env => { beforeEach(() => { win = env.win; doc = win.document; - doc.registerElement('amp-test-element', { - prototype: createAmpElementProtoForTesting(win, - 'amp-test-element', BaseElement), - }); + win.customElements.define('amp-test-element', + createAmpElementForTesting(win, 'amp-test-element', BaseElement)); customElement = doc.createElement('amp-test-element'); element = new BaseElement(customElement); }); diff --git a/test/functional/test-custom-element.js b/test/functional/test-custom-element.js index cdc1dee384f0..240448379d84 100644 --- a/test/functional/test-custom-element.js +++ b/test/functional/test-custom-element.js @@ -21,7 +21,7 @@ import {ElementStub} from '../../src/element-stub'; import {LOADING_ELEMENTS_, Layout} from '../../src/layout'; import {ResourceState} from '../../src/service/resource'; import {Services} from '../../src/services'; -import {createAmpElementProtoForTesting} from '../../src/custom-element'; +import {createAmpElementForTesting} from '../../src/custom-element'; import {poll} from '../../testing/iframe'; @@ -113,14 +113,13 @@ describes.realWin('CustomElement', {amp: true}, env => { container = doc.createElement('div'); doc.body.appendChild(container); - ElementClass = doc.registerElement('amp-test', { - prototype: createAmpElementProtoForTesting( - win, 'amp-test', TestElement), - }); - StubElementClass = doc.registerElement('amp-stub', { - prototype: createAmpElementProtoForTesting( - win, 'amp-stub', ElementStub), - }); + ElementClass = createAmpElementForTesting(win, 'amp-test', TestElement); + StubElementClass = createAmpElementForTesting(win, 'amp-stub', + ElementStub); + + win.customElements.define('amp-test', ElementClass); + win.customElements.define('amp-stub', StubElementClass); + win.ampExtendedElements['amp-test'] = TestElement; win.ampExtendedElements['amp-stub'] = ElementStub; ampdoc.declareExtension('amp-stub'); @@ -1469,9 +1468,9 @@ describes.realWin('CustomElement Service Elements', {amp: true}, env => { beforeEach(() => { win = env.win; doc = win.document; - StubElementClass = doc.registerElement('amp-stub2', { - prototype: createAmpElementProtoForTesting(win, 'amp-stub2', ElementStub), - }); + StubElementClass = createAmpElementForTesting(win, 'amp-stub2', + ElementStub); + win.customElements.define('amp-stub2', StubElementClass); env.ampdoc.declareExtension('amp-stub2'); element = new StubElementClass(); }); @@ -1647,10 +1646,9 @@ describes.realWin('CustomElement', {amp: true}, env => { win = env.win; doc = win.document; clock = lolex.install({target: win}); - ElementClass = doc.registerElement('amp-test-loader', { - prototype: createAmpElementProtoForTesting( - win, 'amp-test-loader', TestElement), - }); + ElementClass = createAmpElementForTesting(win, 'amp-test-loader', + TestElement); + win.customElements.define('amp-test-loader', ElementClass); win.ampExtendedElements['amp-test-loader'] = TestElement; LOADING_ELEMENTS_['amp-test-loader'.toUpperCase()] = true; resources = Services.resourcesForDoc(doc); @@ -1986,10 +1984,9 @@ describes.realWin('CustomElement Overflow Element', {amp: true}, env => { beforeEach(() => { win = env.win; doc = win.document; - ElementClass = doc.registerElement('amp-test-overflow', { - prototype: createAmpElementProtoForTesting( - win, 'amp-test-overflow', TestElement), - }); + ElementClass = createAmpElementForTesting(win, 'amp-test-overflow', + TestElement); + win.customElements.define('amp-test-overflow', ElementClass); resources = Services.resourcesForDoc(doc); resourcesMock = sandbox.mock(resources); element = new ElementClass(); diff --git a/test/functional/test-dom.js b/test/functional/test-dom.js index 4367601acddd..e4cdeea7eecf 100644 --- a/test/functional/test-dom.js +++ b/test/functional/test-dom.js @@ -16,7 +16,7 @@ import * as dom from '../../src/dom'; import {BaseElement} from '../../src/base-element'; -import {createAmpElementProtoForTesting} from '../../src/custom-element'; +import {createAmpElementForTesting} from '../../src/custom-element'; import {loadPromise} from '../../src/event-helper'; import {toArray} from '../../src/types'; @@ -1041,10 +1041,8 @@ describes.realWin('DOM', { const element = doc.createElement('amp-test'); doc.body.appendChild(element); env.win.setTimeout(() => { - doc.registerElement('amp-test', { - prototype: createAmpElementProtoForTesting( - env.win, 'amp-test', TestElement), - }); + env.win.customElements.define('amp-test', createAmpElementForTesting( + env.win, 'amp-test', TestElement)); }, 100); return dom.whenUpgradedToCustomElement(element).then(element => { expect(element.whenBuilt).to.exist; diff --git a/test/functional/test-intersection-observer.js b/test/functional/test-intersection-observer.js index af9c689b6879..75999da61c44 100644 --- a/test/functional/test-intersection-observer.js +++ b/test/functional/test-intersection-observer.js @@ -20,7 +20,7 @@ import { IntersectionObserver, getIntersectionChangeEntry, } from '../../src/intersection-observer'; -import {createAmpElementProtoForTesting} from '../../src/custom-element'; +import {createAmpElementForTesting} from '../../src/custom-element'; import {layoutRectLtwh} from '../../src/layout-rect'; @@ -324,9 +324,9 @@ describe('IntersectionObserver', () => { } } - const ElementClass = document.registerElement('amp-int', { - prototype: createAmpElementProtoForTesting(window, 'amp-int', TestElement), - }); + const ElementClass = createAmpElementForTesting(window, 'amp-int', + TestElement); + customElements.define('amp-int', ElementClass); const iframeSrc = 'http://iframe.localhost:' + location.port + '/test/fixtures/served/iframe-intersection.html'; diff --git a/testing/describes.js b/testing/describes.js index afb9ff907117..5598021c2aeb 100644 --- a/testing/describes.js +++ b/testing/describes.js @@ -96,7 +96,7 @@ import { installAmpdocServices, installRuntimeServices, } from '../src/runtime'; -import {createAmpElementProtoForTesting} from '../src/custom-element'; +import {createAmpElementForTesting} from '../src/custom-element'; import {createElementWithAttributes} from '../src/dom'; import {cssText} from '../build/css'; import {doNotLoadExternalResourcesInTest} from './iframe'; @@ -862,7 +862,7 @@ function installAmpAdStylesPromise(win) { function createAmpElement(win, opt_name, opt_implementationClass) { // Create prototype and constructor. const name = opt_name || 'amp-element'; - const proto = createAmpElementProtoForTesting(win, name); + const proto = createAmpElementForTesting(win, name).prototype; const ctor = function() { const el = win.document.createElement(name); el.__proto__ = proto;