# Custom elements

Source: https://signy.dev/documentation/1/components/custom-elements/

A component as a tag any page can use.

`defineElement(tag, Component)` registers a custom element. When the element is added to a page, it mounts the component inside itself. When it is removed, the component is unmounted. Each element is its own [application](https://signy.dev/documentation/1/application/application/index.html.md), so it works in plain HTML, in a CMS template or inside another framework.

It comes from `signy/app`, and from `signy`. The `signy/web` entry does not include it.

## Define and use

The tag name needs a hyphen, as the browser requires. Props are listed in `props`: an object picks the type of each, a plain array means strings. The attribute for `maxItems` is `max-items`.

```js
const { Component, Signal, html, defineElement } = Signy;

class Counter extends Component {
  count = new Signal(this.props.start ?? 0);

  bump() {
    this.count.set((c) => c + (this.props.step ?? 1));
    this.emit("change", { count: this.count.value });
  }

  render() {
    return html`
      <button onClick=${() => this.bump()}>
        ${() => this.props.label} ${() => this.count.value}
      </button>
    `;
  }
}

defineElement("x-counter", Counter, {
  props: { label: String, start: Number, step: Number },
});

// Plain DOM from here on: the page does not know about Signy
const counter = document.querySelector("x-counter");
counter.addEventListener("change", (event) => {
  document.querySelector("#elements-log").textContent = `Count is ${event.detail.count}`;
});
document.querySelector("#elements-rename").addEventListener("click", () => {
  counter.setAttribute("label", "Taps");
});
```

## Props

A prop is a getter over a signal. Read it inside a binding, like `${() => this.props.label}`, and the text follows the attribute. A value read once, such as in `render()` outside a function, stays as it was.

| Type | From the attribute | When the attribute is missing |
| --- | --- | --- |
| `String` | The text. | `undefined` |
| `Number` | `Number(text)`, so `NaN` for text that is not a number. | `undefined` |
| `Boolean` | `true` for any value, including an empty one. | `false` |

- Attributes change props. Setting `el.label = "x"` changes the prop too, but it does not write the attribute back.
- A property set before the element was defined is kept when the definition arrives.
- Only strings, numbers and booleans come from attributes. Pass an object or a function through the property: `el.items = [1, 2, 3]`.
- The component's own `props` type should list the same names.

## Events

`this.emit(name, detail)` fires on the element, so `addEventListener` and attributes like `onchange` work from the outside. See [custom events and slots](https://signy.dev/documentation/1/components/events-and-slots/index.html.md).

## Shadow DOM and slots

A component with `shadow = true` renders into a shadow root on the element. Content between the tags stays in the page and shows through a `<slot>`. Without a shadow root, the output is added after the children the element already has.

```js
const { Component, html, defineElement } = Signy;

class Badge extends Component {
  shadow = true;
  styles = `
    span {
      padding: 0.25rem 0.75rem;
      border-radius: 999px;
      color: #fff;
      background: linear-gradient(90deg, #c040f0, #7a1fc0);
    }
  `;

  render() {
    return html`<span><slot></slot></span>`;
  }
}

defineElement("x-badge", Badge);
```

## Lifecycle

- Connecting the element builds the component and mounts it. `setup()`, `render()` and `onMount()` run as for any component.
- Removing the element unmounts the component in a microtask, so `onUnmount()` and cleanups run.
- Moving the element, for example with `appendChild` to another parent, removes and adds it in the same task. The component stays mounted.
- An element that is removed, then added again later, gets a fresh component. State from the first one is gone.
- Every element has its own app, context and state. Two `<x-counter>` tags do not share anything.

## Plugins

The `setup` option runs for each element before its app mounts. Install plugins there, for example a router or app level values.

```js
defineElement('x-inbox', Inbox, {
  setup(app, element) {
    app.use(router);
    app.provide(ThemeKey, element.dataset.theme ?? 'light');
  },
});
```

## Limits

- A tag can be defined once. A second `defineElement` with the same name throws, as `customElements.define` does.
- The component runs in the browser. `defineElement` does not render on a server.
- A prop with a type other than `String`, `Number` or `Boolean` is a property, not an attribute.
