-
Notifications
You must be signed in to change notification settings - Fork 378
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
Comments
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. |
In my opening post, I compared this feature to React. There is also a similar Angular 2 feature: 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 // 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 This concept in Angular 2 provides some form of encapsulation of logic for the use of certain elements. In the example, Any thoughts? What other discussions, if any, have there been on encapsulating Custom Elements to certain components? |
No replies yet. Any thoughts or ideas about this? |
The problem here is that It is possible that we can extend the existing API to allow this use case by adding |
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 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. |
Idea: what if registrations are loose? So, for example, 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 The name of the 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.
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 This would also need to apply to units of trees, so not just shadow trees, but also document fragments. Any others? |
This would make it tricky to use Idea: what if the elements created with 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 |
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... |
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 that ... 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). |
That would be really hard to implement in browser engines. |
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. |
@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. |
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. |
Mapping element string names to constructors is just as easy as mapping constructors to string names using |
@WebReflection any thoughts on this idea, since you have experience implementing CE already? |
@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. |
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). |
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. |
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:
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. |
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.
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?
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.
Describe how this has to do with scoped element naming.
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.
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). |
@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. |
@WebReflection Anyways, I'm just simply asking you what you think the honest feasibility of implementing a shadow-root-scoped element naming concept is. |
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. |
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. |
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:
What I see is that 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 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 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. |
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 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:
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. |
OK @Jamesernator I think you have my attention and I like your idea. There is already a 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 That is: 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 In a situation where 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. |
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:
This proposal requires that at least some DOM creation code be updated to use 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. |
@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? |
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
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:
What's important here is that
AwesomeElement
can be used in the "HTML" (JSX) of the component due to the fact that theAwesomeElement
is in scope. Some other file can not useAwesomeButton
unless that other file also importsAwesomeButton
.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:
(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:
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:(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 theMyForm
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 theregisterElement
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:
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 registeringawesome-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?
The text was updated successfully, but these errors were encountered: