- Updated iframe documentation to remove token handling from the setup message, clarifying the focus on theme and locale. - Added a new section in MCP documentation detailing HTTP transport configuration for API access, enhancing clarity on authorization. - Revised frontend API documentation to standardize method naming by removing the 'Api' prefix in examples, improving consistency across backend calls. - Enhanced event handling documentation by updating method names in examples, ensuring alignment with the new naming conventions.
6.1 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:
- The page's HTML becomes the component template
- The page's CSS is automatically scoped
- The page's TypeScript becomes the component class
- The page's
backend.tsprovides server-side logic viaBeforeRender
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:
import { Component } from "@yao/sui";
const self = this as Component;
// self.root - Root element
// self.store - Data store
// self.props - Props from attributes
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:
import { Component } from "@yao/sui";
const self = this as Component;
// Get single prop
const name = self.props.Get("name");
// Get all props
const allProps = self.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
import { $Backend, Component, EventData } from "@yao/sui";
const self = this as Component;
// self.root - Root element (HTMLElement)
// self.store - Data store (data-* attributes)
// self.props - Props (passed attributes)
// self.state - State management
// State watchers
self.watch = {
propertyName: (value: any, state: any) => {
// React to state changes
},
};
// Event handlers (bound to s:on-click="HandleClick")
self.HandleClick = async (event: Event, data: EventData) => {
const result = await $Backend().Call("Method", data.id);
// Handle result
};
Store API
import { Component } from "@yao/sui";
const self = this as Component;
// String data
self.store.Get("key");
self.store.Set("key", "value");
// JSON data
self.store.GetJSON("items");
self.store.SetJSON("items", [{ id: 1 }]);
// Component data (from BeforeRender)
self.store.GetData();
Props API
// Get single prop
const value = self.props.Get("propName");
// Get all props
const props = self.props.List();
State API
// Set state (triggers watchers)
self.state.Set("count", 10);
// Watch state changes
self.watch = {
count: (value: number, state: any) => {
self.root.querySelector(".count")!.textContent = String(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
- Single Root Element: Components must have exactly one root element
- Scoped Styles: CSS is automatically scoped to prevent conflicts
- Recursive Prevention: SUI detects and prevents recursive component inclusion
- Component Pattern: Use
const self = this as Componentto access component APIs