I mentioned in a public-webapps thread how Custom Elements coupled with ShadowDOM, along with a new ability to register Custom Elements on a per-shadow-root basis would be a good way to have Custom-Element-based “web components” that can encapsulate HTML/DOM similar to how React components encapsulate “HTML” (JSX):
Note, this opening description of the proposal is deprecated in favor of the new description in the comment below.
Previous ideas have talked about being able to register elements on a per-document basis, but that doesn’t seem to help things be more modular and componentized. Shadow roots can be the form of encapsulation for Custom Elements in a similar manner as how JSX/render() encapsulates “HTML” (“DOM”) for a React component.
Now, if we could register elements on a per-shadow-root basis, then we’d effectively have encapsulated HTML.
From the email thread, here’s what a small component might look like:
// --------------- HandyForm.js
import AwesomeButton from './AwesomeButton'
import { FancyForm, FancyInput } from './FancyForm'
export default
class HandyForm extends HTMLElement {
constructor() {
this.root = this.createShadowRoot()
this.root.registerElement('cool-button', AwesomeButton)
this.root.registerElement('fancy-form', FancyForm)
this.root.registerElement('fancy-input', FancyInput)
const frag = document.createDocumentFragment()
frag.innerHTML = `
<div>
<fancy-form>
<fancy-input type="text" /> <!-- give us self-closing custom elements, pleeeease w3c -->
<cool-button type="submit"></cool-button>
</fancy-form>
</div>
`
this.root.appendChild(frag)
}
static get observedAttributes() { return [ ... ] }
connectedCallback() { ... }
disconnectedCallback() { ... }
attributeChangedCallback() { ... }
}
// --------------- app.js
import HandyForm from './HandyForm'
// elements registered on the document won't cross into shadow roots
document.registerElement('handy-form', HandyForm)
document.body.appendChild(document.createElement('handy-form'))
As you can see here, the pattern would be that a component author would utilize Custom Elements and ShadowDOM together, along with a new ability to register elements for that specific shadow root (and only for that shadow root, it won’t leak to the outer DOM scope). This effectively allows the component author to define which HTML elements map to which classes inside the component (similar to how JSX elements map to React classes inside a React component).
As part of general recommendations, sites like MDN, MSDN, developer.chrome.com, etc, would recommend that authors of Custom Elements supply end users only the class for the custom element, and that end users register the element using any name they desire. As you see in the example, the end user of the AwesomeButton
element chose to register the AwesomeButton class into his/her component’s shadow root as “cool-button”:
this.root.registerElement('cool-button', AwesomeButton)
Then, the AwesomeButton
end user, who is the author of the HandyForm
element simply exports the HandyForm
class and does not register it onto the document
, leaving it up to the end user of HandyForm
to decide how to do that and with what name. The pattern is part of the import process. For normal JavaScript, an import statement is usually enough, but for HTML elements, the pattern would be
import SomeElement from 'some-element'
document.registerElement('any-name', SomeElement)
// ...
shadowRoot.registerElement('any-name', SomeElement)
For this all to work nicely, an important rule would be that elements registered at the top level with document.registerElement
should not propagate into shadow roots. A new shadow root should start as a clean slate who’s DOM uses only the native elements until Custom Elements are explicitly registered. For exampe:
// --- file1.js
import CustomImageElement from 'somewhere'
document.registerElement('img', CustomImageElement)
// creates a CustomImageElement instance:
const img = document.createElement('img')
document.body.appendChild(img)
img.src = 'path/to/image.png'
// --- file2.js
const el = document.querySelector('.foo')
const root = el.createShadowRoot()
// creates an HTMLImageElement instance, not affected by
// the previous call to document.registerElement:
const img = root.createElement('img')
root.appendChild(img)
img.src = 'path/to/image.png'
// --- file3.js
import OtherImageClass from 'other-place'
const el = document.querySelector('.bar')
const root = el.createShadowRoot()
root.registerElement('img', OtherImageClass)
// creates an OtherImageClass instance:
const img = root.createElement('img')
root.appendChild(img)
img.src = 'path/to/image.png'
// --- file4.js
import CustomImageElement from 'somewhere'
const path = 'path/to/image.png'
const el = document.querySelector('.bar')
const root = el.createShadowRoot()
root.registerElement('img', CustomImageElement)
// creates a CustomImageElement instance:
root.innerHTML = `
<div>
<img src="${path}">
</img>
</div>
`
What do you think of this idea?