The Schema protocol is a JSON-based declarative UI description protocol used to define and render user interfaces. It uses a tree structure to describe component hierarchy and supports dynamic data binding and event handling.
Overview
The Schema protocol describes a complete UI structure through JSON objects, including:
- Component tree structure: Describes component hierarchy through nested node objects
- Component properties: Each component can configure properties supporting primitive values, JS expressions, JS functions, and more
- State management: Manages page-level state data through the
statefield - Event handling: Defines reusable methods through the
methodsfield; component properties can bind to these methods
Design Principles
- Declarative: Uses declarative structures to describe UI rather than imperative operations
- Platform-agnostic: The protocol itself is not tied to a specific framework; implementations are mapped through a component registry
- Type-safe: Ensures Schema structure correctness through type definitions
- Extensible: Supports custom components and property types
Core Concepts
Schema Object
A Schema is a JSON object containing a complete page definition. It must include the componentName field, typically "Page".
Node
A node is the basic unit in the component tree; each node represents a UI component. A node includes:
componentName: Component name (required)id: Unique node identifier (optional, but recommended)props: Component propertieschildren: Child node array- Other optional fields:
slot,loop,condition, etc.
RootNode
The root node is the top-level node of a Schema. In addition to all fields of a regular node, it includes page-level configuration:
state: Global statemethods: Method collectioncss: Global styles
Data Structures
RootNode Type Definition
type RootNode = Omit<Node, 'id'> & {
id?: string; // Optional id for root node
css?: string; // Global CSS style string
fileName?: string; // File name
methods?: Methods; // Method collection
state?: Record<string, unknown>; // Global state
schema?: any; // Embedded or external Schema
};
Node Type Definition
interface Node {
id?: string; // Unique node identifier (optional)
componentName: string; // Component name (required)
props?: Record<string, any> & {
columns?: { slots?: Record<string, any> }[]
}; // Component property collection
children?: Node[]; // Child node array
componentType?: 'Block' | 'PageStart' | 'PageSection'; // Node type
slot?: string | Record<string, any>; // Slot content
params?: string[]; // Parameter name list
loop?: Record<string, any>; // Loop rendering configuration
loopArgs?: string[]; // Loop parameter name list
condition?: boolean | Record<string, any>; // Conditional rendering configuration
}
Field Reference
Required Fields
- componentName (string): Component name; must match a component name in the client component registry
Optional Fields
- id (string): Unique node identifier; recommended for each node to aid debugging and event handling
- props (object): Component property object; keys are property names, values are property values (supports multiple types)
- children (Node[]): Child node array defining child components
- componentType ('Block' | 'PageStart' | 'PageSection'): Node type; usually omitted
- slot (string | object): Slot content; can be a string or object
- params (string[]): Parameter name list
- loop (object): Loop rendering configuration for list rendering
- loopArgs (string[]): Loop parameter name list, e.g.
["item", "index"] - condition (boolean | object): Conditional rendering configuration controlling whether the component renders
RootNode-Specific Fields
- css (string): Global CSS style string
- fileName (string): File name identifier
- methods (Methods): Method collection defining reusable functions
- state (Record<string, unknown>): Global state object
- schema (any): Embedded or external Schema
Property Value Types
Property values (PropValue) support the following types:
1. Primitive Values
string: Stringnumber: Numberboolean: Booleannull: null value
2. JS Expression (JSExpression)
Used for dynamically computing property values; supports accessing state and executing calculations.
interface JSExpression {
type: 'JSExpression'; // Fixed as 'JSExpression'
value: string; // Expression string
model?: boolean; // Whether this is a two-way binding model value
params?: string[]; // Parameters passed by scoped slots
}
Example:
{
"text": {
"type": "JSExpression",
"value": "this.state.userName + ' - ' + this.state.userHandle"
}
}
Two-way binding example:
{
"value": {
"type": "JSExpression",
"value": "this.state.inputValue",
"model": true
}
}
3. JS Function (JSFunction)
Used to define event handler functions.
interface JSFunction {
type: 'JSFunction'; // Fixed as 'JSFunction'
value: string; // Function body string (serializable)
}
Example:
{
"onClick": {
"type": "JSFunction",
"value": "function() { alert('Button clicked'); }"
}
}
4. Slot (JSSlot)
Used to define slot content.
interface JSSlot {
type: 'JSSlot'; // Fixed as 'JSSlot'
value: string | Record<string, any>; // Slot content
}
5. Arrays and Objects
Property values can be arrays or objects, supporting nested structures.
{
"items": ["item1", "item2", "item3"],
"config": {
"key1": "value1",
"key2": {
"type": "JSExpression",
"value": "this.state.dynamicValue"
}
}
}
6. Special Structure: columns
For table and similar components, props supports a special columns structure:
{
"props": {
"columns": [
{
"prop": "name",
"label": "Name",
"slots": {
"default": "custom-name-slot"
}
}
]
}
}
Component Rendering
Basic Rendering
Components are specified via the componentName field; the client looks up the corresponding implementation in the component registry.
{
"componentName": "Text",
"id": "text-1",
"props": {
"text": "Hello World"
}
}
Nested Rendering
Child components are defined through the children field, forming a component tree.
{
"componentName": "CanvasFlexBox",
"id": "container",
"props": {
"flexDirection": "column"
},
"children": [
{
"componentName": "Text",
"id": "title",
"props": {
"text": "Title"
}
},
{
"componentName": "Text",
"id": "content",
"props": {
"text": "Content"
}
}
]
}
Conditional Rendering
The condition field controls whether a component renders.
{
"componentName": "Text",
"id": "conditional-text",
"condition": {
"type": "JSExpression",
"value": "this.state.isVisible"
},
"props": {
"text": "Conditionally rendered text"
}
}
Or use a boolean value:
{
"componentName": "Text",
"id": "conditional-text",
"condition": true,
"props": {
"text": "Conditionally rendered text"
}
}
Loop Rendering
List rendering is implemented through the loop and loopArgs fields.
{
"componentName": "div",
"id": "list-item",
"loop": {
"list": {
"type": "JSExpression",
"value": "this.state.items"
}
},
"loopArgs": ["item", "index"],
"props": {
"style": "padding: 10px;"
},
"children": [
{
"componentName": "Text",
"id": "item-text",
"props": {
"text": {
"type": "JSExpression",
"value": "item.name"
}
}
}
]
}
Slot Rendering
Slot content is defined through the slot field.
{
"componentName": "Card",
"id": "card-1",
"slot": "Slot content text"
}
Or define multiple slots using an object:
{
"componentName": "Card",
"id": "card-1",
"slot": {
"header": "Header content",
"footer": "Footer content"
}
}
State Management
Defining State
Define global state in the state field of the root node.
{
"componentName": "Page",
"state": {
"userName": "John Doe",
"userAge": 25,
"isLoggedIn": true,
"userProfile": {
"name": "John Doe",
"email": "john@example.com"
}
}
}
Using State
Access state in component properties through JS expressions using this.state.
{
"componentName": "Text",
"id": "user-name",
"props": {
"text": {
"type": "JSExpression",
"value": "this.state.userName"
}
}
}
Two-Way Binding
For form components, use model: true to implement two-way binding.
{
"componentName": "Input",
"id": "user-input",
"props": {
"value": {
"type": "JSExpression",
"value": "this.state.inputValue",
"model": true
}
}
}
Event Handling
Defining Methods
Define reusable methods in the methods field of the root node.
{
"componentName": "Page",
"state": {
"formData": {
"name": "",
"email": ""
}
},
"methods": {
"handleSubmit": {
"type": "JSFunction",
"value": "function($event) { console.log('Triggered event object', $event); console.log('Submit data:', this.state.formData); }"
}
}
}
Binding Events
Bind event handler functions in component properties.
Option 1: Reference a method from methods
{
"componentName": "TinyButton",
"id": "submit-btn",
"props": {
"text": "Submit",
"onClick": {
"type": "JSExpression",
"value": "this.handleSubmit"
}
}
}
Option 2: Define JSFunction directly
{
"componentName": "TinyButton",
"id": "submit-btn",
"props": {
"text": "Submit",
"onClick": {
"type": "JSFunction",
"value": "function() { console.log('Submit button clicked'); }"
}
}
}
Complete Examples
Example 1: Simple Page
{
"componentName": "Page",
"fileName": "SimplePage",
"css": ".page-base-style {\n padding: 24px;\n background: #FFFFFF;\n}",
"props": {
"className": "page-base-style"
},
"children": [
{
"componentName": "CanvasFlexBox",
"id": "container",
"props": {
"flexDirection": "column",
"justifyContent": "center",
"alignItems": "center"
},
"children": [
{
"componentName": "Text",
"id": "title",
"props": {
"text": "Welcome to the Schema Protocol",
"style": "font-size: 24px; font-weight: bold; margin-bottom: 20px;"
}
},
{
"componentName": "Text",
"id": "subtitle",
"props": {
"text": "A declarative Schema-based UI rendering protocol",
"style": "font-size: 16px; color: #666;"
}
}
]
}
],
"state": {},
"methods": {},
"id": "body"
}
Example 2: Page with State and Events
{
"componentName": "Page",
"fileName": "UserProfile",
"css": ".page-base-style {\n padding: 24px;\n}",
"props": {
"className": "page-base-style"
},
"state": {
"userName": "John Doe",
"userAvatar": "https://www.example.com/avatar.jpg",
"userBio": "Full-stack developer"
},
"methods": {
"handleClick": {
"type": "JSFunction",
"value": "function() { alert('Button clicked!'); }"
}
},
"children": [
{
"componentName": "CanvasFlexBox",
"id": "profile-container",
"props": {
"flexDirection": "column",
"alignItems": "center",
"gap": "20px"
},
"children": [
{
"componentName": "img",
"id": "avatar",
"props": {
"src": {
"type": "JSExpression",
"value": "this.state.userAvatar"
},
"style": "width: 100px; height: 100px; border-radius: 50%;"
}
},
{
"componentName": "Text",
"id": "name",
"props": {
"text": {
"type": "JSExpression",
"value": "this.state.userName"
},
"style": "font-size: 24px; font-weight: bold;"
}
},
{
"componentName": "Text",
"id": "bio",
"props": {
"text": {
"type": "JSExpression",
"value": "this.state.userBio"
},
"style": "font-size: 16px; color: #666;"
}
},
{
"componentName": "TinyButton",
"id": "action-btn",
"props": {
"text": "Click me",
"onClick": {
"type": "JSExpression",
"value": "this.handleClick"
}
}
}
]
}
],
"id": "body"
}
Example 3: List with Loop Rendering
{
"componentName": "Page",
"fileName": "ProductList",
"state": {
"products": [
{ "id": 1, "name": "Product A", "price": 100 },
{ "id": 2, "name": "Product B", "price": 200 },
{ "id": 3, "name": "Product C", "price": 300 }
]
},
"children": [
{
"componentName": "CanvasFlexBox",
"id": "product-list",
"props": {
"flexDirection": "column",
"gap": "10px"
},
"children": [
{
"componentName": "div",
"id": "product-item",
"loop": {
"list": {
"type": "JSExpression",
"value": "this.state.products"
}
},
"loopArgs": ["item", "index"],
"props": {
"style": "padding: 10px; border: 1px solid #ddd; border-radius: 4px;"
},
"children": [
{
"componentName": "Text",
"id": "product-name",
"props": {
"text": {
"type": "JSExpression",
"value": "item.name"
},
"style": "font-size: 18px; font-weight: bold;"
}
},
{
"componentName": "Text",
"id": "product-price",
"props": {
"text": {
"type": "JSExpression",
"value": "'Price: ¥' + item.price"
},
"style": "font-size: 16px; color: #666;"
}
}
]
}
]
}
],
"methods": {},
"id": "body"
}
Type Definitions
Complete TypeScript Type Definitions
// JS expression
export type JSExpression = {
type: 'JSExpression';
value: string;
model?: boolean;
params?: string[];
};
// JS function
export type JSFunction = {
type: 'JSFunction';
value: string;
};
// Slot
export type JSSlot = {
type: 'JSSlot';
value: string | Record<string, any>
};
// Method collection
export type Methods = Record<string, JSFunction>;
// Property value type (recursive)
export type PropValue =
| string
| number
| boolean
| null
| JSExpression
| JSFunction
| JSSlot
| PropValue[]
| Record<string, PropValue>;
// Node interface
export interface Node {
id?: string; // Unique node identifier (optional)
componentName: string; // Component name (required)
props?: Record<string, any> & {
columns?: { slots?: Record<string, any> }[]
}; // Component property collection
children?: Node[]; // Child node array
componentType?: 'Block' | 'PageStart' | 'PageSection'; // Node type
slot?: string | Record<string, any>; // Slot content
params?: string[]; // Parameter name list
loop?: Record<string, any>; // Loop rendering configuration
loopArgs?: string[]; // Loop parameter name list
condition?: boolean | Record<string, any>; // Conditional rendering configuration
}
// Root node type
export type RootNode = Omit<Node, 'id'> & {
id?: string; // Optional id for root node
css?: string; // Global CSS style string
fileName?: string; // File name
methods?: Methods; // Method collection
state?: Record<string, unknown>; // Global state
schema?: any; // Embedded or external Schema
};
Common Components
Layout Components
-
CanvasFlexBox: Flexbox layout container
flexDirection: Main axis direction ('row' | 'column')justifyContent: Main axis alignmentalignItems: Cross axis alignmentwrap: Whether to wrapgap: Spacing
-
div: Generic container
style: Inline style stringclassName: CSS class name
Basic Components
-
Text: Text component
text: Text contentstyle: Style string
-
img: Image component
src: Image URLalt: Alternative textstyle: Style string
Business Components
-
TinyTabs: Tabs component
modelValue: Currently active tabclassName: CSS class name
-
TinyTabItem: Tab item
title: Tab titlename: Tab name
-
TinyCarousel: Carousel component
height: Heightautoplay: Whether to autoplayinterval: Switch interval (milliseconds)
-
TinyCarouselItem: Carousel item
title: Item title
-
TinyButton: Button component
text: Button textonClick: Click event handler
FAQ
Q: How do I pass data between components?
A: Define global state through the state field; child components access state via JS expressions using this.state.
Q: How do I implement conditional rendering?
A: Use the node's condition field, which can be a boolean value or a JS expression.
Q: How do I implement list rendering?
A: Use the node's loop and loopArgs fields; loop specifies the data source, and loopArgs specifies loop variable names.
Q: How do I implement two-way binding?
A: Set model: true in a JSExpression; applicable to form components.
Q: How do I define component event handlers?
A: There are two approaches:
- Define methods in the root node's
methods, then reference them in component properties usingthis.methodName(e.g.this.handleClick) - Define JSFunction directly in component properties