Views define UI pages. Templates provide custom HTML rendering. All views are automatically wrapped in a layout template with sidebar navigation, navbar, and modern styling.
{
"name": "order_list",
"type": "list",
"model": "order",
"title": "Sales Orders",
"fields": ["order_date", "customer_id", "total", "status"],
"filters": ["status", "order_date"],
"sort": { "field": "order_date", "order": "desc" },
"actions": [
{ "label": "Confirm", "process": "confirm_order", "permission": "order.confirm", "visible": "status == 'draft'" }
]
}{
"name": "order_form",
"type": "form",
"model": "order",
"title": "Sales Order",
"layout": [
{ "row": [
{ "field": "customer_id", "width": 6 },
{ "field": "order_date", "width": 3 },
{ "field": "status", "width": 3, "readonly": true }
]},
{ "tabs": [
{ "label": "Lines", "view": "order_line_list" },
{ "label": "Notes", "fields": ["notes"] }
]}
],
"actions": [
{ "label": "Confirm", "process": "confirm_order", "variant": "primary", "visible": "status == 'draft'" }
]
}{
"name": "lead_kanban",
"type": "kanban",
"model": "lead",
"title": "Lead Pipeline",
"group_by": "status",
"fields": ["name", "company", "expected_revenue"]
}{
"name": "dashboard",
"type": "custom",
"template": "templates/dashboard.html",
"data_sources": {
"orders": { "model": "order", "domain": [["status", "=", "confirmed"]] },
"stats": { "process": "get_stats" }
}
}All views rendered via /app/:module/* are automatically wrapped in a layout template. The layout provides:
- Sidebar — Module navigation with menu items from
module.json - Navbar — Breadcrumbs, user info, logout
- Content area — Where the view renders
- The view content (list, form, kanban, etc.) is rendered first
- The result is injected into
layout.htmlas{{.Content}} - Layout also receives
{{.Title}},{{.Module}},{{.Username}},{{.Menu}}
modules/base/templates/
├── layout.html # Main layout (sidebar + navbar + content)
├── partials/
│ ├── sidebar.html # Left sidebar navigation
│ ├── navbar.html # Top navigation bar
│ ├── pagination.html # Reusable pagination component
│ ├── status_badge.html # Status badge component
│ └── actions.html # Action buttons component
└── views/
├── list.html # List/table view template
├── form.html # Form view template
├── kanban.html # Kanban board template
├── calendar.html # Calendar view template
├── chart.html # Chart view template
├── login.html # Login page (standalone)
└── home.html # Home dashboard
Modules can override default templates by providing their own in templates/. Templates are loaded in module dependency order, so later modules override earlier ones.
Partials are shared across all templates. Any file in a partials/ subdirectory is automatically available:
{{template "templates/partials/status_badge.html" .status}}
{{template "templates/partials/pagination.html" .}}A view can register itself in another module's namespace using register_to:
{
"name": "customer_widget",
"type": "list",
"model": "customer",
"register_to": ["crm", "sales"]
}If the target module is not installed, the registration is silently skipped (no error).
Use module.view_name syntax in URLs:
/app/crm/sales.order_list → renders sales module's order_list view
Templates use Go html/template syntax.
<div class="dashboard">
<h1>Dashboard</h1>
{{range .orders}}
<div>{{.name}} - {{formatCurrency .total}}</div>
{{end}}
</div>| Helper | Usage | Description |
|---|---|---|
formatDate |
{{formatDate .date}} |
Format as YYYY-MM-DD |
formatDateTime |
{{formatDateTime .date}} |
Format as YYYY-MM-DD HH:MM |
formatCurrency |
{{formatCurrency .amount}} |
Format as $0.00 |
truncate |
{{truncate .text 50}} |
Truncate with ... |
upper |
{{upper .text}} |
Uppercase |
lower |
{{lower .text}} |
Lowercase |
title |
{{title .text}} |
Title case |
dict |
{{dict "key" "value"}} |
Create map |
eq |
{{if eq .status "active"}} |
Equality check |
neq |
{{if neq .status "draft"}} |
Inequality check |
safeHTML |
{{safeHTML .html}} |
Render raw HTML |
add |
{{add .page 1}} |
Integer addition |
sub |
{{sub .page 1}} |
Integer subtraction |
seq |
{{range seq 1 10}} |
Generate integer sequence |
join |
{{join .list ", "}} |
Join strings |
contains |
{{if contains .text "foo"}} |
String contains |
hasPrefix |
{{if hasPrefix .url "http"}} |
String prefix check |
default |
{{default "N/A" .value}} |
Default value if empty |
Views are accessible at GET /app/{module}/{view_name}:
curl http://localhost:8080/app/crm/lead_list
curl http://localhost:8080/app/sales/order_formAdd "render": "spa" to a view to swap SSR for a JSON envelope. The engine
loads the same data as SSR, then returns { view, data, meta } with
Content-Type: application/json. A client runtime (web or Flutter) consumes
the envelope and renders via the portable BcRenderer + ReactiveStore. SSR
remains the default and is fully backwards compatible.
See engine/docs/features/spa-mode.md for the full contract, mapping rules,
and test coverage.
| Option | Type | Description |
|---|---|---|
label |
string | Button text |
process |
string | Process to execute |
permission |
string | Required permission |
variant |
string | Button style: primary, danger |
visible |
string | Condition expression |
confirm |
string | Confirmation dialog text |
| Option | Type | Description |
|---|---|---|
register_to |
string[] | Register this view in other modules (graceful if missing) |