yao/sui/docs/components.md
Max 097fb73416 Refactor SUI Command Arguments and Enhance Default Template Handling
- Updated the `build` and `watch` commands to accept a single argument for `<sui>` and made the `<template>` argument optional, improving usability.
- Introduced a default template assignment for the `agent` SUI, ensuring a more intuitive setup for users.
- Enhanced error messages for command usage to provide clearer guidance on expected input format.
- Added new OpenAPI file to the bindata, improving the framework's capabilities for API integration.
2026-01-01 10:28:45 +08:00

6.2 KiB

Components

In SUI, every page is a component. Any page can be embedded into another page using the is attribute.

Core Concept

When a page is used as a component:

  1. The page's HTML becomes the component template
  2. The page's CSS is automatically scoped
  3. The page's TypeScript becomes the component class
  4. The page's backend.ts provides server-side logic via BeforeRender

Creating a Component

A component is just a page with a single root element:

/card/card.html:

<div class="card">
  <h3>{{ title }}</h3>
  <div class="card-body">
    <children></children>
  </div>
</div>

/card/card.css:

.card {
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 16px;
}

.card h3 {
  margin: 0 0 12px;
}

/card/card.ts:

function card(component: HTMLElement) {
  this.root = component;
  this.store = new __sui_store(component);
  this.props = new __sui_props(component);
}

Using Components

Basic Usage

Use the is attribute to embed a page as a component:

<div is="/card" title="My Card">
  <p>Card content goes here</p>
</div>

With Import Alias

Use <import> for cleaner syntax:

<import s:as="Card" s:from="/card" />
<import s:as="Button" s:from="/shared/button" />

<Card title="My Card">
  <p>Content</p>
</Card>

<Button variant="primary">Click Me</Button>

Props

Props are passed as attributes:

<div
  is="/user-card"
  name="{{ user.name }}"
  email="{{ user.email }}"
  avatar="{{ user.avatar }}"
  role="admin"
/>

Access props in the component script:

function userCard(component: HTMLElement) {
  this.root = component;
  this.props = new __sui_props(component);

  // Get single prop
  const name = this.props.Get("name");

  // Get all props
  const allProps = this.props.List();
  // { name: "John", email: "john@example.com", avatar: "...", role: "admin" }
}

Access props in backend script:

function BeforeRender(
  request: Request,
  props: Record<string, any>
): Record<string, any> {
  const userId = props.userId;
  return {
    user: Process("models.user.Find", userId),
  };
}

Children and Slots

Children

Use <children></children> to render child content:

Component (/panel/panel.html):

<div class="panel">
  <div class="panel-header">{{ title }}</div>
  <div class="panel-body">
    <children></children>
  </div>
</div>

Usage:

<div is="/panel" title="Settings">
  <p>This content appears in the panel body</p>
  <button>Save</button>
</div>

Named Slots

Use <slot name="xxx"> for multiple content areas:

Component (/modal/modal.html):

<div class="modal">
  <div class="modal-header">
    <slot name="header"></slot>
  </div>
  <div class="modal-body">
    <children></children>
  </div>
  <div class="modal-footer">
    <slot name="footer"></slot>
  </div>
</div>

Usage:

<div is="/modal">
  <slot name="header">
    <h2>Confirmation</h2>
  </slot>

  <p>Are you sure you want to proceed?</p>

  <slot name="footer">
    <button>Cancel</button>
    <button>Confirm</button>
  </slot>
</div>

Dynamic Components

Variable Component Route

<div is="{{ '/widgets/' + widgetType }}" ...widgetProps></div>

Dynamic Tag

<dynamic route="/components/{{ componentName }}" />

Component Script

Structure

function componentName(component: HTMLElement) {
  // Root element
  this.root = component;

  // Data store (data-* attributes)
  this.store = new __sui_store(component);

  // Props (passed attributes)
  this.props = new __sui_props(component);

  // State management
  this.state = new __sui_state(this);

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

Store API

// String data
this.store.Get("key");
this.store.Set("key", "value");

// JSON data
this.store.GetJSON("items");
this.store.SetJSON("items", [{ id: 1 }]);

// Component data (from BeforeRender)
this.store.GetData();

Props API

// Get single prop
const value = this.props.Get("propName");

// Get all props
const props = this.props.List();

State API

// Set state (triggers watchers)
this.state.Set("count", 10);

// Watch state changes
this.watch = {
  count: (value, state) => {
    this.root.querySelector(".count").textContent = value;
    // state.stopPropagation(); // Prevent bubbling to parent
  },
};

Nested Components

Components can include other components:

<!-- /dashboard/dashboard.html -->
<div class="dashboard">
  <div is="/shared/header" title="Dashboard" />

  <div class="content">
    <div is="/dashboard/stats" data="{{ stats }}" />
    <div is="/dashboard/chart" type="line" data="{{ chartData }}" />
  </div>

  <div is="/shared/footer" />
</div>

Component Backend Script

/user-card/user-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),
    permissions: Process("scripts.auth.GetPermissions", userId),
  };
}

function ApiUpdateUser(userId: string, data: any, request: Request): any {
  return Process("models.user.Save", userId, data);
}

CSS Scoping

Component CSS is automatically scoped using namespace attributes:

Original CSS:

.card {
  border: 1px solid #ddd;
}
.card h3 {
  color: #333;
}

Compiled CSS (scoped):

[s:ns="ns_abc123"] .card {
  border: 1px solid #ddd;
}
[s:ns="ns_abc123"] .card h3 {
  color: #333;
}

Important Notes

  1. Single Root Element: Components must have exactly one root element
  2. Route as Identifier: The page route becomes the component name (e.g., /cardcard())
  3. Scoped Styles: CSS is automatically scoped to prevent conflicts
  4. Recursive Prevention: SUI detects and prevents recursive component inclusion
  5. Script Naming: Function name is derived from the route path