UNPKG

@webcomponents/custom-elements

Version:
165 lines (147 loc) 7.09 kB
/** * @license * Copyright (c) 2016 The Polymer Project Authors. All rights reserved. * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt * Code distributed by Google as part of the polymer project is also * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt */ /** * This shim allows elements written in, or compiled to, ES5 to work on native * implementations of Custom Elements. * * ES5-style classes don't work with native Custom Elements because the * HTMLElement constructor uses the value of `new.target` to look up the custom * element definition for the currently called constructor. `new.target` is only * set when `new` is called and is only propagated via super() calls. super() * is not emulatable in ES5. The pattern of `SuperClass.call(this)`` only works * when extending other ES5-style classes, and does not propagate `new.target`. * * This shim allows the native HTMLElement constructor to work by generating and * registering a stand-in class instead of the users custom element class. This * stand-in class's constructor has an actual call to super(). * `customElements.define()` and `customElements.get()` are both overridden to * hide this stand-in class from users. * * In order to create instance of the user-defined class, rather than the stand * in, the stand-in's constructor swizzles its instances prototype and invokes * the user-defined constructor. When the user-defined constructor is called * directly it creates an instance of the stand-in class to get a real extension * of HTMLElement and returns that. * * There are two important constructors: A patched HTMLElement constructor, and * the StandInElement constructor. They both will be called to create an element * but which is called first depends on whether the browser creates the element * or the user-defined constructor is called directly. The variables * `browserConstruction` and `userConstruction` control the flow between the * two constructors. * * This shim should be better than forcing the polyfill because: * 1. It's smaller * 2. All reaction timings are the same as native (mostly synchronous) * 3. All reaction triggering DOM operations are automatically supported * * There are some restrictions and requirements on ES5 constructors: * 1. All constructors in a inheritance hierarchy must be ES5-style, so that * they can be called with Function.call(). This effectively means that the * whole application must be compiled to ES5. * 2. Constructors must return the value of the emulated super() call. Like * `return SuperClass.call(this)` * 3. The `this` reference should not be used before the emulated super() call * just like `this` is illegal to use before super() in ES6. * 4. Constructors should not create other custom elements before the emulated * super() call. This is the same restriction as with native custom * elements. * * Compiling valid class-based custom elements to ES5 will satisfy these * requirements with the latest version of popular transpilers. */ (() => { 'use strict'; // Do nothing if `customElements` does not exist. if (!window.customElements) return; const NativeHTMLElement = window.HTMLElement; const nativeDefine = window.customElements.define; const nativeGet = window.customElements.get; /** * Map of user-provided constructors to tag names. * * @type {Map<Function, string>} */ const tagnameByConstructor = new Map(); /** * Map of tag names to user-provided constructors. * * @type {Map<string, Function>} */ const constructorByTagname = new Map(); /** * Whether the constructors are being called by a browser process, ie parsing * or createElement. */ let browserConstruction = false; /** * Whether the constructors are being called by a user-space process, ie * calling an element constructor. */ let userConstruction = false; window.HTMLElement = function() { if (!browserConstruction) { const tagname = tagnameByConstructor.get(this.constructor); const fakeClass = nativeGet.call(window.customElements, tagname); // Make sure that the fake constructor doesn't call back to this constructor userConstruction = true; const instance = new (fakeClass)(); return instance; } // Else do nothing. This will be reached by ES5-style classes doing // HTMLElement.call() during initialization browserConstruction = false; }; // By setting the patched HTMLElement's prototype property to the native // HTMLElement's prototype we make sure that: // document.createElement('a') instanceof HTMLElement // works because instanceof uses HTMLElement.prototype, which is on the // ptototype chain of built-in elements. window.HTMLElement.prototype = NativeHTMLElement.prototype; const define = (tagname, elementClass) => { const elementProto = elementClass.prototype; const StandInElement = class extends NativeHTMLElement { constructor() { // Call the native HTMLElement constructor, this gives us the // under-construction instance as `this`: super(); // The prototype will be wrong up because the browser used our fake // class, so fix it: Object.setPrototypeOf(this, elementProto); if (!userConstruction) { // Make sure that user-defined constructor bottom's out to a do-nothing // HTMLElement() call browserConstruction = true; // Call the user-defined constructor on our instance: elementClass.call(this); } userConstruction = false; } }; const standInProto = StandInElement.prototype; StandInElement.observedAttributes = elementClass.observedAttributes; standInProto.connectedCallback = elementProto.connectedCallback; standInProto.disconnectedCallback = elementProto.disconnectedCallback; standInProto.attributeChangedCallback = elementProto.attributeChangedCallback; standInProto.adoptedCallback = elementProto.adoptedCallback; tagnameByConstructor.set(elementClass, tagname); constructorByTagname.set(tagname, elementClass); nativeDefine.call(window.customElements, tagname, StandInElement); }; const get = (tagname) => constructorByTagname.get(tagname); // Workaround for Safari bug where patching customElements can be lost, likely // due to native wrapper garbage collection issue Object.defineProperty(window, 'customElements', {value: window.customElements, configurable: true, writable: true}); Object.defineProperty(window.customElements, 'define', {value: define, configurable: true, writable: true}); Object.defineProperty(window.customElements, 'get', {value: get, configurable: true, writable: true}); })();