Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[idea] Allow custom element naming on a per-shadow-root basis. #488

Closed
trusktr opened this issue Apr 23, 2016 · 30 comments
Closed

[idea] Allow custom element naming on a per-shadow-root basis. #488

trusktr opened this issue Apr 23, 2016 · 30 comments

Comments

@trusktr
Copy link
Contributor

trusktr commented Apr 23, 2016

I say on a "per-shadow-root basis", but it can be more generally just some form of encapsulated basis where an element definition doesn't affect the DOM outside of the encapsulation, and doesn't affect the DOM of an encapsulation inside 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 unit of encapsulation on which I'm making this proposal (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

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:

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:

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:

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:

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:

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?

@trusktr
Copy link
Contributor Author

trusktr commented Apr 23, 2016

I'm referencing ShadowDOM roots as the unit of encapsulation, but there might be other ways. Ultimately, what I'm proposing and asking for is "a way to encapsulate which elements I will use in my DOM or HTML markup on a per-component (custom-element-with-shadow-root) basis, similar to what we have with React where lexical scoping helps us encapsulate specific elements (other React components) for use in a React component.

@trusktr
Copy link
Contributor Author

trusktr commented Apr 26, 2016

In my opening post, I compared this feature to React. There is also a similar Angular 2 feature: @Directives. We would achieve a similar effect in Angular 2 with some code like this:

import {Component} from 'angular2/core';
import {SomeElement} from './some-element.directive';
@Component({
  selector: 'my-app',
  templateUrl: 'app/app.component.html',
  directives: [SomeElement]
})
export class AppComponent { }

where the line directives: [SomeElement] specifies a list of "custom elements" (custom elements in Angular can be made with "directives", although directives can be used in other ways than for just creating custom elements) that are available for use within the component. The SomeElement directive might look like this, which you'll notice applies to <some-element> elements:

// some-element.directive.js
import {Directive, ElementRef} from 'angular2/core';
@Directive({
    selector: 'some-element' // <some-element>
})
export class SomeElement {
    constructor(el: ElementRef) {
      // do something with `el`, a <some-element> element.
    }
}

If you notice the @Component has a selector my-app. This means that the component applies to <my-app> elements.

This concept in Angular 2 provides some form of encapsulation of logic for the use of certain elements. In the example, <some-element> is a "custom element" that is scoped (registered) to be used only within the DOM of a <my-app> element. This is similar to the idea of registering Custom Elements onto ShadowDOM roots in order to scope where those custom elements are used.

Any thoughts? What other discussions, if any, have there been on encapsulating Custom Elements to certain components?

@trusktr
Copy link
Contributor Author

trusktr commented Jun 25, 2016

No replies yet. Any thoughts or ideas about this?

@rniwa
Copy link
Collaborator

rniwa commented Jun 25, 2016

The problem here is that super() must be made from your custom element to HTMLElement's constructor, and HTMLElement's constructor has no way of knowing inside which context the element was created when new MyForm is called. See discussions in #369.

It is possible that we can extend the existing API to allow this use case by adding createElement which sets a special flag on the construction stack to indicate that a given custom element belongs to a specific shadow tree but this would mean that new MyForm wouldn't work for such custom elements.

@rniwa
Copy link
Collaborator

rniwa commented Jun 25, 2016

@domenic
Copy link
Collaborator

domenic commented Jun 25, 2016

There are bigger problems, e.g. how CSS selectors or the HTML parser could work given how they both assume a single global namespace. Consider a selector some-element { color: blue; } for example, or a DocumentFragment containing some-element which is parsed once but whose elements could be inserted anywhere. If there are multiple definitions for some-element floating around all this falls apart.

React sidesteps these issues by moving away from HTML semantics in favor of just divs everywhere, and tracking the metadata seperately. This causes such ... interesting ... knock-on effects as people moving all their CSS into JavaScript as inline styles, since selectors no longer work in React apps. Custom elements is the opposite of this world, actually using and working with the DOM instead of avoiding it and treating it as a rendering layer. So I don't think there's really any interest in proposals to move away from that.

@trusktr
Copy link
Contributor Author

trusktr commented Jun 28, 2016

