Skip to content
Brew

Brew templates

Brew renders HTML through Crema. Each component must produce a single root element. Brew stamps wire:snapshot, wire:effects, wire:id, and wire:name onto that root.

  1. template() — inline Crema on the class
  2. render() → view("brew.todos", data) or inline(source, data)
  3. Convention file src/main/resources/views/brew/<name>.crema.html when both are absent

Inside templates, public properties and @Computed methods are in scope as Crema data.

Surface Example
Tag <brew:counter :start="1" />
Directive @brew('counter', ['start' => 1])
Styles placeholder @brewStyles
Scripts @brewScripts
Script config only @brewScriptConfig (for manual ESM bundling)
JS encode @js($value)

When brew.inject_assets is true, responses that look like full HTML documents get styles + scripts injected automatically if those directives are missing.

URL Content
/brew/brew.js Readable bundle
/brew/brew.min.js Minified (+ source map)

Script tags include a cache-busting ?id= hash, data-csrf, and data-update-uri="/brew/update". Override paths with Brew.setUpdateRoute / Brew.setScriptRoute when you need a custom mount point (brew.path still names the default).

From Kotlin/Java:

Brew.styles()
Brew.scripts()
Brew.scriptConfig()

With brew.inject_morph_markers (default true), Crema wraps conditionals and loops in HTML comment markers so the keyed morph can add/remove blocks without tearing sibling DOM. Prefer wire:key on list items:

@foreach(items as item)
<li wire:key="item-{{ item.id }}">{{ item.title }}</li>
@endforeach
  • Keep interactive UI inside the component root; Brew morphs that subtree.
  • Use wire:ignore (.self / .children) for islands Brew must not touch (third-party widgets).
  • wire:replace forces a full replace instead of a morph when needed.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ title }}</title>
@brewStyles
</head>
<body>
<h1>{{ title }}</h1>
<brew:counter :start="3" />
<brew:todos />
@brew('search')
@brewScripts
</body>
</html>