- 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
1. **File-based routing** - Directory structure defines URL routes
2. **Page = Component** - Every page can be used as a component
3. **Server-side rendering** - Templates rendered on server with data binding
4. **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
```bash
yao sui build <sui> <template> # Build for production
yao sui build <sui> <template> -D # Development mode
yao sui watch <sui> <template> # Watch mode
yao sui trans <sui> <template> # Extract i18n strings
```
---
# Template Syntax
## Data Interpolation
```html
{{ name }} <!-- Variable -->
{{ user.email }} <!-- Nested -->
{{ title ?? 'Default' }} <!-- Default value -->
{{ price * quantity }} <!-- Expression -->
{{ count > 0 ? 'Yes' : 'No' }} <!-- Ternary -->
```
## Conditionals
```html
<div s:if="{{ isActive }}">Active</div>
<div s:elif="{{ isPending }}">Pending</div>
<div s:else>Unknown</div>
<!-- Operators: ==, !=, >, <, >=, <=, &&, ||, ! -->
<div s:if="{{ isAdmin && isActive }}">Admin Panel</div>
```
## Loops
```html
<li s:for="{{ items }}" s:for-item="item" s:for-index="i">
{{ i + 1 }}. {{ item.name }}
</li>
<!-- With conditional -->
<div s:for="{{ users }}" s:for-item="user" s:if="{{ user.active }}">
{{ user.name }}
</div>
```
## Attribute Binding
```html
<a href="{{ '/user/' + id }}">Link</a>
<button s:attr-disabled="{{ !valid }}">Submit</button>
<div class="base {{ isActive ? 'active' : '' }}">Content</div>
<div ...props></div> <!-- Spread -->
```
## 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
```html
{{ P_('models.user.Find', id) }} <!-- Call process -->
{{ True(user) }} <!-- Truthy check -->
{{ Empty(items) }} <!-- Empty check -->
{{ len(items) }} <!-- Array length -->
{{ filter(items, .active) }} <!-- Filter array -->
```
---
# Data Binding
## Page Data (`<page>.json`)
```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:
```typescript
// Type declarations for auth
interface AuthorizedInfo {
user_id?: string;
email?: string;
team_id?: string;
owner_id?: string;
}
declare function Authorized(): AuthorizedInfo | null;
// 1. BeforeRender — called before page render
// Auth: request.authorized (request is the first arg)
function BeforeRender(request: Request): Record<string, any> {
const id = request.params.id; // NOT $param.id
return {
user: Process("models.user.Find", id),
items: Process("models.item.Get", { limit: 10 }),
};
}
// 2. ApiXxx — called via $Backend().Call("GetUsers")
// Auth: Authorized() global function (request is NOT passed)
// Args: only what frontend sends
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 }],
});
}
// 3. @FuncName — called from .json data binding
// Auth: request.authorized (request is appended as last arg by SUI core)
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:
```html
<!-- Using is attribute -->
<div is="/card" title="My Card">
<p>Content</p>
</div>
<!-- Using import -->
<import s:as="Card" s:from="/card" />
<Card title="My Card"><p>Content</p></Card>
```
## Component Structure
**`/card/card.html`**:
```html
<div class="card">
<h3>{{ title }}</h3>
<div class="body"><children></children></div>
</div>
```
## Named Slots
```html
<!-- Component -->
<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>
<!-- Usage -->
<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`):
```typescript
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`):
```typescript
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
```html
<button s:on-click="HandleClick">Click</button>
<input s:on-input="HandleInput" />
<form s:on-submit="HandleSubmit">...</form>
<!-- With data -->
<button s:on-click="DeleteItem" s:data-id="{{ item.id }}" s:json-item="{{ item }}">
Delete
</button>
```
## Event Handler
```typescript
import { $Backend, Component, EventData } from "@yao/sui";
const self = this as Component;
self.DeleteItem = async (event: Event, data: EventData) => {
const id = data.id; // From s:data-id
const item = data.item; // From s:json-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
```typescript
const self = this as Component;
// Set state
self.state.Set("count", 0);
// Watch changes
self.watch = {
count: (value: number) => {
self.root.querySelector(".count")!.textContent = String(value);
},
};
// Trigger update
self.Increment = () => {
const count = self.state.Get("count") || 0;
self.state.Set("count", count + 1);
};
```
---
# Frontend API
## Backend Calls
```typescript
import { $Backend } from "@yao/sui";
// Calls ApiGetUsers() in backend script — no request param is passed
const users = await $Backend().Call("GetUsers");
// Calls ApiGetUser(123) — args are passed positionally
const user = await $Backend().Call("GetUser", 123);
在 GitHub 查看