Html: CustomElementsRegistry whenDefined then => Class

Created on 16 May 2020  Â·  13Comments  Â·  Source: whatwg/html

Currently, it's written in specs that:

If this CustomElementRegistry contains an entry with name _name_, then return a new promise resolved with undefined.

Background

Using .whenDefined('ce-name') is the platform mechanism to await CE dependencies without needing to rely on these being available on the global context.

Use cases for using whenDefined are pretty clear and simple:

  • extending a specific component
  • include a specific component within the component layout to grant proper functionality

Current State

While the component inclusion does not usually require the direct usage of the CE class, the extending part is verbose for no reason.

Example

customElements.whenDefined('base-ce').then(
  () => {
    const BaseCE = customElements.get('base-ce');
    customElements.define('reach-ce', class extends BaseCE {
    });
  }
);

Proposal: resolve the class instead of undefined

Not only extremely simple to polyfill, resolving the class, which can never be garbage collected, once defined through the Registry, seems to follow the _least semantic surprise_, so that extending base components would look instead as such:

customElements.whenDefined('base-ce').then(
  BaseCE => {
    customElements.define('reach-ce', class extends BaseCE {
    });
  }
);

A promise that resolves undefined seems to have no benefit over a promise that passes along what the developer was actually waiting for, so I hope this change will be welcomed:

If this CustomElementRegistry contains an entry with name _name_, then return a new promise resolved with such entry.

Thanks for consideration.


The polyfill in a nutshell

const {whenDefined} = customElements;
Object.defineProperty(
  customElements,
  'whenDefined',
  {
    value(name) {
      return whenDefined.call(this, name)
                        .then(() => this.get(name));
    }
  }
);

The "no need for polyfill" version

customElements.whenDefined('base-ce').then(
  (BaseCE = customElements.get('base-ce')) => {
    customElements.define('reach-ce', class extends BaseCE {
    });
  }
);
additioproposal needs implementer interest custom elements

All 13 comments

This seems like a reasonable request to me.

Although... customElements.define takes an argument ([Edit] meant to say options), and it's feasible that we end up adding more options, in which case we may want to pass along something like a CustomElementDefinition instead so that's something to consider to make it future proof.

@rniwa thanks for considering this improvement, although define takes at least two arguments and the third one is already the options one you're referring to, but the name is unique/global in an case.

However, if the idea is to have support for eventually scoped registries, whenDefined(name, registry = self.customElements).then(() => registry.get(name)) seems to be reasonable as well to me.

@WebReflection Since registry.whenDefined(name) is already an instance method, having to pass the this parameter twice makes no sense.

@ExE-Boss I don’t think I understand your comment, but the idea here is that ‘whenDefined’ resolves with what developers wait to be defined.

No double instance to pass at all.

@rniwa thanks for considering this improvement, although define takes at least two arguments and the third one is already the options one you're referring to, but the name is unique/global in an case.

Yeah, sorry, I meant to say options. What I mean is that it's possible we may want to add more states & information to a custom element definition in which case having some kind of CustomElementDefinition object which has the elements class as one of its properties might be useful instead of just giving the class itself. For now, it could just be a dictionary too.

Just a thought but perhaps that's an overkill.

@rniwa except for WebKit, all other browsers support already the options argument:

customElements.define(name, Class, options);

Even if currently used for builtin extends only, I think the define method itself is already future proof, and moving options one parameter before won't bring much benefits, but likely break backward compatibility (i.e. CE polyfills from 2014)

I can see, for namespaced/scoped/non-conflicting namespaces, the third argument with a registry property might be already a good/forward-compatible move (beside the name) so, for the time being, all whenDefined needs to do is really just resolve with anything that's being "_awaited"_, so that no extra customElements.get(name) is needed once whenDefined(name) is resolved.

The eventual future proof version of this could be whenDefined(name, options) which would be symmetric (or better, easy to reason about) with define(name, Class, options), so that if the eventual registry property is available, there won't be much confusion.

That being said, the registry eventually needs much more discussions around "_how an element can be part of a registry intead of the global shared one_", so it might be overkill indeed, as I don't see it coming any time soon, while using whenDefined is already a pretty common use.

@rniwa except for WebKit, all other browsers support already the options argument:

customElements.define(name, Class, options);

Even if currently used for builtin extends only, I think the define method itself is already future proof, and moving options one parameter before won't bring much benefits, but likely break backward compatibility (i.e. CE polyfills from 2014)

I think you misunderstood me. What I'm talking about is what we use to resolve the promise. I'm saying that instead of:

customElements.whenDefined('some-element'),then((someElement) => ... class OtherElement extends someElement...)

maybe we could do this:

customElements.whenDefined('some-element'),then((elementDefinition) => ... class OtherElement extends ElementDefinition.class...)

This way if a custom element's definition gets more states (e.g. it would have a different parser behavior, such a thing could be accessed from this newly added definition object. But it's probably an overkill.

@rniwa I see, but unless it’s symmetric with get(name) I don’t see it as an improvement, specially 'cause class cannot even be destructured without an alias, so that it won’t bring much better ergonomics, imho

@rniwa I see, but unless it’s symmetric with get(name) I don’t see it as an improvement, specially 'cause class cannot even be de destructured without an alias, so that it won’t bring much better ergonomics, imho

Fair enough. I think the next step is for someone write a PR request to HTML with WPT tests.

I think the next step is for someone write a PR request to HTML with WPT tests.

I can't work much these days due lack of proper internet connection ... so, if nobody will do that by next week, I'll put together a PR next Monday or later.

@rniwa @WebReflection What is a good way to get implementers feedback on this?

@rniwa @WebReflection What is a good way to get implementers feedback on this?

FWIW, I've implemented this behavior in WebKit as of https://trac.webkit.org/r266142.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

annevk picture annevk  Â·  56Comments

annevk picture annevk  Â·  93Comments

dominiccooney picture dominiccooney  Â·  102Comments

mathiasbynens picture mathiasbynens  Â·  83Comments

stevefaulkner picture stevefaulkner  Â·  110Comments