[WICG/webcomponents] Simplified custom element definition (Issue #1064)

Currently, classes have to manage a bunch of explicit wiring that has to be done almost every time. This could all be simplified a lot, to a module that doesn't need to do nearly as much.

```js
<define name="my-counter" attributes="initial">
  <template shadowrootmode="open">
    <button data-inc="+1">+</button>
    <span>{{count}}</span>
    <button data-inc="-1">-</button>
  </template>
  <script type="module">
    export default function initialize(shadowRoot, signal) {
      let count = +this.getAttribute("initial") || 0
      let changed = false

      shadowRoot.updateTemplate({count})

      shadowRoot.addEventListener("click", (ev) => {
        const {inc} = ev.target.dataset
        if (inc !== undefined) {
          ev.stopPropagation()
          count += inc
          changed = true
          shadowRoot.updateTemplate({count})
          this.dispatchEvent(new Event("change"))
        }
      }, {signal})

      shadowRoot.addEventListener("attributechanged", (event) => {
        if (!changed) count = +event.newValue || 0
      }, {signal})
    }
  </script>
</define>
```

The `<define>` element would accept four attributes:

- `name`: the tag name
- `extends`: the builtin it extends (if applicable)
- `attributes`: the observed attributes
- `formassociated`: whether this is form-associated (boolean)

Its children consist of zero or more of the following:

- A `<template>`. If missing, it defaults to what's in its shadow root, replicating the shadow root options in its instances. This is cloned per-instance and used as the instances' children.
- A `<style>` or `<link rel="stylesheet">`. This is adopted into the shadow root without cloning.
- A `<script type="module">`. This has a single `default` export that initializes the instance state, receiving a shadow root and a signal (aborted on any reset). The shadow root would also receive all custom element lifecycle updates as events.

State reset can be done via `elem.reset()`, and is done automatically on form reset.

Form controls get `elem.form` and an implicitly handled `for` attribute to help simplify everything.

> Why not a class? Classes have tons of setup boilerplate. This hides all of that and offloads it all to the browser.

It'd also be nice to see `customElements.define` extended to likewise support such a simplified definition:

```webidl
// `this` is an element instance
// `signal` is aborted and the body re-initialized on state and form reset
callback SimpleInitializer = CustomElementLifecycle (ShadowRoot root, AbortSignal signal);

enum SimpleInitializerType {
  "simple",
};

dictionary SimpleInitializerOptions {
  required DOMString name;
  DOMString? extends;
  sequence<DOMString> attributes = [];
  boolean formAssociated = false;
  (HTMLTemplateElement or DocumentFragment or TrustedHTML or DOMString) template;
  required SimpleInitializer initialize;
};

enum FormStateRestoreType {
  "restore",
  "autocomplete",
};

interface AttributeChangedEvent extends Event {
  readonly attribute DOMString attributeName;
  readonly attribute DOMString? oldValue;
  readonly attribute DOMString? newValue;
}

interface FormAssociatedEvent extends Event {
  readonly attribute HTMLFormElement form;
}

interface FormDisabledEvent extends Event {
  readonly attribute boolean disabled;
}

interface FormStateRestoreEvent extends Event {
  readonly attribute FormStateRestoreType restoreType;
}

partial interface CustomElementRegistry {
    void define(SimpleInitializerOptions options);
};
```

-- 
Reply to this email directly or view it on GitHub:
https://github.com/WICG/webcomponents/issues/1064
You are receiving this because you are subscribed to this thread.

Message ID: <WICG/webcomponents/issues/1064@github.com>

Received on Tuesday, 30 July 2024 15:36:19 UTC