Skip to content
This repository has been archived by the owner on Oct 7, 2020. It is now read-only.

Latest commit

 

History

History
494 lines (370 loc) · 20.6 KB

README.md

File metadata and controls

494 lines (370 loc) · 20.6 KB

Deprecated

This polyfill has served the Web well in the last 6 years, but it's time to use the V1 only polyfill, which includes custom elements builtin extends like this one did before.

Such polyfill is in npm as @ungap/custom-elements and it does all features detections for every browser without any need to worry about anything.

<script src="//unpkg.com/@ungap/custom-elements"></script>
<script>
// alternatively, if bundlers are around
import '@ungap/custom-elements';
// or
require('@ungap/custom-elements');
</script>

However, if all you need is custom elements without builtin extends, a module also used by @ungap/custom-elements , @webreflection/custom-elements-no-builtin is your stop.

<script>
if(!self.customElements)
  document.write('<script src="//unpkg.com/@webreflection/custom-elements-no-builtin"><\x2fscript>');
</script>

Alternatively, if it's a ponyfill hat you are after, see @webreflection/custom-elements instructions, as this module is already exported as ponyfill.


build status donate Backers on Open Collective Sponsors on Open Collective

Announcement

Please use the modern version of this polyfill instead, which includes the following features:

  • no constructor caveats, everything works like in Chrome or Firefox
  • better performance, only defined builtin gets observed, thanks to qsa-observer
  • better memory handling: no leaks, and less operations
  • better ShadowDOM integration: builtin extends are observed within ShadowDOM nodes, either opened or closed

A stand-alone lightweight version of Custom Elements V1 based on top, and compatible with, the battle-tested Custom Elements V0, already used in production with projects such as Google AMP HTML ⚡ and others.

Important: How To Migrate To Native V1

Projects based on this polyfill should consider migrating to V1 API, which is natively available for Safari, Chrome, Firefox, and soon Edge too.

Where built in extends are not possible, you can use my latest built-in-element polyfill instead, which will leave natively working implementations untouched, and will apply the minimal amount of patches to native V1 without built-in elements (only Safari to date).

How To Polyfill Custom Elements V1

This is a bullet-proof way to bring in Custom Elements V1 when needed.

<script>this.customElements||document.write('<script src="//unpkg.com/document-register-element"><\x2fscript>');</script>
<script src="//unpkg.com/@ungap/custom-elements-builtin"></script>

Don't worry though, only very old browsers will pass through that document.write, preserving its 20yo tested nature, while no modern browser will ever complain.

Stick above sequence of scripts on top of any of your pages, and you'll see that only very old browsers will download this polyfill, while others will load less than 1k, delivering Custom Elements with 100% native performance.

If you are bundling instead all the code, consider decoupling DRE bundling a part, or perform the same customElements check on the window and bring in only the right polyfill.

What About Shadow DOM ?

This polyfill, as well as built-in-element one, are about Custom Elements that are a specification a part.

If you need for some reason Shadow DOM, I suggest you to look at attach-shadow "poorlyfill", which provides most basic/needed mechanism to create sandboxed components.

You can try other polyfills around the web too but, if I were you, I'll stay away from Shadow DOM where it's not natively supported because other polyfills are heavier, less compatible, and yet not 100% reliable.

Transpilers VS 1.9

The version 1.9 of this polyfill does not patch browsers with full native support for Custom Elements, as anyone would expect from a polyfill based on features detection.

However, if your transpiler transforms native ES2015 classes into something incompatible, like TypeScript does in this case, you need to update, change, or better configure your tools to support proper classes.

Babel 7 should've solved this in core, so use Babel 7 if you need transpilers.

How to avoid CE Built In

Since version 1.6 the ponyfill flag can be either a string, representing the ponyfill type such "auto" or "force", or an object, with the following shape:

installCE(global, {
  type: 'force' || 'auto' (default),
  noBuiltIn: true (default undefined / false)
});

If you set noBuiltIn to true, the V1 API will be polyfilled where needed.

No extra checks and patches will be applied to make custom elements built-in work.

New Ponyfill in 1.3

As discussed in issue #86 there is currently no way to require document-register-element polyfill without automatic feature detection and possible global context pollution.

Since there could be some very specific case when the browser should be force-patched, the pony version of the module will not attempt to feature detect anything and it will only enrich the environment once invoked.

const installCE = require('document-register-element/pony');

// by default, the second argument is 'auto'
// but it could be also 'force'
// which ignores feature detection and force
// the polyfill version of CustomElements
installCE(global, 'force');

