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 emailBuilder 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
}