Skip to content

Your first component

JavaScript owns what happens inside the component. Go owns what goes into it and what the page is made of. setup() runs in a browser, so it stays in JavaScript; everything around it can be typed.

  1. Write the component.

    web/components.js
    import { define, html, css, computed } from 'alacris';
    /**
    * A person, at a glance.
    *
    * @prop {string[]} tags the labels shown under the name
    * @fires greet {name: string} - the user said hello
    * @slot title - replaces the heading
    */
    define('user-card', {
    props: { name: 'anon', age: 0, tags: [] },
    styles: css`:host { display: block } h3 { margin: 0 }`,
    setup({ name, age, tags }, host) {
    const grown = computed(() => age() >= 18);
    return html`
    <h3><slot name="title">${name}</slot></h3>
    <p>${() => (grown() ? 'adult' : 'minor')} · ${() => tags().join(', ')}</p>
    <button @click=${() => host.emit('greet', { name: name() })}>say hi</button>`;
    },
    });

    The JSDoc block is not decoration. define() says what a prop is called and how it is coerced. The tags say the rest: that tags is a list of strings rather than a list of anything, that a greet event exists and what it carries, that there is a title slot.

  2. Generate the wrapper.

    Terminal window
    go run github.com/bmartel/alacris-go/cmd/alacris-go generate ./web -o ./internal/components
    alacris-go: 1 component(s) -> ./internal/components
    wrote internal/components/components_gen.go
    wrote internal/components/alacris_gen.go
  3. Read what you got.

    internal/components/components_gen.go
    const UserCardTag = "user-card"
    const UserCardSlotTitle = "title"
    const UserCardEventGreet = "greet"
    type UserCardGreetDetail struct {
    Name string `json:"name"`
    }
    type UserCardProps struct {
    // Name ...
    // The component defaults it to 'anon'.
    Name string
    Age int
    // Tags is the labels shown under the name.
    //
    // Crosses as JSON in the tags attribute.
    Tags []string
    }
    func UserCard(p UserCardProps) *alacris.Element
  4. Use it.

    @components.UserCard(components.UserCardProps{
    Name: "Ada",
    Age: 36,
    Tags: []string{"math", "code"},
    }) {
    <span slot={ components.UserCardSlotTitle }>Ada Lovelace</span>
    }
    And this is what reaches the browser
    return alacris.E("user-card").
    Prop("name", "Ada").
    Prop("age", 36).
    Prop("tags", []string{"math", "code"})
    renders verified by go test
    <user-card name="Ada" age="36" tags="[&#34;math&#34;,&#34;code&#34;]"></user-card>
    which the browser turns into the real runtime, upgrading that markup

    Props are kebab-cased and encoded by their Go type.

Defaults it knows about

The generator read name: 'anon' out of define(), so a Name left empty or set to "anon" is left off the element entirely. Smaller HTML, and only a layer that knows the component’s defaults can do it.

Three more things it did:

  • Kebab-casing that matches the runtime. maxCount becomes max-count using the same transformation define.js uses, quirks and all. A hand-written attribute name that gets this wrong produces an element that silently ignores it.
  • A typed event detail. UserCardGreetDetail is what the live layer decodes into, so a rename in the component’s @fires tag becomes a Go compile error rather than a handler that receives an empty struct.
  • A *bool where it matters. A boolean prop whose default is true cannot be a plain bool. Go’s zero value is false, so the field could never mean “leave it alone”. See Props and encoding.
main.go
//go:generate go run github.com/bmartel/alacris-go/cmd/alacris-go generate ./web -o ./internal/components
//go:generate go run github.com/a-h/templ/cmd/templ@latest generate

Order matters: the wrappers first, because the templates use them.

In CI, check rather than regenerate:

Terminal window
alacris-go check ./web -o ./internal/components
  • Props and encoding: what crosses, how, and the two rules that exist because of sharp edges in the runtime.
  • Generating wrappers: every JSDoc tag, and the manifest escape hatch for components the scanner cannot read.
  • Alacris UI: sixty-eight Material components that ship with the module.