What's new in Custom Elements v1

The ability to extend by simply defining classes:

// create a class with custom methods
// overrides, special behavior
class MyGreetings extends HTMLElement {
  show() {
    alert(this.textContent);
  }
}

// define it in the CustomElementRegistry
customElements.define('my-greetings', MyGreetings);

It is also possible to extend native components, as written in specs.

// extends some different native constructor
class MyButton extends HTMLButtonElement {}

// define it specifying what's extending
customElements.define('my-button', MyButton, {extends: 'button'});

// <button is="my-button">click me</button>
document.body.appendChild(
  new MyButton
).textContent = 'click me';

Special methods are also slightly different from v0:

  • the constructor is invoked instead of the createdCallback one
  • connectedCallback is the new attachedCallback
  • disconnectedCallback is the new detachedCallback
  • attributeChangedCallback is sensitive to the public static list of attributes to be notified about
class MyDom extends HTMLElement {
  static get observedAttributes() {
    return ['country'];
  }
  attributeChangedCallback(name, oldValue, newValue) {
    // react to changes for name
    alert(name + ':' + newValue);
  }
}
customElements.define('my-dom', MyDom);
var md = new MyDom();
md.setAttribute('test', 'nope');
md.setAttribute('country', 'UK'); // country: UK

V1 Caveat

The current standard cannot possibly be polifilled "1:1" with vanilla JavaScript because procedurally created instances need an upgrade. If the constructor is needed to setup nodes, there are two solutions:

Upgrading the constructor context

class MyElement extends HTMLElement {
  // the self argument might be provided or not
  // in both cases, the mandatory `super()` call
  // will return the right context/instance to use
  // and eventually return
  constructor(...args) {
    const self = super(...args);
    self.addEventListener('click', console.log);
    // important in case you create instances procedurally:
    // var me = new MyElement();
    return self;
  }
}

Skipping the caveat through extends

// base class to extend, same trick as before
class HTMLCustomElement extends HTMLElement {
  constructor(...$) { const _ = super(...$); _.init(); return _; }
  init() { /* override as you like */ }
}

// create any other class inheriting HTMLCustomElement
class MyElement extends HTMLCustomElement {
  init() {
    // just use `this` as regular
    this.addEventListener('click', console.log);
    // no need to return it
  }
}

Inherited V0 Caveats

Please keep in mind old gotchas with innerHTML or other caveats are still valid.

How

npm install document-register-element will put build/document-register-element.js inside node_modules/document-register-element/ of your project.

If you're working with a tool like Browserify, Webpack, RequireJS, etc, you can import the script at some point before you need to use the API.

import 'document-register-element' // ES2015
// or
require('document-register-element') // CommonJS
// or
define(['document-register-element'], function() {}) // AMD

If you're not using a module system, just place node_modules/document-register-element/build/document-register-element.js somewhere where it will be served by your server, then put

<script src="/path/to/document-register-element.js"></script>

in your head element and you should be good to go.

via CDN

Many thanks to cdnjs for hosting this script. Following an example on how to include it.

<script
  src="//cdnjs.cloudflare.com/ajax/libs/document-register-element/1.13.0/document-register-element.js"
>/* W3C Custom Elements */</script>

Tested On

The live test page is here, containing all tests as listed in the test file.

The following list of desktop browsers has been successfully tested:

  • Chrome
  • Firefox
  • IE 8 or greater (please read about IE8 caveats)
  • Safari
  • Opera

The following list of mobile OS has been successfully tested:

  • iOS 5.1 or greater
  • Android 2.2 or greater
  • FirefoxOS 1.1 or greater
  • KindleFire 3 or greater
  • Windows Phone 7 or greater
  • Opera Mobile 12 or greater
  • Blackberry OS 7* and OS 10
  • webOS 2 or LG TV
  • Samsung Bada OS 2 or greater
  • NOKIA Asha with Express Browser

The good old BB OS 7 is the only one failing the test with className which is not notified as attributeChanged when it's changed. This means BB OS 7 will also fail with id, however changing id at runtime has never been a common or useful pattern.

TL;DR does it work ?

If you see the first clock ticking, the TL;DR answer is yes.

V0 Usage Example

A basic HTML example page

<!DOCTYPE html>
<html>
<head>
  <title>testing my-element</title>
  <script src="js/document-register-element.js"></script>
  <script src="js/my-element.js"></script>
</head>
<body>
  <my-element>
    some content
  </my-element>
</body>

with the following my-element.js content

