Components

Components are reusable UI pieces that implement the Template interface. They encapsulate structure, styling, and behavior into self-contained units.

Overview

Create a component by implementing Template and its render() method. Pass data via constructor parameters.

public class MyComponent implements Template {
    private final String prop;

    public MyComponent(String prop) {
        this.prop = prop;
    }

    public Element render() {
        return div(prop);
    }
}

// Usage: new MyComponent("Hello")

Basic Component

Components implement the Template interface.

public class Card implements Template {
    private final String title;
    private final String content;

    public Card(String title, String content) {
        this.title = title;
        this.content = content;
    }

    public Element render() {
        return div(class_("card"),
            h3(title),
            p(content)
        );
    }
}

// Usage
new Card("Welcome", "Hello, world!")

Using Components

Components are used like any other element.

record Card(String title, String content) implements Template {
    public Element render() { return div(class_("card"), h3(title), p(content)); }
}
record Product(String getName) {}
class ProductService { List<Product> findAll() { return List.of(new Product("Widget")); } }
ProductService productService = new ProductService();
record ProductCard(Product product) implements Template {
    public Element render() { return div(class_("card"), h3(product.getName())); }
}

div(
    h1("Dashboard"),
    new Card("Users", "Manage your users"),
    new Card("Settings", "Configure the app"),
    new Card("Reports", "View analytics")
)

// In a list
List<Product> products = productService.findAll();
div(each(products, p ->
    new ProductCard(p)
))

Props via Constructor

Pass data to components through constructor parameters.

record User(String getAvatar, String getName, String getEmail, boolean isAdmin) {}
Element badge(String text) { return span(class_("badge"), text); }
User user = new User("/a.png", "Ada", "ada@example.com", false);

public class UserCard implements Template {
    private final User user;
    private final boolean showEmail;

    public UserCard(User user) {
        this(user, true);
    }

    public UserCard(User user, boolean showEmail) {
        this.user = user;
        this.showEmail = showEmail;
    }

    public Element render() {
        return div(class_("user-card"),
            img(src(user.getAvatar()), alt(user.getName())),
            h3(user.getName()),
            when(showEmail, () -> p(user.getEmail())),
            when(user.isAdmin(), () -> badge("Admin"))
        );
    }
}

// Usage
new UserCard(user)
new UserCard(user, false)  // Hide email

Builder Pattern

For components with many optional props.

public class Button implements Template {
    private final String text;
    private String variant = "primary";
    private String size = "medium";
    private boolean disabled = false;

    public Button(String text) { this.text = text; }

    public Button variant(String v) { variant = v; return this; }
    public Button size(String s) { size = s; return this; }
    public Button disabled() { disabled = true; return this; }

    public Element render() {
        return button(
            class_("btn btn-" + variant + " btn-" + size),
            attrs().disabled(disabled),
            text);
    }
}

// Usage
new Button("Submit")
new Button("Cancel").variant("secondary")
new Button("Save").size("large").disabled()

Components with Children

Accept child elements via constructor.

public class Panel implements Template {
    private final String title;
    private final Element[] children;

    public Panel(String title, Element... children) {
        this.title = title;
        this.children = children;
    }

    public Element render() {
        return div(class_("panel"),
            div(class_("panel-header"), h3(title)),
            div(class_("panel-body"), fragment(children))
        );
    }
}

// Usage
new Panel("User Info",
    p("Name: John Doe"),
    p("Email: john@example.com"),
    button("Edit Profile")
)

Slot Pattern

Named slots for complex layouts.

public class Dialog implements Template {
    private final Element header;
    private final Element body;
    private final Element footer;

    public Dialog(Element header, Element body, Element footer) {
        this.header = header;
        this.body = body;
        this.footer = footer;
    }

    public Element render() {
        return div(class_("dialog"),
            div(class_("dialog-header"), header),
            div(class_("dialog-body"), body),
            div(class_("dialog-footer"), footer)
        );
    }
}

// Usage
new Dialog(
    h2("Confirm Delete"),
    p("Are you sure you want to delete this item?"),
    fragment(
        button("Cancel"),
        button(class_("btn-danger"), "Delete")
    )
)

Component Composition

Build complex UIs by composing smaller components.

record User(String getAvatar, String getName) {}

// Small, focused components
public class Avatar implements Template {
    private final String src, alt;
    public Avatar(String src, String alt) { this.src = src; this.alt = alt; }
    public Element render() {
        return img(src(src), alt(alt), class_("avatar"));
    }
}

public class UserName implements Template {
    private final String name;
    public UserName(String name) { this.name = name; }
    public Element render() {
        return span(class_("username"), name);
    }
}

// Composed component
public class UserBadge implements Template {
    private final User user;
    public UserBadge(User user) { this.user = user; }
    public Element render() {
        return div(class_("user-badge"),
            new Avatar(user.getAvatar(), user.getName()),
            new UserName(user.getName())
        );
    }
}

Component Reuse

record User(String getAvatar, String getName) {}
class Logo implements Template { public Element render() { return img("/logo.png", "Logo"); } }
class Navigation implements Template { public Element render() { return nav(a(href("/"), "Home")); } }
record UserBadge(User user) implements Template {
    public Element render() { return div(class_("user-badge"), user.getName()); }
}
User currentUser = new User("/a.png", "Ada");
record Comment(User getAuthor, String getText, String getCreatedAt) {}
List<Comment> comments = List.of(new Comment(currentUser, "Nice!", "2026-01-01"));
record Timestamp(String value) implements Template {
    public Element render() { return span(class_("timestamp"), value); }
}

// Reusable across the app
header(
    new Logo(),
    new Navigation(),
    new UserBadge(currentUser)
)

// In a list
div(each(comments, c ->
    div(
        new UserBadge(c.getAuthor()),
        p(c.getText()),
        new Timestamp(c.getCreatedAt())
    )
))

Lifecycle Hooks

Templates support lifecycle hooks for data loading, cleanup, and page metadata.

beforeRender & afterRender

Server-side hooks for data loading and cleanup.

record User(String getName) {}
class UserService { User findById(int id) { return new User("Ada"); } }

public class UserPage implements Template {
    private final UserService userService;
    private User user;

    public UserPage(UserService userService) {
        this.userService = userService;
    }

    @Override
    public void beforeRender(Request request) {
        int userId = request.paramInt("id");
        this.user = userService.findById(userId);
    }

    @Override
    public void afterRender(Request request) {
        // Cleanup, logging, analytics
    }

    @Override
    public Element render() {
        return div(h1(user.getName()));
    }
}

Page Title & Meta

Dynamic page title and SEO metadata — plain Strings, null for the default.

record Product(String getName, String getDescription) {}
private Product product = new Product("Widget",
    "A very fine widget for all your widget needs. Built to last, designed " +
    "to delight, and priced to make your accountant smile every single quarter, " +
    "no matter how the market moves or the weather turns outside.");

@Override
public String pageTitle() {
    return product.getName() + " | Store";
}

@Override
public String description() {
    return product.getDescription().substring(0, 150);
}

Extra Head Elements

Add custom elements to the HTML head.

private String getTitle() { return "Home"; }
private String getImageUrl() { return "/og-image.png"; }

@Override
public Optional<Element> extraHead() {
    return Optional.of(fragment(
        meta("og:title", getTitle()),
        meta("og:image", getImageUrl()),
        link(attrs().rel("preconnect").href("https://fonts.googleapis.com")),
        css("/css/page.css")
    ));
}

Client-Side Lifecycle

Actions to run on DOM ready and cleanup.

@Override
public Action onMount() {
    return all(call("initCharts"), call("setupWebSocket"));
}

@Override
public Action onUnmount() {
    return all(call("closeWebSocket"), call("saveScrollPosition"));
}

Collected scripts

scripts() is collected the way styles() is: every Template rendered in a page contributes once, deduped by content, and the script rides swap fragments, streamed chunks and WebSocket patches. A component owns its behavior; the page never writes inlineScript().

public class CopyButton implements Template {
    static final Cls COPY = cls("copy");

    @Override
    public Element render() {
        return button(COPY, "Copy");
    }

    @Override
    public Optional<Action> scripts() {
        return Optional.of(actions().does(delegate(Selector.type("body"), "click", COPY)
            .handler(callback("e", "t").does(copyFrom(Selector.type("pre")).trigger(v("t"))))));
    }
}

Caching

Control response caching for performance.

private Object currentUser = null;

@Override
public boolean cacheable() {
    return currentUser == null;  // Only cache for anonymous users
}

@Override
public int cacheDuration() {
    return 3600;  // Cache for 1 hour
}
Use beforeRender for data loading, not render(). This keeps render() pure.