| name | sui-development |
| description | Guide for developing web applications using Yao SUI framework. Use when building SUI pages, components, templates, handling data binding, event handling, backend scripts, routing, i18n, or integrating with CUI. Trigger when user mentions SUI, Yao web development, page templates, or frontend/backend integration in Yao applications. |
SUI Development Guide
SUI (Simple UI) is Yao's built-in web framework for building server-rendered pages with reactive components.
Core Concepts
- File-based routing - Directory structure defines URL routes
- Page = Component - Every page can be used as a component
- Server-side rendering - Templates rendered on server with data binding
- Progressive enhancement - Frontend scripts add interactivity
Directory Structure
/suis/<sui>/templates/<template>/
├── __document.html # Global document wrapper
├── __data.json # Global data ($global)
├── __assets/ # Static assets
├── __locales/ # i18n files
└── pages/
└── <page>/ # Route = folder name
├── <page>.html # Template
├── <page>.css # Styles (auto-scoped)
├── <page>.ts # Frontend script
├── <page>.json # Page data config
├── <page>.config # Page settings (guard, cache, SEO)
└── <page>.backend.ts # Server-side logic
Build Commands
yao sui build <sui> <template>
yao sui build <sui> <template> -D
yao sui watch <sui> <template>
yao sui trans <sui> <template>
Template Syntax
Data Interpolation
{{ name }}
{{ user.email }}
{{ title ?? 'Default' }}
{{ price * quantity }}
{{ count > 0 ? 'Yes' : 'No' }}
Conditionals
<div s:if="{{ isActive }}">Active</div>
<div s:elif="{{ isPending }}">Pending</div>
<div s:else>Unknown</div>
<div s:if="{{ isAdmin && isActive }}">Admin Panel</div>
Loops
<li s:for="{{ items }}" s:for-item="item" s:for-index="i">
{{ i + 1 }}. {{ item.name }}
</li>
<div s:for="{{ users }}" s:for-item="user" s:if="{{ user.active }}">
{{ user.name }}
</div>
Attribute Binding
<a href="{{ '/user/' + id }}">Link</a>
<button s:attr-disabled="{{ !valid }}">Submit</button>
<div class="base {{ isActive ? 'active' : '' }}">Content</div>
<div ...props></div>
Built-in Variables
| Variable | Description | Example |
|---|
$param | Route parameters | {{ $param.id }} |
$query | URL query params | {{ $query.search }} |
$payload | POST body | {{ $payload.name }} |
$global | Global data | {{ $global.title }} |
$theme | Current theme | {{ $theme }} |
$locale | Current locale | {{ $locale }} |
$auth | OAuth info (when guarded) | {{ $auth.user_id }} |
Built-in Functions
{{ P_('models.user.Find', id) }}
{{ True(user) }}
{{ Empty(items) }}
{{ len(items) }}
{{ filter(items, .active) }}
Data Binding
Page Data (<page>.json)
{
"title": "Static value",
"$users": "models.user.Get",
"$user": {
"process": "models.user.Find",
"args": ["$param.id"]
},
"$items": {
"process": "@GetItems",
"args": ["active", 10]
}
}
- Keys with
$ prefix call processes
@MethodName calls backend script functions
- Use
$param, $query, $payload in args
Backend Script (<page>.backend.ts)
There are three types of backend functions, each with different auth mechanisms:
interface AuthorizedInfo {
user_id?: string;
email?: string;
team_id?: string;
owner_id?: string;
}
declare function Authorized(): AuthorizedInfo | null;
function BeforeRender(request: Request): Record<string, any> {
const id = request.params.id;
return {
user: Process("models.user.Find", id),
items: Process("models.item.Get", { limit: 10 }),
};
}
function ApiGetUsers(): any[] {
const auth = Authorized();
if (!auth?.user_id) throw new Error("unauthorized");
return Process("models.user.Get", {
wheres: [{ column: "user_id", value: auth.user_id }],
});
}
function GetRecord(request: Request): any {
return Process("models.record.Find", request.params.id);
}
Important: $param is NOT available in backend scripts. Use request.params.
CRITICAL: ApiXxx functions called via $Backend().Call do NOT receive a request
parameter. Use the Authorized() global function for user identity. See
references/backend-api.md for the full explanation and Go runtime source references.
Components
Every page is a component. Use is attribute or <import> to embed:
<div is="/card" title="My Card">
<p>Content</p>
</div>
<import s:as="Card" s:from="/card" />
<Card title="My Card"><p>Content</p></Card>
Component Structure
/card/card.html:
<div class="card">
<h3>{{ title }}</h3>
<div class="body"><children></children></div>
</div>
Named Slots
<div class="modal">
<div class="header"><slot name="header"></slot></div>
<div class="body"><children></children></div>
<div class="footer"><slot name="footer"></slot></div>
</div>
<div is="/modal">
<slot name="header"><h2>Title</h2></slot>
<p>Body content</p>
<slot name="footer"><button>OK</button></slot>
</div>
Props Access
Frontend (card.ts):
import { Component } from "@yao/sui";
const self = this as Component;
const title = self.props.Get("title");
const allProps = self.props.List();
Backend (card.backend.ts):
function BeforeRender(request: Request, props: Record<string, any>): Record<string, any> {
const userId = props.userId;
return { user: Process("models.user.Find", userId) };
}
Event Handling
Event Binding
<button s:on-click="HandleClick">Click</button>
<input s:on-input="HandleInput" />
<form s:on-submit="HandleSubmit">...</form>
<button s:on-click="DeleteItem" s:data-id="{{ item.id }}" s:json-item="{{ item }}">
Delete
</button>
Event Handler
import { $Backend, Component, EventData } from "@yao/sui";
const self = this as Component;
self.DeleteItem = async (event: Event, data: EventData) => {
const id = data.id;
const item = data.item;
await $Backend().Call("DeleteItem", id);
(event.target as HTMLElement).closest(".item")?.remove();
};
self.HandleSubmit = async (event: Event) => {
event.preventDefault();
const form = event.target as HTMLFormElement;
const formData = new FormData(form);
await $Backend().Call("Submit", Object.fromEntries(formData));
};
State Management
const self = this as Component;
self.state.Set("count", 0);
self.watch = {
count: (value: number) => {
self.root.querySelector(".count")!.textContent = String(value);
},
};
self.Increment = () => {
const count = self.state.Get("count") || 0;
self.state.Set("count", count + 1);
};
Frontend API
Backend Calls
import { $Backend } from "@yao/sui";
const users = await $Backend().Call("GetUsers");
const user = await $Backend().Call("GetUser", 123);