yao/sui/docs/frontend-api.md
Max aa01ea216c 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.
2026-01-03 11:31:15 +08:00

12 KiB

Frontend API

SUI provides a rich frontend API for component interaction, backend calls, and rendering.

Component Query

$$() Function

Get a component instance by selector or element:

// By ID
const card = $$("#my-card");

// By element
const element = document.querySelector(".card");
const card = $$(element);

// Access component methods
card.toggle();
card.state.Set("expanded", true);

Query Methods

const component = $$("#my-component");

// Find child component (returns __Query wrapper)
const button = component.find("button");

// Query single element
const title = component.query(".title"); // Returns Element

// Query all elements
const items = component.queryAll(".item"); // Returns NodeList

Backend Calls

Via $Backend

import { $Backend } from "@yao/sui";

// 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

// __sui_backend_call(route, headers, method, ...args)
const result = await __sui_backend_call(
  "/users/list", // Page route
  { "X-Custom-Header": "value" }, // Custom headers
  "ApiGetUsers", // Method name
  { page: 1, limit: 10 } // Arguments
);

Render API

Render Target

Define render targets in HTML:

<div s:render="userList" class="user-list">
  <!-- Content will be replaced here -->
</div>

Render Method

import { $Backend, Component } from "@yao/sui";

const self = this as Component;

self.RefreshUsers = async () => {
  const users = await $Backend().Call("ApiGetUsers");

  // Render with data
  await self.render("userList", { users });
};

Render Options

await self.render("targetName", data, {
  replace: true, // Replace content (default: true)
  showLoader: true, // Show loading indicator
  withPageData: true, // Include page data in render context
  route: "/custom/route", // Use custom route for rendering
});

Yao SDK (Legacy)

The Yao class provides HTTP client functionality:

const yao = new Yao();

// GET request
const data = await yao.Get("/api/users", { page: 1 });

// POST request
const result = await yao.Post("/api/users", { name: "John" });

// Download file
await yao.Download("/api/export", { format: "csv" }, "export.csv");

// Token management
const token = yao.Token();
yao.SetCookie("key", "value", 30); // 30 days
yao.DeleteCookie("key");

The OpenAPI client provides a modern HTTP client with type safety and error handling.

Initialization

const api = new OpenAPI({ baseURL: "/api" });

HTTP Methods

// GET
const response = await api.Get<User[]>("/users");

// POST
const response = await api.Post<User>("/users", {
  name: "John",
  email: "john@example.com",
});

// PUT
const response = await api.Put<User>("/users/123", {
  name: "John Updated",
});

// DELETE
const response = await api.Delete<void>("/users/123");

Error Handling

const response = await api.Get<User[]>("/users");

if (api.IsError(response)) {
  console.error(`Error: ${response.error.error_description}`);
  return;
}

const users = response.data;

Response Types

interface APIResponse<T> {
  data: T;
}

interface APIError {
  error: {
    error: string;
    error_description: string;
  };
}

File API

Initialization

const api = new OpenAPI({ baseURL: "/api" });
const fileApi = new FileAPI(api);

Upload

const fileInput = document.querySelector<HTMLInputElement>("#file");
const file = fileInput.files[0];

// Upload with progress
const response = await fileApi.Upload(
  file,
  {
    path: "documents",
    groups: ["team-a"],
    compressImage: true,
  },
  (progress) => {
    console.log(`${progress.percentage}%`);
  }
);

Upload Multiple

const responses = await fileApi.UploadMultiple(
  Array.from(fileInput.files),
  { path: "uploads" },
  (fileIndex, progress) => {
    console.log(`File ${fileIndex}: ${progress.percentage}%`);
  }
);

File Operations

// List files
const files = await fileApi.List({
  page: 1,
  pageSize: 20,
  contentType: "image/*",
  orderBy: "created_at desc",
});

// Get file info
const info = await fileApi.Retrieve("file-id");

// Download
const blob = await fileApi.Download("file-id");
if (!api.IsError(blob)) {
  const url = URL.createObjectURL(blob.data);
  window.open(url);
}

// Delete
await fileApi.Delete("file-id");

// Check existence
const exists = await fileApi.Exists("file-id");

Utility Methods

// Format file size
FileAPI.FormatSize(1024); // "1 KB"
FileAPI.FormatSize(1048576); // "1 MB"

// Get extension
FileAPI.GetExtension("doc.pdf"); // "pdf"

// Check type
FileAPI.IsImage("image/png"); // true
FileAPI.IsDocument("application/pdf"); // true

Cross-Origin Support

const api = new OpenAPI({ baseURL: "https://api.example.com" });

if (api.IsCrossOrigin()) {
  console.log("Cross-origin API");
}

// Set CSRF token after login
const loginResponse = await api.Post("/auth/login", credentials);
if (!api.IsError(loginResponse) && loginResponse.data.csrf_token) {
  api.SetCSRFToken(loginResponse.data.csrf_token);
}

// Clear tokens on logout
api.ClearTokens();

Custom Events

Emit

import { Component } from "@yao/sui";

const self = this as Component;

self.Select = () => {
  self.emit("card:selected", { id: self.store.Get("id") });
};

Listen

import { Component } from "@yao/sui";

const self = this as Component;

self.root.addEventListener("card:selected", (e: CustomEvent) => {
  console.log("Selected:", e.detail.id);
});

State Change Events

// Listen to child state changes
self.root.addEventListener("state:change", (e: CustomEvent) => {
  const { key, value, target } = e.detail;
  console.log(`${key} = ${value}`);
});

Complete Example

import { $Backend, Component, EventData } from "@yao/sui";

const self = this as Component;

// Initialize API
const api = new OpenAPI({ baseURL: "/api" });
const fileApi = new FileAPI(api);

// State watchers
self.watch = {
  users: (users: any[]) => self.render("userList", { users }),
  loading: (loading: boolean) => {
    self.root.classList.toggle("loading", loading);
  },
};

// Load users
async function loadUsers() {
  self.state.Set("loading", true);

  const response = await api.Get<User[]>("/users");
  if (!api.IsError(response)) {
    self.state.Set("users", response.data);
  }

  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

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:

// 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

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: {} });
};