[w3c/webcomponents] [idea] Allowing custom element registrations on per-shadow-root basis. (#488)

I say on a "per-shadow-root basis", but, in reality, just on any type of encapsulated basis where the an element definition definition doesn't affect code outside of the encapsulation. It seems like Shadow Roots are designed to be the encapsulating unit of the DOM (powerful when paired with encapsulation of JavaScript logic in Custom Elements), so I'm currently using Shadow Roots as the basis of encapsulation on which I'm making this proposal (although, if there are any other possible forms of encapsulation, I'm open to ideas!).

So, here's my proposal:

## background

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 (JavaScript) 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" on a per-component basis* (but keep in mind JSX "is just JavaScript" as JSX compiles to plain JS). This is powerful in React because the "custom elements" in React ("React components") are just classes that are imported and contained *within the React component's JavaScript lexical 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` can be used in the "HTML" (JSX) of the component due to the fact that the `AwesomeElement` is in scope. Some other file can not use `AwesomeButton` unless that other file also imports `AwesomeButton`.

This is much better than using globals!!

Now, let me compare to the current Custom Elements API. The problem with the current Custom Elements API is that all registered custom elements are globals, registered globally for the entire web app via `document.registerElement()`! Of course, the scope we're talking about with Custom Elements is the HTML document scope, not a JavaScript lexical scope like with React components.

## solution

I'd like to propose a possible solution that will introduce the ability for Custom Element authors to scope (encapsulate) custom elements within their components (achieving an effect of encapsulation similar to React components, but using a ShadowDOM scope rather than a JavaScript lexical scope): we can allow the registration of Custom Elements onto ShadowDOM roots.

### custom elements on shadow roots

Before showing how this (Custom Element) component encapsulation would work, first let's see how registering a Custom Element into a ShadowDOM root would work:

```js
import CustomImageElement from 'somewhere'

const path = 'path/to/image.png'
const el = document.querySelector('.bar')
const root = el.createShadowRoot()
root.registerElement('img', CustomImageElement) // assume here for sake of argument we can override native elements.

// The following 'img' tag will cause a `CustomImageElement` instance to be created:
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. I'll make a separate proposal for that.)

Here's one more example using the imperative form of element creation and obeying the hyphen-required-in-custom-element-name rule:

```js
import CustomImageElement from 'other-place'

const el = document.querySelector('.bar')
const root = el.createShadowRoot()
root.registerElement('my-img', CustomImageElement)

// creates a CustomImageElement instance:
const img = root.createElement('my-img')

root.appendChild(img)
img.src = 'path/to/image.png'
```

In both of the last two examples, a Custom Element is imported (thanks to ES6 Modules) then registered onto a shadow root. The registered element can only be used within the DOM of the shadow root it is registered with, the registration does not escape the shadow root (i.e. 'my-img' tags will not instantiate new CustomImageElements outside of the shadow root), and 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 either. 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, because the custom element was not registered on the shadow root:
shadowRoot.appendChild(shadowRoot.createElement('img'))
```

(Note, I'm also implying that the `createElement` method would need to exist on shadow roots, which makes sense if shadow roots will have their own custom element registrations.)

### web component encapsulation

Now, let me show how component encapsulation (similar to React) would work with Web Components made using the awesome pairing of Custom Elements and ShadowDOM APIs. In the above React example, `AwesomeButton` is a component that is defined in a similar fashion to the `MyForm` class: it imports any components that it needs and uses them within the lexical scope of it's ES6 module. In the Custom Element API, we don't have the luxury of the JavaScript lexical scope within our markup (at least not without some way to specify a map of symbols to object that exist in the lexical scope, which ends up being what the `registerElement` method is for).

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 registration of `<awesome-button>` inside of our Custom Element component. Instead of relying on JavaScript's lexical scoping, we've used our component's ShadowDOM root by registering `awesome-button` onto it.

This would give freedom to web component developers: it would allow developers to specify what names are used for Custom Elements within their own custom-element-based components.

An idea like this, whereby the registration of an element can be encapsulated within a component (i.e. the list of allowed HTML elements 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?

---
You are receiving this because you are subscribed to this thread.
Reply to this email directly or view it on GitHub:
https://github.com/w3c/webcomponents/issues/488

Received on Saturday, 23 April 2016 01:39:13 UTC