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

[Proposal] Registration API #52

Open
oscarotero opened this issue Nov 30, 2023 · 13 comments
Open

[Proposal] Registration API #52

oscarotero opened this issue Nov 30, 2023 · 13 comments

Comments

@oscarotero
Copy link

oscarotero commented Nov 30, 2023

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:

class MyComponent extends HTMLElement {
  static tagName = "my-component";

  static register(tagName = this.tagName) {
    customElements.define(this.tagName = tagName, this);
  }
}

This would allow to register the element in different ways:

import MyComponent from "./my-component.js";

MyComponent.register(); // Register as `<my-component>`.
import MyComponent from "./my-component.js";

MyComponent.register("your-component"); // Register as `<your-component>`.
import MyComponent from "./my-component.js";

MyComponent.tagName = "your-component";
MyComponent.register(); // Register as `<your-component>`.
@thepassle
Copy link

What is the benefit of this over just doing customElements.define?

@oscarotero
Copy link
Author

I can think of some benefits:

  1. Provides a consistent way to register web components. Currently, some custom elements are registered automatically on importing the javascript code, others need to do customElements.define. With this protocol, all components will work in the same way.
  2. Allows to edit the tag name in case of conflict with other custom element registered with the same name.
  3. 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();
    });
  4. 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);
       }
    }

@thepassle
Copy link

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);
});

@Westbrook
Copy link
Collaborator

@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.

@oscarotero
Copy link
Author

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.

@keithamus
Copy link
Contributor

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.

@thepassle
Copy link

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?

@keithamus
Copy link
Contributor

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)
  }
}

@thepassle
Copy link

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.

@trusktr
Copy link

trusktr commented Dec 5, 2023

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

@mattlucock
Copy link

mattlucock commented Dec 8, 2023

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.

@keithamus
Copy link
Contributor

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.

@mattlucock
Copy link

mattlucock commented Dec 9, 2023

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.

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

No branches or pull requests

6 participants