Idea: what if registrations are loose? So, for example, let myForm = new MyForm just instantiates the element, but there is no tagName associated with it yet. When myForm gets attached into, for example, a shadow root with MyForm registered to the my-form tagName, then the instance gains that name while in that shadow node tree, and therefore selectors that target my-form within that node tree would properly target that instance.

As for document fragments, maybe they too can have a loose registry in the same fashion.

Element instances would then be able to be detached and and attached into new node trees, and would take on new tagNames. The element name then becomes sort of like a label, telling the node tree "Hey, whenever an element that is instanceof MyForm is in this tree, we refer to it as my-form in the markup, and my-form when selecting elements by tag name with CSS."

The name of the MyForm instance would be unknown until the node is placed in the context of a tree, and in that case the tagName is simply a way to refer to the instance while the instance lives in that tree.

Instances of a Custom Element don't really actually need to know what they are referred to as in order to be what they are. For example, my mom in my family tree calls me one thing, but my friends in one of my friend trees might call me something else, but in the end, I am still me.

There are bigger problems, e.g. how CSS selectors or the HTML parser could work given how they both assume a single global namespace

This would have to change a little, but that's okay because this would be a hidden implementation detail and would not affect end usage. The parser can look up the registry depending on which tree the parsing is happening for. For example, setting innerHTML on a shadow root would cause the parser to use that root's registry; similar with CSS.

This would also need to apply to units of trees, so not just shadow trees, but also document fragments. Any others?

@trusktr
Copy link
Contributor Author

trusktr commented Jun 28, 2016

This would make it tricky to use innerHTML on a random div element that hasn't been connected yet, and therefore querySelector would not be able to know what tag names are associated with any of the elements.

Idea: what if the elements created with innerHTML et al are just inert until they exist in a live node tree within the document or within a shadow tree, and are only ever upgraded at that point? So,

let div = new HTMLDivElement
div.innerHTML = "<my-form></my-form>" // MyForm is not instantiated because we don't know what registry is associated yet.
div.querySelector('my-form') // HTMLUnknownElement instance (or similar)
someShadowRoot.appendChild(div)
div.querySelector('my-form') // MyForm instance, has been upgraded based on root's registry.
someShadowRoot.removeChild(div)
div.querySelector('my-form') // HTMLUnknownElement, and MyForm instance was GCed?

Downside would be a loss of the MyForm instance in that example. Maybe there's other ways to make it happen? I can think of some nice ways, but they break backwards compatibility. For example, perhaps querySelector only works on "live" nodes that are in document or shadow tree, but then that means querySelector would not work on trees that haven't been attached yet because it wouldn't know which registry to use.

@trusktr
Copy link
Contributor Author

trusktr commented Jun 28, 2016

Another backwards-incompatible idea could be to simply make selecting based on tag names an throw an error when not selecting on a document or shadow tree, while allow only selectors that don't use tag names to work.

At this point I'm just stating some ideas in hopes that it might spark some actually good solution, but I don't see a clear solution to the naming problem, yet...

@trusktr
Copy link
Contributor Author

trusktr commented Jun 28, 2016

Just some more brain-storming, but not necessarily a solution:

Maybe a registry can be an instantiable?

let myRegistry = new ElementRegistry
myRegistry.define('my-form', MyForm)

let shadowRoot = someEl.createShadowRoot(myRegistry)
let frag = document.createDocumentFragment(myRegistry)

let form = frag.createElement('my-form')

shadowRoot.appendChild(form)
frag.appendChild(new MyForm) //  works based on supplied registry

let div = frag.createElement('div')
div.innerHTML = '<my-form></my-form>' // uses myRegistry to find class
div.querySelector('my-form')

In the case thatdiv is removed and attached to a new tree, then perhaps lookup is reversed: a name is looked up by class. If the new node tree where div is connected to has a name associated with MyForm, then maybe that element in div's tree takes that new name? If the name isn't registered, then it takes the name <unknown-element> and has a property class to reference the class.

... Or, what if registries are used to specify context?

div.querySelector('my-form', myRegistry) // find nodes that are instance of whatever is mapped in the registry.
someElement.innerHTML('<my-form></my-form>', myRegistry)

