Open oscarotero opened 12 months ago
What is the benefit of this over just doing customElements.define
?
I can think of some benefits:
customElements.define
. With this protocol, all components will work in the same way.Allows to add a prefix of the tag name of all components:
import Button from "./components/button.js";
import Icon from "./components/icon.js";
import Selector from "./components/selector.js";
[Button, Icon, Selector].forEach((comp) => {
comp.tagName = "prefix-" + comp.tagName;
comp.register();
});
Provides a way to know the tag name used by a component from other component:
import Button from "./components/button.js";
class ButtonGroup extend HTMLElement {
constructor() {
const buttons = this.querySelectorAll(Button.tagName);
}
}
But you can just do this as well? Im trying to understand what the benefit of having a .register
method is.
import Button from "./components/button.js";
import Icon from "./components/icon.js";
import Selector from "./components/selector.js";
[Button, Icon, Selector].forEach((comp) => {
comp.tagName = "prefix-" + comp.tagName;
- comp.register();
+ customElements.define(comp.tagName, comp);
});
@oscarotero thanks for starting this conversation!!
The "Goals" section proposed in @matthewp's protocol proposal template will be a useful piece of work to smooth the discussion around any proposal. Would be great to start there (even if it's just one or two to get the conversation started) in issues opened as well, so we can all be sure to be on the same page when discussing something like this.
We will be discussing the template ratification at next week's Web Components Community Group meeting, feel free to join in the conversation.
Thanks @Westbrook I don't think I can be in the meeting, but will wait to the template ratification in order to use it for this proposal.
I've used a similar pattern in WebComponents.guide.
One of the benefits to having a static method call is that components can more easily register their dependants, and I imagine it'll be more useful when we have scoped registries:
class MyComponent extends HTMLElement {
static define(tagName = 'my-component', registry = customElements) {
registry.define(tagName, this);
const myreg = new CustomElementRegistry()
MyDependant.define('my-dependant', myreg);
MyOtherDependant.define('my-other', myreg);
}
}
This allows consumers to call MyComponent.define()
without thinking about the internal structures/dependents of that component.
Shouldnt an element that uses other elements internally via scoped registries just define them by default? If you forget calling .define, the element doesnt render anything? Why not do it automatically?
You mean to move it into the constructor or something? So the code in my comment would instead be:
let registry = new CustomElementRegistry()
class MyComponent extends HTMLElement {
constructor() {
super()
if (!registry.get('my-dependent')) registry.define('my-dependent', MyDependent)
if (!registry.get('my-other')) registry.define('my-other', MyOtherDependent)
}
}
Yeah, or something like we do in scoped-elements: https://github.com/open-wc/open-wc/blob/master/packages/scoped-elements/html-element.js
im not sure what the benefit of requiring a consumer of your element to manually call .define before being able to use the element is?
I do like a static property to suggest a default tagName (even though its just that; a suggestion, because a consumer of your class may register it under a different name) I also do this in generic-components, I just dont think a lot of abstractions over customElements.define actually provide anything over… just calling customElements.define.
I believe scoped registries solve all use cases listed so far.
The only difficulty is getting all library authors to not automatically define their elements. And what about existing libraries that authors don't have time to update existing libraries?
There are many custom elements today using a class decorator like @element('some-el')
, which automatically defines. They are likely encouraged to leave things as is unless the upstream decorator makes definition not happen by default.
Maybe that's what frameworks similar to Lit should do in that case, is release a breaking major version at some point where the decorator does not define by default? Those decorators can return a subclass with a method similar to above .register
(I use .defineElement
in my lib to make it less ambiguous) that users can call, optionally taking in a registry arg (or maybe requiring the registry arg is better, forcing people to think about which registry), f.e.
import {element, Element} from '@lume/element'
@element('my-el')
export class MyEl extends Element {...}
import {MyEl} from './MyEl.js'
// ... inside other element class ...
const reg = new CustomElementsRegistry()
this.attachShadow({mode: 'open', customElements: reg})
MyEl.defineElement() // error, no reg provided
MyEl.defineElement(reg) // ok
MyEl.defineElement(reg, 'other-name') // custom name
I agree that a custom element shouldn't force a user to have that custom element defined by a particular name, and I think it's unfortunate that that is what typically happens. Of course, a big benefit of the custom element defining itself is that a user who doesn't care about the name and just wants to use the default name can do so. I like the idea of a custom element class having a static property that contains an author-specified default name that a user can use if they don't want to fuss; that seems like quite a good idea, actually.
But I strongly agree with @thepassle that abstracting customElements.define
like this is a mistake. Also, I think the idea of this tag name being mutable (as opposed to an author-specified constant) is a mistake, since it implies that an entire custom element class is only allowed to be registered once under one particular name, which isn't a constraint that custom elements normally have (and in general I don't think the benefit of this has been justified).
The biggest problem I have with this protocol proposal is: I don't think this is a protocol, since it doesn't relate to interaction between components. A component author could simply choose to expose this or some variation of it as part of a component's public API, and indeed, the only way you could use this is if the component author chose to make it part of the public API—in which case, this isn't some standard 'protocol'; it's just that component's particular API.
This proposal cannot achieve "a consistent way to register web components" for "all components", since in general, most components will not support this. And indeed, we already have a consistent way to register web components, that all components support: it's customElements.define
.
FWIW instances can access this.localName
or customElements.getName(MyClassElement)
to get the name as defined, so I agree that static tagName
is of limited value.
I was mistaken in my previous comment; I got confused and thought you could register a custom element twice under different names, but it's fairly intuitive that you can't, since if you instantiated a custom element directly, but it has multiple names, what the tag name would be for that instance is indeterminate.
Given that, I don't think it's obvious that user-customized tag names actually are a good idea. If two different implementations try and define the same custom element on the same page under different names, the one that goes second will fail, which is unpredictable since in general the ordering is arbitrary. Of course, scoped registries would theoretically solve this problem, and the fact that this limitation exists at all is silly (I think all the limitations of custom element registries are silly, but that's a matter for another time), but the single global registry is currently the status quo. The linked article justifies user-customized tag names by saying
However, there is still no way to customize the “tag name”. What if
my-butt
is already occupied? A reusable element needs to allow registering itself with a different tag name.
But I think the flexibility the author desires simply isn't possible with a single global registry. Customizing the tag name solves one problem but creates another, equivalent problem.
EDIT: Also, how do we know whether my-butt
is already occupied, and what do we do in response? If we change the name under which we define it, what if a different implementation tries to define a custom element under that name? It's naming conflicts all the way down.
Hi. I'd like to propose a generic API to register custom elements easier, and allow to customize the tag name if it's needed. I got the inspiration from this article from @mayank99.
The code:
This would allow to register the element in different ways: