From aa01ea216c21452f78f27ddf252302191b9ed134 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 3 Jan 2026 11:31:15 +0800 Subject: [PATCH] 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. --- sui/README.md | 2 +- sui/docs/agent-sui.md | 94 +++++++++++- sui/docs/components.md | 71 ++++----- sui/docs/event-handling.md | 305 ++++++++++++++++++------------------- sui/docs/frontend-api.md | 292 ++++++++++++++++++++++++++--------- 5 files changed, 485 insertions(+), 279 deletions(-) diff --git a/sui/README.md b/sui/README.md index 373dafff..bcd82615 100644 --- a/sui/README.md +++ b/sui/README.md @@ -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 diff --git a/sui/docs/agent-sui.md b/sui/docs/agent-sui.md index 5085f68e..a04a7e41 100644 --- a/sui/docs/agent-sui.md +++ b/sui/docs/agent-sui.md @@ -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. diff --git a/sui/docs/components.md b/sui/docs/components.md index ec235354..5a78be84 100644 --- a/sui/docs/components.md +++ b/sui/docs/components.md @@ -203,75 +203,68 @@ Use `` 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 }, }; diff --git a/sui/docs/event-handling.md b/sui/docs/event-handling.md index feb3c3d3..29730ff4 100644 --- a/sui/docs/event-handling.md +++ b/sui/docs/event-handling.md @@ -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 {
{{ item.name }} @@ -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 - + ``` ```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 = {}; + +self.HandleInput = (event: Event, data: EventData) => { + const input = event.target as HTMLInputElement; + formData[data.field] = input.value; +}; ``` ## Keyboard Events ```html - + ``` ```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
- + @@ -355,53 +350,51 @@ function Search(component: HTMLElement) {
  • {{ todo.title }} - +
  • ``` ```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); +}; ``` diff --git a/sui/docs/frontend-api.md b/sui/docs/frontend-api.md index 06901038..64a7c053 100644 --- a/sui/docs/frontend-api.md +++ b/sui/docs/frontend-api.md @@ -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("/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("/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: {} }); +}; ```