var MyElement = document.registerElement(
  'my-element',
  {
    prototype: Object.create(
      HTMLElement.prototype, {
      createdCallback: {value: function() {
        console.log('here I am ^_^ ');
        console.log('with content: ', this.textContent);
      }},
      attachedCallback: {value: function() {
        console.log('live on DOM ;-) ');
      }},
      detachedCallback: {value: function() {
        console.log('leaving the DOM :-( )');
      }},
      attributeChangedCallback: {value: function(
        name, previousValue, value
      ) {
        if (previousValue == null) {
          console.log(
            'got a new attribute ', name,
            ' with value ', value
          );
        } else if (value == null) {
          console.log(
            'somebody removed ', name,
            ' its value was ', previousValue
          );
        } else {
          console.log(
            name,
            ' changed from ', previousValue,
            ' to ', value
          );
        }
      }}
    })
  }
);

Why

I wrote a couple of blog posts about this polyfill, and here's the quick summary:

  • document-register-element.js is a stand alone polyfill which aims to support as many browsers as possible, without requiring extra dependencies at all, all in about 5KB minified and gzipped.

Add if you want the dom4 normalizer, and you'll find yourself in a modern DOM environment that works reliably with today's browsers, with an eye always open on performance.

Common Issues + Caveat

Here a list of gotchas you might encounter when developing CustomElement components.

HTML{TABLE|ROW|INPUT|SELECT|others...}Element

As described in issue 6 it's not possible to fully inherit a table, input, select, or other special element behaviors.

// This will NOT work as expected
document.registerElement(
  'my-input',
  {
    prototype: Object.create(
      HTMLInputElement.prototype
    )
  }
);

var mi = document.createElement('my-input');

The correct way to properly implement a custom input that will be also backward compatible is the following one:

// This will NOT work as expected
document.registerElement(
  'my-input',
  {
    extends: 'input', // <== IMPORTANT
    prototype: Object.create(
      HTMLInputElement.prototype
    )
  }
);

// how to create the input
var mi = document.createElement(
  'input',    // the extend
  'my-input'  // the enriched custom definition
);

Another approach is to use just a basic HTMLElement component and initialize its content at runtime.

document.registerElement(
  'my-input',
  {
    prototype: Object.create(
      HTMLElement.prototype,
      {
        createdCallback: {value: function () {
          // here the input
          this.el = this.appendChild(
            document.createElement('input')
          );
        }}
      }
    )
  }
);

var mi = document.createElement('my-input');

In this case every method that wants to interact with the input will refer this.el instead of just this.

Using innerHTML

In order to avoid huge performance impact, native behavior overwrite problems and incompatibilities, there is now a helper script, which aim is to make off-line custom elements creation possible using template strings instead of needing manual document.createElement replacements.

The helper is a simple innerHTML function that returns the given node, after setting innerHTML and, in case the polyfill is used, initialize nodes.

This helper is needed in order to be aligned with native implementations, but please remember that createdCallback could be asynchronous, even if triggered ASAP after injecting HTML through this function.

Changing the style property

If you change the style property via node.style.cssText or node.style.backgroundColor = "red" this change will most likely reflect through node.getAttribute("style").

In order to prevent footguns inside attributeChangedCallback invocations causing potential stack overflows, the style property has been filtered starting from version 0.1.1, also reflecting current native implementation where changing this special property won't invoke the callback.

(yes, even using node.setAttribute("style", "value") that you shouldn't ... just use node.style.cssText = "value" instead)

About IE8

Starting from version 0.2.0 there is an experimental support for IE8. There is a specific file that needs to be loaded in IE8 only upfront, plus a sequence of polyfills that will be simply ignored by every browser but downloaded in IE8.

Please check base.html file in order to have a basic model to reuse in case you want to support IE8.

All tests pass and there is a map component example that already works in IE8 too.

Remember there are few things to consider when IE8 is a target but since it didn't cost many bytes to have it in, I've decided to merge the logic and maintain only one file that will work in IE8 too.

IE8 caveats

  • it's IE8
  • all operations are batched and eventually executed ASAP but asynchronously. This behavior is closer to native Mutation Observers but might have some extra glitch in rendering time
  • className is right now the only special attribute that reacts. Others might be implemented in the dre-ie8-upfront-fix.js file.
  • in order to have node reacting to attributes changes, these must be live on the DOM
  • if you are using extends when create a custom element, remember to minify the production code or wrap such reserved word in quotes

Contributors

This project exists thanks to all the people who contribute. [Contribute].

Backers

Thank you to all our backers! 🙏 [Become a backer]

Sponsors

Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [Become a sponsor]