[proposal] Allow custom element registrations on per-shadow-root basis.
Reported by
trusktr@gmail.com,
Apr 15 2016
|
|||
Issue description
UserAgent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36
Steps to reproduce the problem:
1. Use React
2. Notice the powerful concept of encapsulating HTML within a component.
3. Wish there was something similar in the native web APIs
What is the expected behavior?
That it should exist.
What went wrong?
It's not implemented yet.
Did this work before? N/A
Chrome version: 49.0.2623.112 Channel: n/a
OS Version: OS X 10.10.2
Flash Version: Shockwave Flash 21.0 r0
But, we're getting closer! We currently have the ability to register Custom Elements onto the top-level document by doing
```js
document.registerElement('any-name', SomeElementClass)
// ^ this currently ignores the class' constructor, so leave the constructor empty for now.
```
This allows us to define a class that encapsulates the behavior of our Custom Element, which is awesome!!
But, there are some limitations of this when compared to using React instead of the native Custom Elements API. First, let me describe what React has that makes it powerful:
JSX in React encapsulates "HTML" (but keep in mind JSX "is just JavaScript" as JSX compiles to plain JS) on a per-component basis. This is powerful in React because the "custom elements" in React are classes that are imported and contained within the React component's JavaScriptlexical scope. For example:
```js
import React from 'react'
import AwesomeButton from 'AwesomeButton'
export default
class MyForm extends React.Component {
constructor() {
this.value = "hello form"
}
render() {
return (
<form>
<input type="text" value={this.value} />
<AwesomeButton type="submit">Submit</AwesomeButton>
</form>
)
}
componentDidMount() { ... }
componentWillUnmount() { ... }
componentWillReceiveProps() { ... }
}
```
What's important here is that AwesomeElement is lexically scoped to the component thanks to how JavaScript works. Some other file can not use AwesomeButton unless that other file also imports AwesomeButton.
This is much better than using globals!!
The problem with the current Custom Elements API: everything is a global! Custom Elements are currently registered globally for the entire web app, via `document.registerElement()`.
I'd like to propose a possible solution that I will introduce the ability for custom element authors to scope imported custom elements to their components (achieving an effect of encapsulation similar to React components): allow the registration of Custom Elements onto ShadowDOM roots. Before showing how the custom element "web component" encapsulation would work, first let's see how registering a Custom Element onto a shadow root would work:
```js
// --- 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)
// The 'img' tag creates a CustomImageElement instance:
root.innerHTML = `
<div>
<img src="${path}">
</img>
</div>
`
```
(Note, as we can see in the example, I am also indirectly proposing that we be allowed to override native elements; in this case the IMG element is overridden.)
Here's one more example using the imperative form of element creation and following the hyphen-required rule:
```js
import CustomImageElement from 'other-place'
const el = document.querySelector('.bar')
const root = el.createShadowRoot()
root.registerElement('img', CustomImageElement)
// creates a CustomImageElement instance:
const img = root.createElement('img')
root.appendChild(img)
img.src = 'path/to/image.png'
```
In both of the last two examples, a Custom Element is registered on a shadow root. The registration is only valid within the DOM of those shadow roots and the registration does not escape the shadow root, thus the shadow root encapsulates the registration. If the shadow root contains a sub-shadow-root, then the sub-shadow-root is not affected by the parent shadow root's registration. Likewise, registrations on the `document` do not propagate into shadow roots. For example:
```js
import CustomImageElement from 'somewhere'
document.registerElement('img', CustomImageElement)
// ...
// creates an instance of HTMLImageElement despite the registration on the
// document:
shadowRoot.appendChild(shadowRoot.createElement('img'))
```
(Note, I'm also implying here that the `createElement` method would need to exist on shadow roots, which makes sense if shadow roots will have their own custom element registrations.)
Now, let me show how component encapsulation would work with web components made with the paring of Custom Elements and ShadowDOM (providing encapsulation of HTML/DOM similarly to React components). In the previous React example, AwesomeButton is a component that is defined in a similar fashion to MyForm class: it imports any components that it needs and uses them within the lexical scope of the module that its definition lives in. In the Custom Element API, we don't have the luxury of the JavaScript lexical scope passing into our markup (well, at least not with some sophisticated and possibly mal-performing template-string tagging hackery).
So, let's get down to business: let's see what a Custom Element "component" would look like. Let's recreate the React-based MyForm example above, but this time using Custom Elements + ShadowDOM coupled with the idea that we can register Custom Elements onto ShadowDOM roots:
```js
import AwesomeButton from 'AwesomeButton'
export default
class MyForm extends HTMLElement {
constructor() {
this.root = this.createShadowRoot()
this.root.registerElement('awesome-button', AwesomeButton)
this.frag = document.createDocumentFragment()
this.value = 'hello form'
this.render()
}
// A naive render function that has no diffing like React. We could use
// React here for that.
render() {
this.frag.innerHTML = `
<div>
<form>
<input type="text" value="${this.value}" /> <!-- give us self-closing custom elements, pleeeease w3c -->
<awesome-button type="submit">Submit</awesome-button>
</form>
</div>
`
if (this.root.hasChildNodes())
this.root.removeChild(this.root.firstChild)
this.root.appendChild(frag)
}
connectedCallback() { ... }
disconnectedCallback() { ... }
attributeChangedCallback() { ... }
}
```
What we can see in this example is that we've effectively encapsulated the meaning of `<awesome-button>` inside of our Custom Element component. Instead of using JavaScript's lexical scoping, we've used the encapsulation of our component's shadow root by registering `awesome-button` onto it. This gives freedom to the web component developer; allowing the developer to determing what names are used for Custom Elements that are used withing the developer's own custom-element-based component.
An idea like this, whereby the registration of an element can be encapsulated within a component, will be a great way to increase modularity in the web platform.
What do you think of this idea?
,
Apr 18 2016
,
Apr 18 2016
This should be filed and discussed on https://github.com/w3c/webcomponents/. |
|||
►
Sign in to add a comment |
|||
Comment 1 by ccameron@chromium.org
, Apr 15 2016Labels: -OS-Mac OS-All