Any other ideas on how to possibly scope elements? Until this is possible, I can't foresee stopping to use JS libraries to achieve this (for example using scoped variables in React).

@rniwa
Copy link
Collaborator

rniwa commented Jun 28, 2016

Idea: what if registrations are loose? So, for example, let myForm = new MyForm just instantiates the element, but there is no tagName associated with it yet. When myForm gets attached into, for example, a shadow root with MyForm registered to the my-form tagName, then the instance gains that name while in that shadow node tree, and therefore selectors that target my-form within that node tree would properly target that instance.

That would be really hard to implement in browser engines.

@domenic
Copy link
Collaborator

domenic commented Jul 21, 2016

Let's close this. As noted, there is no real way to implement this in browser engines, or at least no realistic proposal has been made. The closed thread can be used if people want to continue iterating toward such a proposal, but there is no outstanding issue with custom elements or shadow DOM here.

@trusktr
Copy link
Contributor Author

trusktr commented Jul 17, 2017

@domenic You are the most eager on here to close ideas (nothing personal, just noting). We can really benefit from some type of scoping. React is proof not to ignore, for example.

@trusktr trusktr changed the title [idea] Allowing custom element registrations on per-shadow-root basis. [idea] Allow custom element naming on a per-shadow-root basis. Sep 6, 2017
@trusktr
Copy link
Contributor Author

trusktr commented Sep 6, 2017

Update: Vue has per-component renaming of components, meaning we can name a shadow component anything we want within light component. These ideas are real.

@trusktr
Copy link
Contributor Author

trusktr commented Nov 2, 2017

Mapping element string names to constructors is just as easy as mapping constructors to string names using Map so I imagine shadow-root-scoped elements easy to implement ("polyfill")...

@trusktr
Copy link
Contributor Author

trusktr commented Nov 2, 2017

@WebReflection any thoughts on this idea, since you have experience implementing CE already?

@WebReflection
Copy link

@trusktr this thread is from 2016 and we're nearly 2018 ... if developers answered that is hard to implement and no proposal has been concretely written since 2016 I don't think there's much else to do, right?

I also don't fully understand what is that you are trying to solve (you or vue) ... shadow DOM is good to reuse CSS and components, having random names per shadow DOM doesn't seem to be a wise move for reusability, IMO, but again, I'm not interested in necro-bumping this 2016 ticket started with a Custom Elements V0 example, sorry.

@trusktr
Copy link
Contributor Author

trusktr commented Nov 3, 2017

The standards process is much slower than necrobumping this thread, so it seems fine if we are to continue to grow ideas.

If you don't see the power that comes with Vue/React/Angular/etc on component-scoped naming of components, then there's not much else I can tell you other than you should learn more about those libraries.

All I have proposed here was to have a way to scope element names on a per-shadow-root basis, and it doesn't matter if it was v0 or v1 when I started the discussion, the same concept still applies:

Scalable component architecture nowadays always includes per-component naming of inner components, and it would be great to have this in Web Components just like we have it in basically every other web framework.

Whoever thought making Custom Elements all globals is simply thinking in globally-polluting sort of way, and now we will all have to pay for it in the long run. The future generations of web developers will have yet another way to ridicule the web and will continue to make triple A titles outside of web technology unless we make serious design decisions that are based on modern engineering standards (f.e. not globals).

@AlessandroEmm
Copy link

AlessandroEmm commented Nov 3, 2017

I'm totally with trusktr on this. We use web components to load functionality from different services/locations with different development lifecycles. Theres no control of how each service names his components - thus collision is something that is very likely over time.

We currently get around it with namespacing the element-names but thats quite ugly especially knowing that components are really only being used in defined scopes/ShadowDOMs.

@WebReflection
Copy link

If you don't see the power that comes with Vue/React/Angular/etc on component-scoped naming of components ...

it's not that I don't see the power, I haven't even read this thread because is from 2016 and after a quick scan I've noticed more than a person involved with the process and the implementation said already it's not gonna happen.

I also don't buy the issue because if this is the reason:

We use web components to load functionality from different services/locations with different development lifecycles

sounds to me like looking for troubles. What do you mean you don't know what you are loading?

