# Chapter 17: Web Components

> ⚠ **Planned — not in Loup 1.25.** This chapter describes the design in the Language Reference (§2-4.10, with §1-16.7.1 for CSS and §1-20.6 for bootstrap registration). The toolchain does not implement it yet, so its listings are design sketches and are not verified.

The Reference specifies W3C Custom Elements v1 support: a class annotated `@webcomponent("tag-name")` becomes a custom element that any HTML page, framework or Loup fence can use. The browser runtime registers it with `customElements.define()`, renders it into a shadow root and keeps its observed attributes in sync.

Loup 1.25 has no browser runtime, and the compiler does not know the annotation: it accepts the class, ignores `@webcomponent`, and warns after the program's output:

```text
main.lpweb:3:1: warning: Unknown annotation '@webcomponent'
```

## Declaring a component

````loup,ignore
@mode(web.compiled)

from ui import (signal, View, Signal);

@webcomponent("my-counter")
class CounterElement implements WebComponentLifecycle {
    @attribute var label: string = "Count";
    @attribute var step: int = 1;

    let count: Signal[int] = signal(0);

    override func attributeChanged(name: string, oldValue: string?, newValue: string?): void {
        # the @attribute field already holds the new value
    }

    func render(): View {
        return ```(ui)
        <Column spacing={$ 8 $}>
          <Text>{$ this.label $}: {$ this.count.get() $}</Text>
          <Button onclick={$ func(): void { this.count.update(func(n: int): int => n + this.step); } $}>
            <Text>+{$ this.step $}</Text>
          </Button>
          <Slot />
        </Column>
        ```;
    }
}
````

*Listing 17-1: A counter custom element (design sketch)*

A page then writes `<my-counter label="Score" step="5"><span>…</span></my-counter>`. The rules (§2-4.10.2):

- The tag is a compile-time string literal containing a hyphen and only ASCII lowercase letters, digits, `-`, `.` and `_`. Invalid or duplicate tags are compile-time errors.
- The class must declare `func render(): View`, which re-renders when a signal it reads changes.
- `@webcomponent` is valid only in `web.scripted` and `web.compiled` files on the `browser` target; elsewhere it is `E_WEBCOMPONENT_NOT_WEB`.

## Attributes and lifecycle

`@attribute` marks an observed attribute (§2-4.10.3). On a `var` field the type is `string`, `int`, `float` or `bool`: numbers are parsed (a malformed value keeps the default and logs a warning), and a `bool` is `true` when the attribute is present. Attribute names are the member names lowercased, so `myLabel` observes `mylabel`; write `@attribute("my-label")` to choose another. A `get`/`set` property pair may also be an attribute; one with no stored backing field is `E_ATTRIBUTE_NO_BACKING_FIELD`, and `@attribute` outside a `@webcomponent` class is `E_ATTRIBUTE_NOT_WEBCOMPONENT`.

`WebComponentLifecycle` (core §1-11.2) declares `connected()`, `disconnected()`, `adopted()` and `attributeChanged(name, oldValue, newValue)`, each with a default no-op body, so a class overrides only what it needs. Register outside listeners in `connected()` and remove them in `disconnected()`.

## Shadow DOM, slots and CSS

Each element renders into an open shadow root, available as `this.shadowRoot` (type `ShadowRoot` from `web.dom`). `<Slot />` in a `ui` fence or `<slot>` in an `html` fence marks where light-DOM children appear; a slot anywhere but a `@webcomponent`'s `render()` is `E_SLOT_NOT_WEBCOMPONENT` (§2-4.10.4).

A `css` fence evaluated in `render()` is attached to the shadow root with Constructable Stylesheets, and the class-name mangling that `css` fences normally get (§1-16.7.1) is turned off, since the shadow boundary already isolates names:

````loup,ignore
@mode(web.compiled)

from ui import (View);

@webcomponent("status-badge")
class StatusBadge implements WebComponentLifecycle {
    @attribute var tone: string = "info";

    func render(): View {
        let styles = ```(css)
        .badge { background: navy; color: white; padding: 4px 8px; }
        ```;
        return ```(html):(none)
        <span class="{$ styles.badge $}" data-tone="{$ this.tone $}"><slot></slot></span>
        ```;
    }
}
````

*Listing 17-2: Component-scoped CSS (design sketch)*

Today a `css` fence is an ordinary template whose `render({})` returns its text unchanged; there is no `StyleSheet` value or mangling yet (Chapter 14).

## Using components in fences

A hyphenated tag in a fence is looked up in the `ComponentRegistry` named by `components=` (§2-4.10.6); a `@webcomponent` match maps the tag's attributes to its `@attribute` fields:

````loup,ignore
@mode(web.compiled)

from ui import (ComponentRegistry, View);

pub func Scoreboard(): View {
    let registry = ComponentRegistry.of({"my-counter": CounterElement});
    return ```(html):(components=registry)
    <section class="scores">
      <my-counter label="Home" step={$ 1 $}></my-counter>
      <my-counter label="Away" step={$ 1 $}></my-counter>
    </section>
    ```;
}
````

*Listing 17-3: Using a registered element (design sketch)*

The runtime registers every `@webcomponent` class during bootstrap, before `mount`: instantiate → initialize → register custom elements → wire events → hydrate → mount (§1-20.6). A parent passes data down through attributes, as above. The Reference does not specify how a child sends events up; the `this.dispatch` and `on:event` syntax of earlier drafts of this book is not in it.

## Summary

- Web Components (Language Reference §2-4.10) are not in Loup 1.25; `@webcomponent` is currently an unknown annotation that is ignored with a warning.
- `@webcomponent("tag-name")` needs `render(): View` and is valid only in web modes on the browser target.
- `@attribute` fields (`string`, `int`, `float`, `bool`) become observed attributes; `WebComponentLifecycle` methods all default to no-ops.
- Elements render into an open shadow root; `<Slot />` projects children; `css` in `render()` attaches to the shadow root unmangled.
- A `ComponentRegistry` makes elements usable in fences; registration happens during bootstrap, before `mount`.
