Documentation menu

Design proposal

Web Components

Status: Principle and design direction. The Vite integration supports one automatically discovered eager component registry; route-level manifests and template property/event binding syntax remain draft.

Goldar uses the browser’s Custom Elements model rather than defining a proprietary component runtime.

Use any implementation

The same view can use a custom element implemented with direct platform APIs:

export class FancyTabs extends HTMLElement {
	connectedCallback() {
		// Initialize browser behavior.
	}
}

Or with Lit:

import { LitElement, html } from "lit";

export class FancyTabs extends LitElement {
	render() {
		return html`<slot></slot>`;
	}
}

Or with another helper. Goldar depends on the platform contract, not the helper’s internal model.

Use custom elements as HTML

A compiled Handlebars view uses the element directly:

<fancy-tabs active-tab={{activeTab}}>
	<section slot="tab">Overview</section>
	<section slot="tab">Activity</section>
</fancy-tabs>

There is no Goldar wrapper component. Before the implementation loads, the element and its light-DOM children are still valid HTML. When registered, the browser upgrades the element.

Registration

The browser registration API is customElements.define():

if (!customElements.get("fancy-tabs")) {
	customElements.define("fancy-tabs", FancyTabs);
}

Goldar also supports self-registering modules:

import "./fancy-tabs.js";

For application-local components, name the module after its tag and default-export the constructor:

// src/fancy-tabs.ts
export default class FancyTabs extends HTMLElement {}

The compiler records <fancy-tabs> usage and the Vite plugin resolves the unique local fancy-tabs.ts module. The components plugin option remains an override for package modules or non-conventional layouts.

Integrated goldar build and goldar dev applications receive a generated browser entry that imports the stable virtual module. A custom Vite pipeline imports it from its own browser entry:

import "virtual:goldar/client";

The client module also imports compiled template styles. Registrations are emitted in lexical tag order. An existing registration with the same constructor is accepted, while a different constructor for the same tag throws. Missing or ambiguous local modules produce source-located template errors. Server template modules never import the browser implementations.

This registry is deliberately eager. Compiler-generated route manifests and route-specific loading chunks remain future work.

Loading policy

Custom-element implementation code should be independently loadable. Possible policies include:

  • Load eagerly from the application client entry. This is the currently implemented discovered registry path.
  • Load with the route.
  • Load when the browser is idle.
  • Load when an element becomes visible.
  • Load on the first relevant interaction.
  • Use an implementation that was already registered by another package.

The default should favor correctness and predictable upgrade timing. More aggressive lazy loading is an optimization and must not make keyboard or pointer interaction unreliable.

Attributes, properties, children, and functions

The compiler must respect DOM distinctions:

  • Serializable string-like values may become attributes.
  • Complex objects should be assigned as properties.
  • Content remains children or named slots.
  • Local functions become event listeners or callable properties.
  • Server capabilities become stable action references rather than serialized closures.

For example:

<fancy-tabs
	active-tab={{activeTab}}
	tabs={{tabs}}
	onchange={{onTabChange}}
>
	{{ outlet }}
</fancy-tabs>

The compiler may lower active-tab to an attribute, tabs to a property, and onchange to a listener based on the element registry’s types. It must not stringify an object or function into server HTML.

The exact template syntax for distinguishing attributes, properties, and listeners is not yet fixed.

Events

Custom elements communicate outward using standard events:

this.dispatchEvent(
	new CustomEvent("tabchange", {
		bubbles: true,
		composed: true,
		detail: { tabId },
	}),
);

Typed view tooling should diagnose event names and callback payloads when the registry provides them.

Events crossing a shadow boundary generally need composed: true. Events intended for ancestor delegation generally need bubbles: true. The component author remains responsible for choosing the correct browser behavior.

Type registry

The compiler and language service should combine:

  • Native HTMLElementTagNameMap entries.
  • Application-owned custom-element definitions.
  • Package-provided registries.
  • Attribute, property, event, and slot metadata.

This registry powers completion and diagnostics in .hbs files. It does not become a runtime service locator.

Conflicting definitions for the same custom-element name should fail during development or compilation when Goldar can detect them.

Server rendering

The baseline server output is ordinary custom-element markup and children. A component may optionally support richer server rendering or Declarative Shadow DOM, but Goldar must not require every custom-element library to implement a framework-specific SSR interface.

Server output should remain useful before upgrade. Components that hide all meaningful content until JavaScript runs work against Goldar’s HTML-first model, even if the framework technically permits them.

When to use a Web Component

Web Components are valuable when a piece of UI benefits from:

  • Browser identity and lifecycle.
  • Encapsulated interaction.
  • Reuse outside Goldar.
  • Shadow DOM.
  • A stable imperative element API.

Plain HTML and view functions are cheaper when the UI only needs document composition. Goldar should make Web Components available everywhere without encouraging every view to become one.