Has the term XCS came up already? That's Cross Component Scripting which is IMO as bad as XSS.

We managed to make npm modules name convention good enough for the biggest open source repository out there, I am sure we can survive with current Custom Elements global naming status for the time being.

I rather would like to see Custom Elements V1 out in every browser and as of today I have zero interest in making Custom Elements any different from the current status, the only bit I honestly miss is the abiity to tell when a node has been upgraded.

The name? I don't care, use prefixes like we do in npm and every other global shared place.

@trusktr
Copy link
Contributor Author

trusktr commented Nov 3, 2017

Well, I mentioned you for a reason: you're one of the only two (three?) people who have polyfilled Custom Elements, so therefore you might be able to provide the best insight (for me) on how feasible it would be to implement scoped Custom Element naming, or if that is even possible.

I've noticed more than a person involved with the process and the implementation said already it's not gonna happen

So, just because "the powers that be" say to jump off a bridge (i.e. write bad code with lower maintenance standards), you will do so? You will let them rule? You won't speak up?

We use web components to load functionality from different services/locations with different development lifecycles

sounds to me like looking for troubles. What do you mean you don't know what you are loading?

He means, for example, you load any number of 3rd party libraries, eventually, some Custom Element names are going to conflict. This is a problem that leads to high-maintenance costs and head aches, and a problem that designers of Custom Elements have decided is okay to have in current and future applications the more complex they get.

We do know what we import into our applications, but sometimes we don't know the name of every single component that a library might register, and for there to be a stupid runtime error because of this is just ugly.

We could've opted to have some form of scoping, and therefore we would be able to avoid or easily mitigate the problem without having to resort to forking 3rd party code and consuming the forked code from somewhere else, or without having to resort to absurd build steps to rename elements (these are high maintenance burdens that can easily be avoided).

People who use Vue/React/etc don't have this problem, they can easily name components anything they want within the scope of their components.

Has the term XCS came up already? That's Cross Component Scripting which is IMO as bad as XSS.

Describe how this has to do with scoped element naming.

We managed to make npm modules name convention good enough for the biggest open source repository out there, I am sure we can survive with current Custom Elements global naming status for the time being.

It's slightly different, because there's only one source of truth when we get NPM dependencies: npmjs.org.

There's not one source of truth with Custom Elements: any authors can publish any number of custom elements to anywhere, and inside the packages that they publish, even if it is on NPM, there can be conflicting element names. There isn't a mechanism that avoids publishing of an already-existing element name, like there is with NPM packages. If there were, then you're right, such a mechanism like NPM's would prevent the problem, but it doesn't exist with element naming until it's too late, and the elements have been placed into an application only to cause runtime error. You'll never get a runtime error about duplicate NPM packages with the same name, because duplicate-named packages from NPM will never get into your application to begin with. It's not quite the same.

I rather would like to see Custom Elements V1 out in every browser and as of today I have zero interest in making Custom Elements any different from the current status, the only bit I honestly miss is the abiity to tell when a node has been upgraded.

Okay, that's totally fair, better to have that than nothing maybe; but I would also hope that we can be flexible and willing to adapt to newer and better engineering standards (f.e. avoiding global mechanisms like global variables or global registries which are things we've proven many times to be problematic).

@trusktr
Copy link
Contributor Author

trusktr commented Nov 3, 2017

@WebReflection Sidenote: You sort of twisted what @AlessandroEmm said. He did not say he doesn't know what he loads into his application: he said he doesn't control the development process for all the components that are imported into his application (f.e. they can be 3rd-party libraries), and therefore it can be possible that eventually importing multiple (3rd-party) libraries can cause unwanted name collisions that are ugly and time-consuming to deal with.

@trusktr
Copy link
Contributor Author

trusktr commented Nov 3, 2017

@WebReflection Anyways, I'm just simply asking you what you think the honest feasibility of implementing a shadow-root-scoped element naming concept is.

@AlessandroEmm
Copy link

I like webcomponents because they offer me encapsulation and clear APIs, which makes it possible to integrate source I don't know that adhere to APIs. I don't see how this is looking for troubles as it is working perfectly fine - when there are no collisions in the namespace.

@caridy
Copy link

caridy commented Nov 3, 2017

I have tried to convinced @domenic in the past about this, he ended up convincing me that it is probably not realistic due to implementation constrains, which I understand. Just for the record, this is real, this is going to be a big pain for anyone mashing up web components from different authors (think of multiple html imports). This is the reason for the NPM success, remember? for the first time we were able to not have conflicting dependencies. Can that model work on the browser? I don't know! Do we have an escape hatch to support this? I have found none other that using a different infrastructure for composition (without using web components).

If you can have control over the registry, and control over all the universe of components that your app will ever use, you can probably lock down the registry, and get away with it. But if that's not possible for your app, then you can't really mash up things from different sources due to conflicts.

@WebReflection
Copy link

WebReflection commented Nov 4, 2017

Anyways, I'm just simply asking you what you think the honest feasibility of implementing a shadow-root-scoped element naming concept is.

I don't have enough background to understand platform constrains so whatever I think/say might be irrelevant or wrong but I see the following issues:

  • if you document.createElement('some-component') which one is it?
  • if you have an element in a shadow root and you clone/import it somewhere where there is another definition, which one will win?
  • are elements upgraded multiple times if these travels around the DOM outside their shadow root?

What I see is that x-gallery should be a well known element in the page that behaves in an expected way. I want to be able to read its name, check the resgistry, verify its class, know its observed properties and know which event it exposes ... otherwise it'll be full chaos if I don't know what the hell is that x-gallery.

There are also way to avoid conflicts, you register elements only if not registered yet.

if (!customElements.get('x-gallery'))
  customElements.define('x-gallery', class extends HTMLElement {});

From components that behaves accordingly to their trusted x-gallery they should also do similar dance:

customElements.whenDefined('x-gallery').then(() => {
  const XGallery = customElements.get('x-gallery');
  if (XGallery.brand === 'MyFavorite')
    customElements.define('x-gallery-card', class extends HTMLElement {});
});

And so on, so you have CE that trust each other and work as expected instead of having to deal with unpredictable components that might collide with each other.

And I am talking about a silly gallery, I can't imagine a <paypal-credit-card> provided by anyone byt paypal what could cause on a page.

TL;DR I think technically it could be done but the list of caveats and footguns would be longer than a list of benefits which are, IMO, rather confusing to me.

Go and ask A-Frame developers how happy they'd be to have anyone offering different A-Frame components ... I mean, come on, we all can namespace and namespace for developers have always been a plus in trust, rarely an issue.

@Jamesernator
Copy link

I find the suggested mechanics of this proposal are weird but I still agree with namespacing elements to shadow doms, it's one of the reasons I've had little interest in the Custom Elements part of webcomponents and have really only looked at using Shadow DOM directly with divs. My only real interest in Custom Elements is the life-cycle events (which if this issue happens then that sole reason to even consider using custom elements vanishes).

I'd suggest a simpler proposal that guarantees that when an element is created it's already associated with a Shadow DOM for example instead of this auto-magically associating itself when attached to a shadow DOM like currently proposed:

const foo = new FooBarElement()
shadowRoot.customElements.define('foo-bar', FooBarElement)
// foo magically adopts the semantics within shadowRoot
shadowRoot.appendChild(foo)

I'd instead just propose that a shadow root must be provided when creating the element (and that new CustomElement only remains for backwards compatibility with global customElements) e.g.:

shadowRoot.customElements.define('foo-bar', FooBarElement)

// Some options

// 1. Just have a shadowRoot as optional second argument to createElement
// although I think this might be backwards incompatible?
const foo = document.createElement('foo-bar', shadowRoot)

// 2. Just have createElement be a method of shadowRoot, definitely 
// compatible and pretty simple to use really
const foo = shadowRoot.createElement('foo-bar')

// 3. Method on HTMLElement itself, possibly useful for creating
// elements from the constructor itself
const foo = FooBarElement.create(shadowRoot)

Now given something like this we'd need to consider what sort've surface area it would have. These are the things I can think of that would need changes:

  • Assignment to .innerHTML would need to during the fragment creation stage store the current ShadowRoot of the current element and lookup element names in that ShadowRoot before checking window
  • Similarly we'd ideally have a similar thing for importNode so we clone nodes from templates as if they were already associated with the current ShadowRoot
  • CSS rules would probably need a need some additional block-thing (like @scoped { }) or something like that would allow adding rules to shadow root defined elements (for example @scoped { local-registered-name { color: red; } })

