Update Documentation for Frontend API and Event Handling Enhancements
- Expanded the README to include CUI integration details in the Frontend API section, clarifying communication methods with the CUI host. - Added new examples for frontend scripting styles, including Direct and Component styles, to improve user understanding of event handling and form submissions. - Updated event handling documentation to reflect changes in handler signatures and data structures, enhancing clarity on event data usage. - Revised component documentation to standardize method naming conventions and improve consistency across examples.
This commit is contained in:
parent
e6778e83fd
commit
aa01ea216c
5 changed files with 485 additions and 279 deletions
|
|
@ -75,7 +75,7 @@ yao sui watch agent
|
|||
- [Data Binding](docs/data-binding.md) - Built-in variables and functions
|
||||
- [Event Handling](docs/event-handling.md) - Event binding and state management
|
||||
- [Internationalization](docs/i18n.md) - Translation and localization
|
||||
- [Frontend API](docs/frontend-api.md) - Component query, backend calls, render API
|
||||
- [Frontend API](docs/frontend-api.md) - Component query, backend calls, render API, CUI integration
|
||||
- [Agent SUI](docs/agent-sui.md) - AI Agent application setup
|
||||
|
||||
## Agent SUI
|
||||
|
|
|
|||
|
|
@ -237,16 +237,94 @@ Use standard SUI template syntax:
|
|||
|
||||
## Frontend Script
|
||||
|
||||
Frontend scripts can be written in two styles:
|
||||
|
||||
### Direct Style (Simple Pages)
|
||||
|
||||
```typescript
|
||||
// Runs immediately when script loads
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const form = document.querySelector("#myForm") as HTMLFormElement;
|
||||
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
// Handle submission
|
||||
});
|
||||
});
|
||||
|
||||
// Smooth scrolling
|
||||
document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
|
||||
anchor.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
const target = document.querySelector(this.getAttribute("href"));
|
||||
target?.scrollIntoView({ behavior: "smooth" });
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Component Style (Interactive Pages)
|
||||
|
||||
**`/assistants/demo/pages/index/index.ts`**:
|
||||
|
||||
```typescript
|
||||
function index(component: HTMLElement) {
|
||||
this.root = component;
|
||||
this.store = new __sui_store(component);
|
||||
import { $Backend, Component, EventData } from "@yao/sui";
|
||||
|
||||
this.handleClick = async (event: Event) => {
|
||||
const data = await this.backend.ApiGetData();
|
||||
console.log(data);
|
||||
};
|
||||
}
|
||||
const self = this as Component;
|
||||
|
||||
// Event handler bound to s:on-click="HandleClick"
|
||||
self.HandleClick = async (event: Event, data: EventData) => {
|
||||
const result = await $Backend().Call("ApiGetData", data.id);
|
||||
console.log(result);
|
||||
};
|
||||
|
||||
// Form submission
|
||||
self.HandleSubmit = async (event: Event) => {
|
||||
event.preventDefault();
|
||||
const form = event.target as HTMLFormElement;
|
||||
const formData = new FormData(form);
|
||||
await $Backend().Call("ApiSubmit", Object.fromEntries(formData));
|
||||
};
|
||||
```
|
||||
|
||||
## CUI Integration
|
||||
|
||||
When Agent SUI pages are embedded in CUI via `/web/` routes, they can communicate with the CUI host.
|
||||
|
||||
### Receiving Context
|
||||
|
||||
```typescript
|
||||
window.addEventListener("message", (e) => {
|
||||
if (e.origin !== window.location.origin) return;
|
||||
|
||||
if (e.data.type === "setup") {
|
||||
const { theme, locale } = e.data.message;
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Sending Actions
|
||||
|
||||
```typescript
|
||||
// Helper function
|
||||
const sendAction = (name: string, payload?: any) => {
|
||||
window.parent.postMessage(
|
||||
{ type: "action", message: { name, payload } },
|
||||
window.location.origin
|
||||
);
|
||||
};
|
||||
|
||||
// Show notification
|
||||
sendAction("notify.success", { message: "Done!" });
|
||||
|
||||
// Navigate
|
||||
sendAction("navigate", {
|
||||
route: "/agents/demo/detail",
|
||||
title: "Details",
|
||||
});
|
||||
|
||||
// Close sidebar
|
||||
sendAction("event.emit", { key: "app/closeSidebar", value: {} });
|
||||
```
|
||||
|
||||
See [Frontend API - CUI Integration](frontend-api.md#cui-integration) for complete documentation.
|
||||
|
|
|
|||
|
|
@ -203,75 +203,68 @@ Use `<slot name="xxx">` for multiple content areas:
|
|||
### Structure
|
||||
|
||||
```typescript
|
||||
function componentName(component: HTMLElement) {
|
||||
// Root element
|
||||
this.root = component;
|
||||
import { $Backend, Component, EventData } from "@yao/sui";
|
||||
|
||||
// Data store (data-* attributes)
|
||||
this.store = new __sui_store(component);
|
||||
const self = this as Component;
|
||||
|
||||
// Props (passed attributes)
|
||||
this.props = new __sui_props(component);
|
||||
// self.root - Root element (HTMLElement)
|
||||
// self.store - Data store (data-* attributes)
|
||||
// self.props - Props (passed attributes)
|
||||
// self.state - State management
|
||||
|
||||
// State management
|
||||
this.state = new __sui_state(this);
|
||||
// State watchers
|
||||
self.watch = {
|
||||
propertyName: (value: any, state: any) => {
|
||||
// React to state changes
|
||||
},
|
||||
};
|
||||
|
||||
// Backend API
|
||||
this.backend = {
|
||||
ApiMethod: async (...args) => {
|
||||
/* ... */
|
||||
},
|
||||
};
|
||||
|
||||
// State watchers
|
||||
this.watch = {
|
||||
propertyName: (value, state) => {
|
||||
// React to state changes
|
||||
},
|
||||
};
|
||||
|
||||
// Methods
|
||||
this.handleClick = (event, data, context) => {
|
||||
// Handle events
|
||||
};
|
||||
}
|
||||
// Event handlers (bound to s:on-click="HandleClick")
|
||||
self.HandleClick = async (event: Event, data: EventData) => {
|
||||
const result = await $Backend().Call("ApiMethod", data.id);
|
||||
// Handle result
|
||||
};
|
||||
```
|
||||
|
||||
### Store API
|
||||
|
||||
```typescript
|
||||
import { Component } from "@yao/sui";
|
||||
|
||||
const self = this as Component;
|
||||
|
||||
// String data
|
||||
this.store.Get("key");
|
||||
this.store.Set("key", "value");
|
||||
self.store.Get("key");
|
||||
self.store.Set("key", "value");
|
||||
|
||||
// JSON data
|
||||
this.store.GetJSON("items");
|
||||
this.store.SetJSON("items", [{ id: 1 }]);
|
||||
self.store.GetJSON("items");
|
||||
self.store.SetJSON("items", [{ id: 1 }]);
|
||||
|
||||
// Component data (from BeforeRender)
|
||||
this.store.GetData();
|
||||
self.store.GetData();
|
||||
```
|
||||
|
||||
### Props API
|
||||
|
||||
```typescript
|
||||
// Get single prop
|
||||
const value = this.props.Get("propName");
|
||||
const value = self.props.Get("propName");
|
||||
|
||||
// Get all props
|
||||
const props = this.props.List();
|
||||
const props = self.props.List();
|
||||
```
|
||||
|
||||
### State API
|
||||
|
||||
```typescript
|
||||
// Set state (triggers watchers)
|
||||
this.state.Set("count", 10);
|
||||
self.state.Set("count", 10);
|
||||
|
||||
// Watch state changes
|
||||
this.watch = {
|
||||
count: (value, state) => {
|
||||
this.root.querySelector(".count").textContent = value;
|
||||
self.watch = {
|
||||
count: (value: number, state: any) => {
|
||||
self.root.querySelector(".count")!.textContent = String(value);
|
||||
// state.stopPropagation(); // Prevent bubbling to parent
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -76,23 +76,21 @@ Use `s:json-*` to pass complex data:
|
|||
### Handler Signature
|
||||
|
||||
```typescript
|
||||
function Page(component: HTMLElement) {
|
||||
this.root = component;
|
||||
import { $Backend, Component, EventData } from "@yao/sui";
|
||||
|
||||
this.handleClick = (event: Event, data: any, context: EventContext) => {
|
||||
// event - The DOM event
|
||||
// data - Combined data from s:data-* and s:json-*
|
||||
// context - Event context with element references
|
||||
};
|
||||
}
|
||||
const self = this as Component;
|
||||
|
||||
self.HandleClick = (event: Event, data: EventData) => {
|
||||
// event - The DOM event
|
||||
// data - Combined data from s:data-* and s:json-*
|
||||
};
|
||||
```
|
||||
|
||||
### EventContext
|
||||
### EventData
|
||||
|
||||
```typescript
|
||||
interface EventContext {
|
||||
rootElement: HTMLElement; // Component root element
|
||||
targetElement: HTMLElement; // Element that triggered the event
|
||||
interface EventData {
|
||||
[key: string]: any; // Data from s:data-* and s:json-* attributes
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -103,7 +101,7 @@ interface EventContext {
|
|||
<div s:for="{{ items }}" s:for-item="item">
|
||||
<span>{{ item.name }}</span>
|
||||
<button
|
||||
s:on-click="deleteItem"
|
||||
s:on-click="DeleteItem"
|
||||
s:data-id="{{ item.id }}"
|
||||
s:json-item="{{ item }}"
|
||||
>
|
||||
|
|
@ -114,19 +112,19 @@ interface EventContext {
|
|||
```
|
||||
|
||||
```typescript
|
||||
function ItemList(component: HTMLElement) {
|
||||
this.root = component;
|
||||
import { $Backend, Component, EventData } from "@yao/sui";
|
||||
|
||||
this.deleteItem = async (event: Event, data: any, context: EventContext) => {
|
||||
const id = data.id; // String from s:data-id
|
||||
const item = data.item; // Object from s:json-item
|
||||
const self = this as Component;
|
||||
|
||||
if (confirm(`Delete ${item.name}?`)) {
|
||||
await this.backend.ApiDeleteItem(id);
|
||||
context.targetElement.closest(".item").remove();
|
||||
}
|
||||
};
|
||||
}
|
||||
self.DeleteItem = async (event: Event, data: EventData) => {
|
||||
const id = data.id; // String from s:data-id
|
||||
const item = data.item; // Object from s:json-item
|
||||
|
||||
if (confirm(`Delete ${item.name}?`)) {
|
||||
await $Backend().Call("ApiDeleteItem", id);
|
||||
(event.target as HTMLElement).closest(".item")?.remove();
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## State Management
|
||||
|
|
@ -134,13 +132,12 @@ function ItemList(component: HTMLElement) {
|
|||
### State Object
|
||||
|
||||
```typescript
|
||||
function Counter(component: HTMLElement) {
|
||||
this.root = component;
|
||||
this.state = new __sui_state(this);
|
||||
import { Component } from "@yao/sui";
|
||||
|
||||
// Initial state
|
||||
this.state.Set("count", 0);
|
||||
}
|
||||
const self = this as Component;
|
||||
|
||||
// Initial state
|
||||
self.state.Set("count", 0);
|
||||
```
|
||||
|
||||
### State Watchers
|
||||
|
|
@ -148,26 +145,25 @@ function Counter(component: HTMLElement) {
|
|||
React to state changes with watchers:
|
||||
|
||||
```typescript
|
||||
function Counter(component: HTMLElement) {
|
||||
this.root = component;
|
||||
this.state = new __sui_state(this);
|
||||
import { Component } from "@yao/sui";
|
||||
|
||||
// Define watchers
|
||||
this.watch = {
|
||||
count: (value: number, state: State) => {
|
||||
this.root.querySelector(".count").textContent = value;
|
||||
},
|
||||
const self = this as Component;
|
||||
|
||||
items: (value: any[], state: State) => {
|
||||
this.renderItems(value);
|
||||
},
|
||||
};
|
||||
// Define watchers
|
||||
self.watch = {
|
||||
count: (value: number) => {
|
||||
self.root.querySelector(".count")!.textContent = String(value);
|
||||
},
|
||||
|
||||
this.increment = () => {
|
||||
const count = this.state.Get("count") || 0;
|
||||
this.state.Set("count", count + 1); // Triggers watcher
|
||||
};
|
||||
}
|
||||
items: (value: any[]) => {
|
||||
renderItems(value);
|
||||
},
|
||||
};
|
||||
|
||||
self.Increment = () => {
|
||||
const count = self.state.Get("count") || 0;
|
||||
self.state.Set("count", count + 1); // Triggers watcher
|
||||
};
|
||||
```
|
||||
|
||||
### Stop Propagation
|
||||
|
|
@ -175,10 +171,10 @@ function Counter(component: HTMLElement) {
|
|||
Prevent state changes from bubbling to parent:
|
||||
|
||||
```typescript
|
||||
this.watch = {
|
||||
localState: (value: any, state: State) => {
|
||||
self.watch = {
|
||||
localState: (value: any, state: any) => {
|
||||
// Handle locally
|
||||
this.updateUI(value);
|
||||
updateUI(value);
|
||||
|
||||
// Stop propagation to parent components
|
||||
state.stopPropagation();
|
||||
|
|
@ -193,18 +189,17 @@ Store manages `data-*` attributes on the component:
|
|||
### Basic Usage
|
||||
|
||||
```typescript
|
||||
function Card(component: HTMLElement) {
|
||||
this.root = component;
|
||||
this.store = new __sui_store(component);
|
||||
import { Component } from "@yao/sui";
|
||||
|
||||
// Get/Set string values
|
||||
const id = this.store.Get("id");
|
||||
this.store.Set("id", "123");
|
||||
const self = this as Component;
|
||||
|
||||
// Get/Set JSON values
|
||||
const items = this.store.GetJSON("items");
|
||||
this.store.SetJSON("items", [{ id: 1 }, { id: 2 }]);
|
||||
}
|
||||
// Get/Set string values
|
||||
const id = self.store.Get("id");
|
||||
self.store.Set("id", "123");
|
||||
|
||||
// Get/Set JSON values
|
||||
const items = self.store.GetJSON("items");
|
||||
self.store.SetJSON("items", [{ id: 1 }, { id: 2 }]);
|
||||
```
|
||||
|
||||
### Component Data
|
||||
|
|
@ -213,7 +208,7 @@ Get data from BeforeRender:
|
|||
|
||||
```typescript
|
||||
// Backend returns: { user: { name: "John" }, settings: {...} }
|
||||
const data = this.store.GetData();
|
||||
const data = self.store.GetData();
|
||||
console.log(data.user.name); // "John"
|
||||
```
|
||||
|
||||
|
|
@ -222,30 +217,30 @@ console.log(data.user.name); // "John"
|
|||
### Emit Events
|
||||
|
||||
```typescript
|
||||
function ItemCard(component: HTMLElement) {
|
||||
this.root = component;
|
||||
import { Component } from "@yao/sui";
|
||||
|
||||
this.selectItem = () => {
|
||||
const item = this.store.GetJSON("item");
|
||||
const self = this as Component;
|
||||
|
||||
// Emit custom event
|
||||
this.emit("item:selected", { item });
|
||||
};
|
||||
}
|
||||
self.SelectItem = () => {
|
||||
const item = self.store.GetJSON("item");
|
||||
|
||||
// Emit custom event
|
||||
self.emit("item:selected", { item });
|
||||
};
|
||||
```
|
||||
|
||||
### Listen to Events
|
||||
|
||||
```typescript
|
||||
function ItemList(component: HTMLElement) {
|
||||
this.root = component;
|
||||
import { Component } from "@yao/sui";
|
||||
|
||||
// Listen to child events
|
||||
this.root.addEventListener("item:selected", (e: CustomEvent) => {
|
||||
const { item } = e.detail;
|
||||
console.log("Selected:", item);
|
||||
});
|
||||
}
|
||||
const self = this as Component;
|
||||
|
||||
// Listen to child events
|
||||
self.root.addEventListener("item:selected", (e: CustomEvent) => {
|
||||
const { item } = e.detail;
|
||||
console.log("Selected:", item);
|
||||
});
|
||||
```
|
||||
|
||||
### State Change Events
|
||||
|
|
@ -253,14 +248,14 @@ function ItemList(component: HTMLElement) {
|
|||
Parent components can listen to state changes:
|
||||
|
||||
```typescript
|
||||
function Parent(component: HTMLElement) {
|
||||
this.root = component;
|
||||
import { Component } from "@yao/sui";
|
||||
|
||||
this.root.addEventListener("state:change", (e: CustomEvent) => {
|
||||
const { key, value, target } = e.detail;
|
||||
console.log(`State ${key} changed to ${value} in`, target);
|
||||
});
|
||||
}
|
||||
const self = this as Component;
|
||||
|
||||
self.root.addEventListener("state:change", (e: CustomEvent) => {
|
||||
const { key, value, target } = e.detail;
|
||||
console.log(`State ${key} changed to ${value} in`, target);
|
||||
});
|
||||
```
|
||||
|
||||
## Form Handling
|
||||
|
|
@ -268,7 +263,7 @@ function Parent(component: HTMLElement) {
|
|||
### Form Submit
|
||||
|
||||
```html
|
||||
<form s:on-submit="handleSubmit">
|
||||
<form s:on-submit="HandleSubmit">
|
||||
<input name="email" type="email" required />
|
||||
<input name="password" type="password" required />
|
||||
<button type="submit">Login</button>
|
||||
|
|
@ -276,77 +271,77 @@ function Parent(component: HTMLElement) {
|
|||
```
|
||||
|
||||
```typescript
|
||||
function LoginForm(component: HTMLElement) {
|
||||
this.root = component;
|
||||
import { $Backend, Component } from "@yao/sui";
|
||||
|
||||
this.handleSubmit = async (event: Event) => {
|
||||
event.preventDefault();
|
||||
const self = this as Component;
|
||||
|
||||
const form = event.target as HTMLFormElement;
|
||||
const formData = new FormData(form);
|
||||
self.HandleSubmit = async (event: Event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const email = formData.get("email");
|
||||
const password = formData.get("password");
|
||||
const form = event.target as HTMLFormElement;
|
||||
const formData = new FormData(form);
|
||||
|
||||
try {
|
||||
await this.backend.ApiLogin(email, password);
|
||||
window.location.href = "/dashboard";
|
||||
} catch (error) {
|
||||
alert("Login failed");
|
||||
}
|
||||
};
|
||||
}
|
||||
const email = formData.get("email");
|
||||
const password = formData.get("password");
|
||||
|
||||
try {
|
||||
await $Backend().Call("ApiLogin", email, password);
|
||||
window.location.href = "/dashboard";
|
||||
} catch (error) {
|
||||
alert("Login failed");
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Input Binding
|
||||
|
||||
```html
|
||||
<input type="text" s:on-input="handleInput" s:data-field="name" />
|
||||
<input type="text" s:on-input="HandleInput" s:data-field="name" />
|
||||
```
|
||||
|
||||
```typescript
|
||||
function Form(component: HTMLElement) {
|
||||
this.root = component;
|
||||
this.formData = {};
|
||||
import { Component, EventData } from "@yao/sui";
|
||||
|
||||
this.handleInput = (event: Event, data: any) => {
|
||||
const input = event.target as HTMLInputElement;
|
||||
this.formData[data.field] = input.value;
|
||||
};
|
||||
}
|
||||
const self = this as Component;
|
||||
const formData: Record<string, string> = {};
|
||||
|
||||
self.HandleInput = (event: Event, data: EventData) => {
|
||||
const input = event.target as HTMLInputElement;
|
||||
formData[data.field] = input.value;
|
||||
};
|
||||
```
|
||||
|
||||
## Keyboard Events
|
||||
|
||||
```html
|
||||
<input s:on-keydown="handleKeydown" s:on-keyup="handleKeyup" />
|
||||
<input s:on-keydown="HandleKeydown" s:on-keyup="HandleKeyup" />
|
||||
```
|
||||
|
||||
```typescript
|
||||
function Search(component: HTMLElement) {
|
||||
this.root = component;
|
||||
import { Component } from "@yao/sui";
|
||||
|
||||
this.handleKeydown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Enter") {
|
||||
this.search();
|
||||
}
|
||||
const self = this as Component;
|
||||
|
||||
if (event.key === "Escape") {
|
||||
this.clear();
|
||||
}
|
||||
};
|
||||
}
|
||||
self.HandleKeydown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Enter") {
|
||||
search();
|
||||
}
|
||||
|
||||
if (event.key === "Escape") {
|
||||
clear();
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
```html
|
||||
<div class="todo-app">
|
||||
<form s:on-submit="addTodo">
|
||||
<form s:on-submit="AddTodo">
|
||||
<input
|
||||
name="title"
|
||||
placeholder="Add todo..."
|
||||
s:on-keydown="handleKeydown"
|
||||
s:on-keydown="HandleKeydown"
|
||||
/>
|
||||
<button type="submit">Add</button>
|
||||
</form>
|
||||
|
|
@ -355,53 +350,51 @@ function Search(component: HTMLElement) {
|
|||
<li s:for="{{ todos }}" s:for-item="todo">
|
||||
<input
|
||||
type="checkbox"
|
||||
s:on-change="toggleTodo"
|
||||
s:on-change="ToggleTodo"
|
||||
s:data-id="{{ todo.id }}"
|
||||
s:attr-checked="{{ todo.completed }}"
|
||||
/>
|
||||
<span class="{{ todo.completed ? 'completed' : '' }}">
|
||||
{{ todo.title }}
|
||||
</span>
|
||||
<button s:on-click="deleteTodo" s:data-id="{{ todo.id }}">×</button>
|
||||
<button s:on-click="DeleteTodo" s:data-id="{{ todo.id }}">×</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
```
|
||||
|
||||
```typescript
|
||||
function TodoApp(component: HTMLElement) {
|
||||
this.root = component;
|
||||
this.state = new __sui_state(this);
|
||||
this.store = new __sui_store(component);
|
||||
import { $Backend, Component, EventData } from "@yao/sui";
|
||||
|
||||
this.watch = {
|
||||
todos: (todos: any[]) => {
|
||||
this.render("todoList", { todos });
|
||||
},
|
||||
};
|
||||
const self = this as Component;
|
||||
|
||||
this.addTodo = async (event: Event) => {
|
||||
event.preventDefault();
|
||||
const form = event.target as HTMLFormElement;
|
||||
const input = form.querySelector("input") as HTMLInputElement;
|
||||
self.watch = {
|
||||
todos: (todos: any[]) => {
|
||||
self.render("todoList", { todos });
|
||||
},
|
||||
};
|
||||
|
||||
if (input.value.trim()) {
|
||||
const todo = await this.backend.ApiAddTodo(input.value);
|
||||
const todos = this.state.Get("todos") || [];
|
||||
this.state.Set("todos", [...todos, todo]);
|
||||
input.value = "";
|
||||
}
|
||||
};
|
||||
self.AddTodo = async (event: Event) => {
|
||||
event.preventDefault();
|
||||
const form = event.target as HTMLFormElement;
|
||||
const input = form.querySelector("input") as HTMLInputElement;
|
||||
|
||||
this.toggleTodo = async (event: Event, data: any) => {
|
||||
const checkbox = event.target as HTMLInputElement;
|
||||
await this.backend.ApiToggleTodo(data.id, checkbox.checked);
|
||||
};
|
||||
if (input.value.trim()) {
|
||||
const todo = await $Backend().Call("ApiAddTodo", input.value);
|
||||
const todos = self.state.Get("todos") || [];
|
||||
self.state.Set("todos", [...todos, todo]);
|
||||
input.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
this.deleteTodo = async (event: Event, data: any) => {
|
||||
await this.backend.ApiDeleteTodo(data.id);
|
||||
const todos = this.state.Get("todos").filter((t) => t.id !== data.id);
|
||||
this.state.Set("todos", todos);
|
||||
};
|
||||
}
|
||||
self.ToggleTodo = async (event: Event, data: EventData) => {
|
||||
const checkbox = event.target as HTMLInputElement;
|
||||
await $Backend().Call("ApiToggleTodo", data.id, checkbox.checked);
|
||||
};
|
||||
|
||||
self.DeleteTodo = async (event: Event, data: EventData) => {
|
||||
await $Backend().Call("ApiDeleteTodo", data.id);
|
||||
const todos = self.state.Get("todos").filter((t: any) => t.id !== data.id);
|
||||
self.state.Set("todos", todos);
|
||||
};
|
||||
```
|
||||
|
|
|
|||
|
|
@ -38,19 +38,15 @@ const items = component.queryAll(".item"); // Returns NodeList
|
|||
|
||||
## Backend Calls
|
||||
|
||||
### Via Component
|
||||
### Via $Backend
|
||||
|
||||
```typescript
|
||||
function Page(component: HTMLElement) {
|
||||
this.root = component;
|
||||
import { $Backend } from "@yao/sui";
|
||||
|
||||
this.loadData = async () => {
|
||||
// Call backend API methods
|
||||
const users = await this.backend.ApiGetUsers();
|
||||
const user = await this.backend.ApiGetUser(123);
|
||||
const result = await this.backend.ApiCreateUser("John", "john@example.com");
|
||||
};
|
||||
}
|
||||
// Call backend API methods
|
||||
const users = await $Backend().Call("ApiGetUsers");
|
||||
const user = await $Backend().Call("ApiGetUser", 123);
|
||||
const result = await $Backend().Call("ApiCreateUser", "John", "john@example.com");
|
||||
```
|
||||
|
||||
### Direct Call
|
||||
|
|
@ -80,22 +76,22 @@ Define render targets in HTML:
|
|||
### Render Method
|
||||
|
||||
```typescript
|
||||
function Page(component: HTMLElement) {
|
||||
this.root = component;
|
||||
import { $Backend, Component } from "@yao/sui";
|
||||
|
||||
this.refreshUsers = async () => {
|
||||
const users = await this.backend.ApiGetUsers();
|
||||
const self = this as Component;
|
||||
|
||||
// Render with data
|
||||
await this.render("userList", { users });
|
||||
};
|
||||
}
|
||||
self.RefreshUsers = async () => {
|
||||
const users = await $Backend().Call("ApiGetUsers");
|
||||
|
||||
// Render with data
|
||||
await self.render("userList", { users });
|
||||
};
|
||||
```
|
||||
|
||||
### Render Options
|
||||
|
||||
```typescript
|
||||
await this.render("targetName", data, {
|
||||
await self.render("targetName", data, {
|
||||
replace: true, // Replace content (default: true)
|
||||
showLoader: true, // Show loading indicator
|
||||
withPageData: true, // Include page data in render context
|
||||
|
|
@ -292,32 +288,32 @@ api.ClearTokens();
|
|||
### Emit
|
||||
|
||||
```typescript
|
||||
function Card(component: HTMLElement) {
|
||||
this.root = component;
|
||||
import { Component } from "@yao/sui";
|
||||
|
||||
this.select = () => {
|
||||
this.emit("card:selected", { id: this.store.Get("id") });
|
||||
};
|
||||
}
|
||||
const self = this as Component;
|
||||
|
||||
self.Select = () => {
|
||||
self.emit("card:selected", { id: self.store.Get("id") });
|
||||
};
|
||||
```
|
||||
|
||||
### Listen
|
||||
|
||||
```typescript
|
||||
function CardList(component: HTMLElement) {
|
||||
this.root = component;
|
||||
import { Component } from "@yao/sui";
|
||||
|
||||
this.root.addEventListener("card:selected", (e: CustomEvent) => {
|
||||
console.log("Selected:", e.detail.id);
|
||||
});
|
||||
}
|
||||
const self = this as Component;
|
||||
|
||||
self.root.addEventListener("card:selected", (e: CustomEvent) => {
|
||||
console.log("Selected:", e.detail.id);
|
||||
});
|
||||
```
|
||||
|
||||
### State Change Events
|
||||
|
||||
```typescript
|
||||
// Listen to child state changes
|
||||
this.root.addEventListener("state:change", (e: CustomEvent) => {
|
||||
self.root.addEventListener("state:change", (e: CustomEvent) => {
|
||||
const { key, value, target } = e.detail;
|
||||
console.log(`${key} = ${value}`);
|
||||
});
|
||||
|
|
@ -326,54 +322,200 @@ this.root.addEventListener("state:change", (e: CustomEvent) => {
|
|||
## Complete Example
|
||||
|
||||
```typescript
|
||||
function UserDashboard(component: HTMLElement) {
|
||||
this.root = component;
|
||||
this.store = new __sui_store(component);
|
||||
this.state = new __sui_state(this);
|
||||
import { $Backend, Component, EventData } from "@yao/sui";
|
||||
|
||||
// Initialize API
|
||||
const api = new OpenAPI({ baseURL: "/api" });
|
||||
const fileApi = new FileAPI(api);
|
||||
const self = this as Component;
|
||||
|
||||
// State watchers
|
||||
this.watch = {
|
||||
users: (users) => this.render("userList", { users }),
|
||||
loading: (loading) => {
|
||||
this.root.classList.toggle("loading", loading);
|
||||
},
|
||||
};
|
||||
// Initialize API
|
||||
const api = new OpenAPI({ baseURL: "/api" });
|
||||
const fileApi = new FileAPI(api);
|
||||
|
||||
// Load users
|
||||
this.loadUsers = async () => {
|
||||
this.state.Set("loading", true);
|
||||
// State watchers
|
||||
self.watch = {
|
||||
users: (users: any[]) => self.render("userList", { users }),
|
||||
loading: (loading: boolean) => {
|
||||
self.root.classList.toggle("loading", loading);
|
||||
},
|
||||
};
|
||||
|
||||
const response = await api.Get<User[]>("/users");
|
||||
if (!api.IsError(response)) {
|
||||
this.state.Set("users", response.data);
|
||||
}
|
||||
// Load users
|
||||
async function loadUsers() {
|
||||
self.state.Set("loading", true);
|
||||
|
||||
this.state.Set("loading", false);
|
||||
};
|
||||
const response = await api.Get<User[]>("/users");
|
||||
if (!api.IsError(response)) {
|
||||
self.state.Set("users", response.data);
|
||||
}
|
||||
|
||||
// Create user
|
||||
this.createUser = async (event: Event, data: any) => {
|
||||
const response = await this.backend.ApiCreateUser(data.name, data.email);
|
||||
const users = this.state.Get("users");
|
||||
this.state.Set("users", [...users, response]);
|
||||
};
|
||||
|
||||
// Upload avatar
|
||||
this.uploadAvatar = async (event: Event) => {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const file = input.files[0];
|
||||
|
||||
const response = await fileApi.Upload(file, { path: "avatars" });
|
||||
if (!api.IsError(response)) {
|
||||
this.emit("avatar:uploaded", { url: response.data.url });
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize
|
||||
this.loadUsers();
|
||||
self.state.Set("loading", false);
|
||||
}
|
||||
|
||||
// Create user
|
||||
self.CreateUser = async (event: Event, data: EventData) => {
|
||||
const response = await $Backend().Call("ApiCreateUser", data.name, data.email);
|
||||
const users = self.state.Get("users");
|
||||
self.state.Set("users", [...users, response]);
|
||||
};
|
||||
|
||||
// Upload avatar
|
||||
self.UploadAvatar = async (event: Event) => {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const file = input.files![0];
|
||||
|
||||
const response = await fileApi.Upload(file, { path: "avatars" });
|
||||
if (!api.IsError(response)) {
|
||||
self.emit("avatar:uploaded", { url: response.data.url });
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize
|
||||
loadUsers();
|
||||
```
|
||||
|
||||
## CUI Integration
|
||||
|
||||
When SUI pages are embedded in CUI via `/web/` routes, they can communicate with the CUI host.
|
||||
|
||||
### URL Parameters
|
||||
|
||||
CUI automatically replaces special parameter values:
|
||||
|
||||
| Value | Replaced With |
|
||||
| ---------- | -------------------------------- |
|
||||
| `__theme` | Current theme (`light` / `dark`) |
|
||||
| `__locale` | Current locale (e.g., `en-us`) |
|
||||
|
||||
> **Note**: Authentication uses secure HTTP-only cookies, no token parameter needed.
|
||||
|
||||
### Receiving Messages from CUI
|
||||
|
||||
```typescript
|
||||
window.addEventListener("message", (e) => {
|
||||
// Only accept messages from same origin
|
||||
if (e.origin !== window.location.origin) return;
|
||||
|
||||
const { type, message } = e.data;
|
||||
switch (type) {
|
||||
case "setup":
|
||||
// Initial context from CUI
|
||||
document.documentElement.setAttribute("data-theme", message.theme);
|
||||
console.log("Locale:", message.locale);
|
||||
break;
|
||||
case "update":
|
||||
// Data updates from CUI
|
||||
handleUpdate(message);
|
||||
break;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Sending Actions to CUI
|
||||
|
||||
Use the unified Action system to trigger CUI operations:
|
||||
|
||||
```typescript
|
||||
// Helper function
|
||||
const sendAction = (name: string, payload?: any) => {
|
||||
window.parent.postMessage(
|
||||
{ type: "action", message: { name, payload } },
|
||||
window.location.origin
|
||||
);
|
||||
};
|
||||
|
||||
// Show notification
|
||||
sendAction("notify.success", { message: "Operation completed!" });
|
||||
sendAction("notify.error", { message: "Something went wrong" });
|
||||
|
||||
// Navigate to page
|
||||
sendAction("navigate", {
|
||||
route: "/agents/my-app/detail",
|
||||
title: "Details",
|
||||
query: { id: "123" },
|
||||
});
|
||||
|
||||
// Open in new tab
|
||||
sendAction("navigate", {
|
||||
route: "/agents/my-app/report",
|
||||
target: "_blank",
|
||||
});
|
||||
|
||||
// Refresh menu
|
||||
sendAction("app.menu.reload");
|
||||
|
||||
// Close sidebar
|
||||
sendAction("event.emit", { key: "app/closeSidebar", value: {} });
|
||||
```
|
||||
|
||||
### Available Actions
|
||||
|
||||
| Category | Action | Description | Payload |
|
||||
| -------- | ----------------- | ------------------------- | ------------------------------------------- |
|
||||
| Navigate | `navigate` | Open page in sidebar/tab | `{ route, title?, icon?, query?, target? }` |
|
||||
| | `navigate.back` | Go back in history | - |
|
||||
| Notify | `notify.success` | Success notification | `{ message, duration?, closable? }` |
|
||||
| | `notify.error` | Error notification | `{ message, duration?, closable? }` |
|
||||
| | `notify.warning` | Warning notification | `{ message, duration?, closable? }` |
|
||||
| | `notify.info` | Info notification | `{ message, duration?, closable? }` |
|
||||
| App | `app.menu.reload` | Refresh application menu | - |
|
||||
| Modal | `modal.open` | Open modal dialog | `{ ... }` |
|
||||
| | `modal.close` | Close modal | - |
|
||||
| Table | `table.search` | Trigger table search | `{ keywords }` |
|
||||
| | `table.refresh` | Refresh table data | - |
|
||||
| Form | `form.submit` | Submit form | - |
|
||||
| | `form.reset` | Reset form | - |
|
||||
| Event | `event.emit` | Emit custom event | `{ key, value }` |
|
||||
| Confirm | `confirm` | Show confirmation dialog | `{ title, content }` |
|
||||
|
||||
### Complete Example
|
||||
|
||||
```typescript
|
||||
import { $Backend, Component, EventData } from "@yao/sui";
|
||||
|
||||
const self = this as Component;
|
||||
|
||||
// Helper: Send action to CUI
|
||||
const sendAction = (name: string, payload?: any) => {
|
||||
window.parent.postMessage(
|
||||
{ type: "action", message: { name, payload } },
|
||||
window.location.origin
|
||||
);
|
||||
};
|
||||
|
||||
// Initialize CUI communication
|
||||
function init() {
|
||||
window.addEventListener("message", (e) => {
|
||||
if (e.origin !== window.location.origin) return;
|
||||
|
||||
if (e.data.type === "setup") {
|
||||
const { theme, locale } = e.data.message;
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
}
|
||||
});
|
||||
|
||||
(window as any).sendAction = sendAction;
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
// Event handlers
|
||||
self.HandleSave = async (event: Event, data: EventData) => {
|
||||
try {
|
||||
await $Backend().Call("ApiSave", data);
|
||||
sendAction("notify.success", { message: "Saved successfully!" });
|
||||
} catch (error: any) {
|
||||
sendAction("notify.error", { message: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
self.HandleViewDetail = (event: Event, data: EventData) => {
|
||||
sendAction("navigate", {
|
||||
route: `/agents/my-app/detail`,
|
||||
title: "Details",
|
||||
query: { id: data.id },
|
||||
});
|
||||
};
|
||||
|
||||
self.HandleClose = () => {
|
||||
sendAction("event.emit", { key: "app/closeSidebar", value: {} });
|
||||
};
|
||||
```
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue