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:
main.lpweb:3:1: warning: Unknown annotation '@webcomponent'
Declaring a component
@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. @webcomponentis valid only inweb.scriptedandweb.compiledfiles on thebrowsertarget; elsewhere it isE_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:
@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:
@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;
@webcomponentis currently an unknown annotation that is ignored with a warning. @webcomponent("tag-name")needsrender(): Viewand is valid only in web modes on the browser target.@attributefields (string,int,float,bool) become observed attributes;WebComponentLifecyclemethods all default to no-ops.- Elements render into an open shadow root;
<Slot />projects children;cssinrender()attaches to the shadow root unmangled. - A
ComponentRegistrymakes elements usable in fences; registration happens during bootstrap, beforemount.