Overall I don't think the necessary surface area of changes is likely to be huge, and it wouldn't need to have anything that breaks current semantics in order to be easy to use and safe for distributing components.

@WebReflection
Copy link

WebReflection commented Nov 8, 2017

OK @Jamesernator I think you have my attention and I like your idea.

There is already a CustomElementRegistry class exposed so I guess having multiple instances provided by the platform shouldn't be too difficult.

Each instance should be aware of its parent registry 'cause shadows can be in shadows so a bottom up lookup can be performed.

We've got the scoped issue "solved", we need to fix the creation ambiguity.

Since we're moving from global to localized, and since the document.createElement has been already hijacked in various way, I wonder how difficult would it be to confine the element creation through the customElements instance itself.

That is: customElements.create('my-element') with room for an optional second argument as object to learn from the past and not repeat same awkward situation we have with document.createElement.

So now we have localized ability and zero creation ambiguity, yet we miss one major issue I've previously raised: what happens to a node created and upgraded in a precise shadow, once it lands in another part of the document?

class SubEl extends HTMLElement {
  connectedCallback() {
    this.textContent =
      `${this.parentNode.nodeName} > ${this.nodeName}`;
  }
}
// for demo sake, we define it globally
customElements.define('s-el', SubEl);

const ParentEl = (() => {
  const shadows = new WeakMap;
  return class ParentEl extends HTMLElement {
    constructor() {
      const sd = super().attachShadow({mode: 'closed'});
      // sd.customElements.define('s-el', AnotherClass);
      shadows.set(this, sd);
      sd.innerHTML = '<s-el></s-el>';
      this.addEventListener('click', this, {once: true});
    }
    onclick(e) {
      e.preventDefault();
      const sd = shadows.get(this);
      this.ownerDocument.body.appendChild(sd.firstChild);
    }
    handleEvent(e) {
      this['on' + e.type](e);
    }
  };
})();
// for demo sake, we define it globally
customElements.define('p-el', ParentEl);

document.body.innerHTML = '<p-el></p-el>';

Now, if you click on that node you'll see a BODY > S-EL instead of #document-fragment > S-EL.

In a situation where ParentEl instance shadowRoot registers its own S-EL and that node is passed to the document body where there was another S-EL, what is expected/desired behavior?

IMO that should throw an Illegal DOM operation or something similar but it's a concern we eventually need to address and/or explain to developers.

@justinfagnani
Copy link
Contributor

I've had a rough proposal for scoped custom element definitions that associated floating around the Polymer team for a little while, and I was just pinged that @Jamesernator had a very similar idea :)

I put my doc up in a gist: https://gist.github.com/justinfagnani/d67d5a5175ec220e1f3768ec67a056bc

I've floated it casually past a few browser implementers, and so far it seem like they don't see a critical show stopper in it. It would need a lot more scrutiny obviously.

The summary of the proposal is:

  • CustomElementRegistry becomes constructible and can inherit definitions from a parent registry.
  • Element.prototype.attachShadow() accepts a registry to use instead of the global registry.
  • ShadowRoot gains DOM creation APIs like createElement() and importNode() which can be used instead of document.createElement() to use scoped registries.
  • Elements remember their scope, so innerHTML creates DOM in the element's scope, and attachShadow() can use its element's scope by default.

This proposal requires that at least some DOM creation code be updated to use ShadowRoots (or the result of getRootNode()) instead of Documents for imperative DOM creation, so there may be some upgrade difficulties when mixing new scope-aware code and old scope-unaware code.

I think this concern might be mitigated by being able to upgrade frameworks and template libraries, and by the global registry still being the default registry. Using scoping wouldn't necessarily require that all DOM creation code in an app use the scoped API, just the code that's needed to work within a scope.

@justinfagnani
Copy link
Contributor

@domenic not sure if we're closer to a realistic proposal now, but should this issue be reopened, or should I create a new one with my proposal?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants