
Glass Easel
- 6 installs
- 318 repo stars
- Updated July 28, 2026
- wechat-miniprogram/glass-easel
Helps with ai & agent building tasks during AI-assisted development.
About
glass-easel is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- glass-easel
- AI & Agent Building
- AI-coding skill
Glass Easel by the numbers
- 6 all-time installs (skills.sh)
- +1 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #12,756 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/wechat-miniprogram/glass-easel --skill glass-easelAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 6 |
|---|---|
| repo stars | ★ 318 |
| Last updated | July 28, 2026 |
| Repository | wechat-miniprogram/glass-easel ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
glass-easel Component Development Knowledge
1. Framework Overview
glass-easel is a declarative rendering component framework. It describes the interface through templates and drives rendering via data binding. Each component has an independent Shadow Tree, and components communicate through properties and events. After setData, data and the interface are updated synchronously within the same call stack — there is no asynchronous batched update.
See Component Definition Reference for details.
2. Component Definition
Two equivalent styles; Chaining API is recommended (full TypeScript type inference, convenient logic splitting).
Chaining API (Recommended)
import * as glassEasel from 'glass-easel'
import { wxml } from 'glass-easel-template-compiler'
const componentSpace = glassEasel.getDefaultComponentSpace()
export const Counter = componentSpace.define()
.template(wxml(`
<div class="counter">
<div>{{ count }}</div>
<button bind:tap="increment">+</button>
</div>
`))
.data(() => ({ count: 0 }))
.init(({ setData, data, method }) => {
const increment = method(() => {
setData({ count: data.count + 1 })
})
return { increment }
})
.registerComponent()Definition API
export const Counter = componentSpace.defineComponent({
template: wxml(`
<div class="counter">
<div>{{ count }}</div>
<button bind:tap="increment">+</button>
</div>
`),
data: { count: 0 },
methods: {
increment() {
this.setData({ count: this.data.count + 1 })
},
},
})See Component Definition Reference for the full list of chaining methods and configuration fields.
3. Core Configuration Options
| Option | Description | Example |
|---|---|---|
template | Compiled template object | .template(wxml(\...\)) |
data | Internal component data (accepts a function) | .data(() => ({ msg: 'hi' })) |
staticData | Static data (plain object, deep-copied on creation) | .staticData({ msg: 'hi' }) |
property | Externally exposed property | .property('name', String) |
init | Instance initialization function | .init(({ method, lifetime }) => { ... }) |
methods | Batch method definitions | .methods({ fn() {} }) |
lifetime | Lifetime callback | .lifetime('attached', fn) |
pageLifetime | Page lifetime callback | .pageLifetime('show', fn) |
observer | Data observer | .observer(['a', 'b'], fn) |
behavior | Include a behavior | .behavior(sharedBehavior) |
usingComponents | Reference other components | .usingComponents({ child: ChildComp }) |
options | Component options | .options({ multipleSlots: true }) |
init Function
init is the core entry point for component logic, providing the following utilities:
| Utility | Description |
|---|---|
self | Component instance (equivalent to this) |
setData | Update data |
data | Current data reference |
method | Mark as a component method (can be bound in templates) |
listener | Mark as an event listener (TS receives event object) |
lifetime | Register a lifetime |
pageLifetime | Register a page lifetime |
observer | Register a data observer |
implement | Implement a Trait Behavior |
relation | Declare component relations |
.init(({ self, setData, data, method, listener, lifetime, observer }) => {
let count = 0
lifetime('attached', () => { count += 1 })
observer('inputVal', () => {
setData({ processed: data.inputVal.trim() })
})
const greet = method(() => {
setData({ msg: `Hello #${count}!` })
})
const onTap = listener((e) => {
console.log(e.detail)
})
return { greet, onTap }
})See Component Definition Reference for full property configuration and init utility details.
4. Template Syntax
WXML syntax; all tags must be properly closed (<div></div> or <div />).
Data Binding
<div>{{ message }}</div>
<div>{{ a + b }}</div>
<div>{{ flag ? 'Yes' : 'No' }}</div>Conditional Rendering
<div wx:if="{{ score >= 90 }}">Excellent</div>
<div wx:elif="{{ score >= 60 }}">Pass</div>
<div wx:else>Fail</div>List Rendering
<block wx:for="{{ list }}" wx:key="id">
<div>{{ index }}: {{ item.name }}</div>
</block>Custom variable names: wx:for-index="i" wx:for-item="t"
Event Binding
<button bind:tap="onTap">Normal binding</button>
<button catch:tap="onTap">Stop propagation</button>
<button mut-bind:tap="onTap">Mutually exclusive binding</button>Capture phase: capture-bind: / capture-catch: / capture-mut-bind:
Two-way Binding
<child model:count="{{ parentCount }}" />class / style Binding
<div class:selected="{{ active }}" class:disabled="{{ !enabled }}" />
<div style:color="red" style:font-size="{{ size }}px" />slot
<!-- Child component -->
<div><slot /></div>
<!-- Parent component -->
<child><div>Projected content</div></child>Other Syntax
- Temporary variables:
<block let:tmp="{{ complex.data }}">{{ tmp.name }}</block> - dataset:
<div data:userId="{{ id }}" bind:tap="onTap" /> - Event marks:
<div mark:index="{{ i }}" bind:tap="onTap" /> - Template fragments:
<template name="card">...</template>+<template is="card" data="{{ ...obj }}" /> - Template imports:
<import src="./shared.wxml" />/<include src="./header.wxml" /> - WXS scripts:
<wxs module="utils" src="./utils.wxs" />
See Template Syntax Reference for full syntax details.
5. Event System
Triggering Events
self.triggerEvent('customEvent', { someData: 'value' })
// Bubbling + cross-component bubbling
self.triggerEvent('customEvent', detail, { bubbles: true, composed: true })triggerEvent options: bubbles (bubbling), composed (cross-component bubbling), capturePhase (capture phase), extraFields (extra fields)
Listening to Events
- Template binding:
bind:/catch:/mut-bind:andcapture-variants - Declarative listeners:
'nodeId.event': handler(this.eventlistens to the component itself) - Imperative:
self.addListener(event, handler, options)/self.removeListener(event, handler)
Custom Events
// Child component triggers
self.triggerEvent('change', { value: newValue })
// Parent template listens: <child bind:change="onChildChange" />
const onChildChange = listener((e) => { console.log(e.detail.value) })See Event System Reference for full details.
6. Lifetimes
| Lifetime | Trigger Timing | Common Usage |
|---|---|---|
created | Instance just created | Rarely used (not yet attached to node tree) |
attached | After being attached to the page | Most common — initialize data, requests, bindingss |
moved | After being moved in the node tree | Only triggered in wx:for |
detached | After being removed from the page | Clean up resources, cancel subscriptions |
.init(({ lifetime }) => {
lifetime('attached', () => { /* initialization */ })
lifetime('detached', () => { /* cleanup */ })
})Others: error (catches exceptions within the component), pageLifetime (page-level broadcasts like show/hide). See Lifetime Reference for details.
7. Data Updates
Basic Update
setData({ count: 1 }) // Immediate update
setData({ 'obj.a': 2 }) // Path syntax
setData({ 'list[0].name': 'New' }) // Array pathAdvanced Path Updates
self.replaceDataOnPath(['obj', 'a'], 3) // Replace deep field
self.spliceArrayDataOnPath(['arr'], 1, 2, [5, 6]) // Array splice
self.applyDataUpdates() // Apply changesBatch Updates (Reduce Rendering Count)
self.groupUpdates(() => {
self.updateData({ a: 1 })
self.updateData({ b: 2 })
})Data Observers
observer(['a', 'b'], () => {
self.updateData({ sum: self.data.a + self.data.b })
})Note: Do not set the fields being observed within the observer, otherwise it will cause an infinite loop. See Data Management Reference for details.
8. Component Interaction
Property Passing (Parent → Child)
<my-child name="{{ userName }}" count="{{ total }}" />Event Communication (Child → Parent)
// Child component triggers
self.triggerEvent('change', { value: newValue })
// Parent template: <my-child bind:change="onChildChange" />slot Content Projection
<!-- Default slot -->
<my-child><div>Content</div></my-child>
<!-- Multiple slots (requires multipleSlots: true) -->
<my-child>
<div slot="header">Header</div>
<div slot="footer">Footer</div>
</my-child>Behaviors Code Reuse
const shared = componentSpace.define()
.property('name', String)
.registerBehavior()
// Component includes it
.behavior(shared)Other Interaction Mechanisms
- Trait Behaviors: Similar to interfaces, recommended over regular behaviors
- relations: Strong association between parent-child components (e.g., form/input)
- generics: Abstract nodes, where the consumer specifies the concrete implementation
- placeholders: Placeholders for lazily loaded components
- styleScope: Class prefix for style isolation
- externalClasses: Allow consumers to pass in custom classes
See Component Interaction Reference for details.
9. Common Patterns
Counter
export const Counter = componentSpace.define()
.template(wxml(`
<div>{{ count }}</div>
<button bind:tap="increment">+</button>
<button bind:tap="decrement">-</button>
`))
.data(() => ({ count: 0 }))
.init(({ setData, data, method }) => {
const increment = method(() => { setData({ count: data.count + 1 }) })
const decrement = method(() => { setData({ count: data.count - 1 }) })
return { increment, decrement }
})
.registerComponent()Child Component with Properties and Events
export const MyButton = componentSpace.define()
.property('label', String)
.property('disabled', Boolean)
.template(wxml(`
<button disabled="{{ disabled }}" bind:tap="onTap">{{ label }}</button>
`))
.init(({ self, data, method }) => {
const onTap = method(() => {
if (!data.disabled) {
self.triggerEvent('click', { label: data.label })
}
})
return { onTap }
})
.registerComponent()Referencing Child Components and Communication
export const App = componentSpace.define()
.usingComponents({ 'my-btn': MyButton })
.template(wxml(`
<my-btn label="Submit" bind:click="onBtnClick" />
<div wx:if="{{ submitted }}">Submitted</div>
`))
.data(() => ({ submitted: false }))
.init(({ setData, method }) => {
const onBtnClick = method(() => { setData({ submitted: true }) })
return { onBtnClick }
})
.registerComponent()10. Performance Best Practices
See Best Practices Reference for details.
11. Error Handling and Robustness
See Best Practices Reference for details.
14. Notes
1. API Style: Prefer the Chaining API 2. Synchronous Updates: setData is immediately synchronous; consecutive calls each trigger independent renders; use groupUpdates for batch updates 3. method Export: Functions wrapped with method/listener in init must be returned to be bindable in templates 4. Data Observers: Do not set the fields being observed within the observer 5. property observer vs observer: Property observer only triggers when the value changes; data observers trigger whenever the field is set via setData 6. Template Closing: All WXML tags must be properly closed; expressions inside curly braces must be valid JS expressions 7. Event Colon: Always use bind:tap (with colon), do not omit it 8. slot Mode: Prefer single slot (best performance); only enable multipleSlots or dynamicSlots when needed 9. behaviors: Prefer Trait Behaviors (better TS support); use regular behaviors as a compatibility option
Best Practices: Performance Optimization and Robustness
Part 1: Data Update Performance
1. Synchronous Update Mechanism
In glass-easel, setData is executed synchronously — each call independently triggers one template update. Multiple consecutive setData calls each trigger their own update, causing unnecessary rendering overhead.
Anti-pattern:
.lifetime('attached', function () {
this.setData({ a: 1 }) // Triggers one render
this.setData({ b: 2 }) // Triggers another render
})2. groupUpdates + updateData Batch Updates
Use groupUpdates to merge multiple updates into a single render:
.lifetime('attached', function () {
this.groupUpdates(() => {
this.updateData({ a: 1 })
this.updateData({ b: 2 })
})
// Only triggers one render
})The difference between updateData and setData: updateData does not immediately trigger rendering; it must be used with groupUpdates or within data observers.
3. Advanced Path Updates
replaceDataOnPath and spliceArrayDataOnPath only update specified fields within an object, avoiding processing the entire object path, with better performance than setData's path syntax.
.lifetime('attached', function () {
this.groupUpdates(() => {
// Update obj.a[0]
this.replaceDataOnPath(['obj', 'a', 0], 3)
// Array splice: starting at index 1, delete 2 items, insert [5, 6]
this.spliceArrayDataOnPath(['list'], 1, 2, [5, 6])
})
})Notes:
- After calling
replaceDataOnPath/spliceArrayDataOnPath, rendering does not happen immediately; you need to callapplyDataUpdates()or wrap ingroupUpdates. - Multiple calls can be made consecutively and applied all at once.
4. Data Update Behavior in Observers
In data observer callbacks, setData and updateData behave the same — neither immediately triggers rendering; they are applied uniformly after the observer finishes. Therefore, updateData is recommended in observers for clearer semantics.
.observer(['a', 'b'], function () {
// Recommended: clearer semantics
this.updateData({ sum: this.data.a + this.data.b })
})Observers can reduce template update count: observers execute before data is applied to the template, and data set within observers is merged with the original update into a single render.
Limitation: Do not set the observed field itself within the observer; otherwise, it will cause an infinite loop.
5. Deep Copy Control (dataDeepCopy)
By default, data undergoes a deep copy before updating (DeepCopyKind.Simple), preventing direct modification of this.data from affecting the template. Three modes:
| Mode | Value | Performance | Description |
|---|---|---|---|
Simple | Default | Average | Does not support recursive fields |
SimpleWithRecursion | Must be set | Worse | Supports recursive fields |
None | Must be set | Best | Completely disables copying, preserves prototype chain |
.options({
dataDeepCopy: glassEasel.DeepCopyKind.None,
})Limitation: When set to None, you must not directly modify this.data (e.g., this.data.a = 2); otherwise, behavior is unpredictable.
6. Property Passing Deep Copy (propertyPassingDeepCopy)
When properties are passed from parent to child components, a deep copy is performed by default. Can be set in the child component:
.options({
propertyPassingDeepCopy: glassEasel.DeepCopyKind.None,
})Limitation: When set to None, the child component must not modify passed-in property objects; otherwise, it may affect parent component data.
7. propertyEarlyInit
Default initialization flow: Initialize template with component's own data → trigger created → apply external properties → trigger observers → update template again. This means one extra template update.
With propertyEarlyInit: true: Merge own data and external properties first → trigger observers → initialize template → trigger created. Only one render.
.options({
propertyEarlyInit: true,
})Limitation: Data observers may trigger before the created lifetime; if observers depend on initialization logic in created, issues may arise.
8. Use Local Variables Instead of Non-rendering Data
Data that does not participate in template rendering should not be placed in .data or .staticData; instead, use local variables in the init function. This avoids unnecessary data overhead and template updates.
.init(({ self, lifetime }) => {
// Non-rendering state as local variables
let timer = null
let requestId = 0
lifetime('attached', () => {
timer = setInterval(() => { /* ... */ }, 1000)
})
lifetime('detached', () => {
clearInterval(timer)
})
})9. property comparer to Prevent Unnecessary Updates
Use comparer for custom property comparison logic; returning false indicates the value has not changed and no update is triggered:
.property('config', {
type: Object,
comparer(newVal, oldVal) {
// Return true to indicate value has changed (needs update), false for unchanged
return JSON.stringify(newVal) !== JSON.stringify(oldVal)
},
})A global default comparer can also be set via the component option propertyComparer.
---
Part 2: List Rendering Performance
1. Correct Usage of wx:key
wx:key helps the framework identify the correspondence between items during list updates, achieving updates with minimal operations.
Basic Usage
<block wx:for="{{ list }}" wx:key="id">
<div>{{ item.name }}</div>
</block>Usage Guidelines
- Uniqueness:
wx:keyvalues must be unique within the list. Duplicate keys produce warnings, and the framework automatically adds suffixes to distinguish them (e.g.,b--0,b--1), but this incurs additional overhead. - Type: All keys are converted to strings for comparison.
- Stability: Keys should remain unchanged throughout the list item's lifetime. Using
indexas a key has no effect when list items are moved.
When wx:key Is Needed
- Must be specified when list items are reordered, inserted, or deleted in the middle.
- Must be specified when list items contain stateful child components; otherwise, component state may become corrupted.
When wx:key Is Not Needed
- When list items never change or only append/remove at the end, omitting key can trigger the fast comparison sub-algorithm, which has better performance.
- When list items are purely presentational and contain no components, omitting key has no impact on results.
2. Handling Duplicate Keys
When duplicate keys exist in the list:
- The framework generates warning messages
- Duplicate keys get different suffixes to distinguish them (e.g.,
a--0,a--1) - This incurs additional processing overhead and should be avoided
3. List Update Optimization Recommendations
For all scenarios below, prefer spliceArrayDataOnPath over rebuilding the entire array with setData. It works like Array.prototype.splice, applying only the minimal change, which avoids re-diffing the whole list.
- Waterfall loading: Appending items at the end; specifying or not specifying key makes little performance difference. Use
spliceArrayDataOnPathwithindexbeyond array length to append:
// Append new items at the end (index = undefined means append)
const newItems = [{ id: 101, text: '...' }, { id: 102, text: '...' }]
this.spliceArrayDataOnPath(['list'], undefined, 0, newItems)
this.applyDataUpdates()- Scrolling comments: Remove-head-append-tail pattern; specify key to reduce unnecessary component recreation. Use
groupUpdatesto batch the two mutations into a single render:
this.groupUpdates(() => {
// Remove the first item
this.spliceArrayDataOnPath(['comments'], 0, 1, [])
// Append a new item at the end
this.spliceArrayDataOnPath(['comments'], undefined, 0, [newComment])
})- Single item move (e.g., pinning/drag sorting): Must specify key; glass-easel shows the most significant performance improvement in this scenario. Remove the item from its old position and insert it at the new position:
const item = this.data.list[fromIndex]
this.groupUpdates(() => {
// Remove from old position
this.spliceArrayDataOnPath(['list'], fromIndex, 1, [])
// Insert at new position (adjust index if needed after removal)
const toIdx = toIndex > fromIndex ? toIndex - 1 : toIndex
this.spliceArrayDataOnPath(['list'], toIdx, 0, [item])
})- Random rearrangement: Specify key; glass-easel guarantees minimum operation count. When replacing a contiguous range, use a single splice:
// Replace items at index 1..3 with new shuffled items
this.spliceArrayDataOnPath(['list'], 1, 3, [
{ id: 10, name: 'A' },
{ id: 11, name: 'B' },
])
this.applyDataUpdates()---
Part 3: Component Options Tuning
1. virtualHost (Virtual Host Node)
Use Cases
Setting virtualHost: true prevents the component from generating a real host DOM node; its child nodes are directly mounted to the parent. Applicable for:
- Component acts as a layout container where the host node interferes with flex/grid layout
- Need to reduce DOM depth
.options({
virtualHost: true,
})Layout Impact
Before enabling (with host DOM node):
<my-component>
<div class="a">
<child class="b"> <!-- child is a flex item -->
<div>child content</div>After enabling (without host DOM node):
<my-component>
<div class="a">
<!-- child does not generate a DOM node -->
<div>child content</div> <!-- Directly participates in parent container layout -->Solution for class/style Not Working
After enabling virtualHost, class and style on the component node itself become ineffective. Solution: Declare class as an external class and style as a property, then apply them on internal nodes.
const childComponent = componentSpace.define()
.options({ virtualHost: true })
.externalClasses(['class'])
.property('style', String)
.template(wxml(`
<div class="class" style="{{ style }}">child</div>
`))
.registerComponent()
// Usage is the same as regular components
// <child class="my-class" style="color: red" />Limitations:
getBackendElement()and$$returnnull- Multiple child nodes of the component are directly exposed to the parent container, which may require an extra wrapper element
2. slot Mode Performance Comparison
Performance ranking of three slot modes: Single slot > Multiple slots > Dynamic slots
| Mode | Option | Performance | Slot Content Creation Rule |
|---|---|---|---|
| Single slot | Default | Best | Always created, regardless of whether <slot /> tag exists |
| Multiple slots | multipleSlots: true | Good | Always created, regardless of whether a matching named <slot /> exists |
| Dynamic slots | dynamicSlots: true | Average | Created on demand, created/destroyed along with <slot /> tag |
Selection Guidelines
- Use default single slot when named slots are not needed
- Use
multipleSlots: truewhen named slots are needed but slot repetition in lists is not - Use
dynamicSlots: truewhen<slot />is needed inwx:foror slot data passing is required
Note: multipleSlots disables glass-easel's single slot-specific optimizations. Do not enable this option for components that don't need multiple slots.
Performance Implications of Slot Content Creation
In single and multiple slot modes, slot content is always created, even if there is no corresponding <slot /> tag in the component template. This means:
- Components in slot content will normally trigger the
attachedlifetime - If slot content is heavy, resources are consumed even if not displayed
In dynamic slot mode, slot content is created on demand, only when a <slot /> tag exists. For conditional rendering scenarios, dynamic slots may save more resources.
3. Trait Behaviors vs Regular Behaviors
Type Safety Comparison
Regular Behaviors' interface methods lack strict type checking in TypeScript — consumers need to know the specific component type to call methods.
Trait Behaviors provide interface-level type safety:
// Define interface
interface Toggleable {
toggle(): void
isActive(): boolean
}
const toggleableTrait = componentSpace.defineTraitBehavior<Toggleable>()
// Component implements interface
const myComp = componentSpace.define()
.implement(toggleableTrait, {
toggle() { /* ... */ },
isActive() { return true },
})
.registerComponent()
// Consumer only needs to know the interface, not the specific component
const impl = child.traitBehavior(toggleableTrait)!
impl.toggle() // Type-safe with full autocompletionCode Quality Comparison
| Aspect | Regular Behaviors | Trait Behaviors |
|---|---|---|
| Type Safety | Weak; depends on specific component type | Strong; interface-level type checking |
| Decoupling | Consumer needs to know component type | Consumer only needs to know the interface |
| Interface Conversion | Not supported | Supported (implement interface A, provide interface B) |
| Use Case | Sharing implementation code | Defining component interaction protocols |
Recommendation: Prefer Trait Behaviors for component interaction; use regular Behaviors only when implementation code needs to be shared.
---
Part 4: Error Handling and Robustness
1. error Lifetime
When a lifetime callback or event callback within a component throws an exception, the error lifetime is triggered. It is called before global error listeners and can be used for component-level error capture and recovery.
.lifetime('error', function (err) {
console.error('Internal component exception:', err)
// Can perform degradation handling here
})Error Handling Flow
When an exception is caught, glass-easel handles it in the following order:
1. Trigger the component's error lifetime (if any) 2. Call all global error listeners 3. If any listener returns false, stop further processing 4. If throwGlobalError is true, re-throw the exception 5. Otherwise, output to console.error
2. safeCallback
safeCallback wraps a function call with try-catch; caught exceptions automatically enter the error handling flow without interrupting outer logic.
const result = glassEasel.safeCallback(
'MyOperation', // Operation name (used in error messages)
myFunction, // Function to call
thisArg, // this binding
[arg1, arg2], // Argument array
relatedComponent, // Associated component instance (optional)
)
// Returns the function's return value on success; returns undefined on exceptionSuitable for executing untrusted or potentially failing callbacks, ensuring exceptions don't interrupt the main flow.
Scenarios Where glass-easel Automatically Uses safeCallback
| Category | Scenario |
|---|---|
| Lifetimes | created/attached/detached and other lifetime callbacks, page lifetime callbacks |
| Events | Event listener callbacks |
| Initialization | .init() initialization function |
| Data | .data() data generator function, property default function |
| Observers | Data observers (observer), property observers, property comparers (comparer) |
| Component Relations | linked/unlinked/linkChanged/linkFailed callbacks |
| Other | ComponentSpace.groupRegister() callback, backend render completion callback, MutationObserver callback |
This means exceptions in all the above scenarios are automatically caught and will not cause program crashes.
3. Avoiding Infinite Loops in Data Observers
Data observers trigger when observed fields are set, even if the value has not changed. Therefore, the following rule must be observed:
Rule: Do not set the fields being observed within the observer.
Bad example:
.observer('a', function () {
// Infinite loop! Even if the value hasn't changed, setting a triggers the observer again
this.updateData({ a: 1 })
})Correct pattern — observe source fields, update derived fields:
.observer(['a', 'b'], function () {
// Correct: observe a/b, update sum (not an observed field)
this.updateData({
sum: this.data.a + this.data.b,
})
})If you truly need to modify a field based on its own value, add a conditional check inside the observer to update only when necessary:
.observer('value', function () {
const clamped = Math.max(0, Math.min(100, this.data.value))
if (clamped !== this.data.value) {
this.updateData({ value: clamped })
}
})Note: The above pattern still carries risks; this.data.value is only a snapshot of the pre-update value when dataDeepCopy is not None. Prefer the "observe source fields, update derived fields" pattern.
4. detached Lifetime Resource Cleanup
detached is triggered after the component is removed from the page; suitable for performing cleanup operations. At this point, the component is no longer in the node tree and should not operate on nodes or update data.
.init(({ self, lifetime }) => {
let timer = null
let abortController = null
lifetime('attached', () => {
// Start timer
timer = setInterval(() => { /* ... */ }, 1000)
// Make request
abortController = new AbortController()
fetch('/api/data', { signal: abortController.signal })
})
lifetime('detached', () => {
// Clean up timer
if (timer) {
clearInterval(timer)
timer = null
}
// Cancel in-progress requests
if (abortController) {
abortController.abort()
abortController = null
}
})
})Component Definition Detailed Reference
Two Definition Styles
| Definition API | Chaining API (Recommended) | |
|---|---|---|
| Entry method | componentSpace.defineComponent({...}) | componentSpace.define().xxx().registerComponent() |
| Code style | Single configuration object | Chained method calls |
| TypeScript support | Limited | Full type inference |
| Logic splitting | Centralized in one object | Chaining allows multiple calls, easy to split |
Chaining API Example
export const myComponent = componentSpace.define()
.template(wxml(`<div>{{ message }}</div>`))
.data(() => ({ message: 'Hello' }))
.init(({ method, setData }) => {
const greet = method(() => { setData({ message: 'Hi!' }) })
return { greet }
})
.registerComponent()Definition API Example
export const myComponent = componentSpace.defineComponent({
template: wxml(`<div>{{ message }}</div>`),
data: { message: 'Hello' },
methods: {
greet() { this.setData({ message: 'Hi!' }) },
},
})Mixing Styles
Use .definition() to mix Definition API configuration into Chaining API:
export const myComponent = componentSpace.define()
.definition({
data: { count: 0 },
methods: { reset() { this.setData({ count: 0 }) } },
})
.init(({ self, setData, method }) => {
const increment = method(() => { setData({ count: self.data.count + 1 }) })
return { increment }
})
.registerComponent()Chaining API Method List
| Method | Description |
|---|---|
.template(template) | Set the template |
.data(gen) | Add data fields (accepts a function that returns an object) |
.staticData(data) | Set static data (cloned on creation) |
.property(name, def) | Add a property |
.methods(funcs) | Batch add methods |
.init(func) | Add initialization function |
.lifetime(name, func) | Add lifetime callback |
.pageLifetime(name, func) | Add page lifetime callback |
.observer(paths, func) | Add data observer |
.behavior(beh) | Include a behavior |
.implement(traitBehavior, impl) | Implement a trait behavior |
.usingComponents(list) | Reference other components |
.placeholders(list) | Set placeholder components |
.generics(list) | Set generics (abstract nodes) |
.externalClasses(list) | Set external classes |
.relation(name, rel) | Add component relation |
.definition(def) | Mix in Definition API configuration |
.options(options) | Set component options |
.registerComponent() | Register as a component |
.registerBehavior() | Register as a behavior |
Definition API Configuration Fields
| Field | Type | Description |
|---|---|---|
is | string | Component path name |
behaviors | `(string \ | GeneralBehavior)[]` |
using | `Record<string, string \ | ComponentDefinition>` |
generics | `Record<string, { default: ... } \ | true>` |
placeholders | Record<string, string> | Placeholder components |
template | Compiled template object | Template |
externalClasses | string[] | External classes |
data | `TData \ | (() => TData)` |
properties | TProperty | Property definitions |
methods | TMethod | Method definitions |
listeners | `Record<string, Function \ | string>` |
relations | Record<string, RelationParams> | Component relations |
lifetimes | Record<string, Function> | Lifetimes |
pageLifetimes | Record<string, Function> | Page lifetimes |
observers | Record<string, Function> | Data observers |
options | ComponentOptions | Component options |
data
data accepts a function that returns a data object, ensuring each instance gets an independent copy:
.data(() => ({ message: 'Hello world!' }))staticData takes an object directly (deep-copied on creation). Both can be used together; data overrides staticData fields with the same name. For complex objects, prefer data(() => ({...})).
property
Properties are externally exposed data fields. The simplest form — pass a type constructor:
.property('name', String)
.property('count', Number)Type Constructors
| Constructor | Value Type | Default Value |
|---|---|---|
String | string | '' |
Number | number | 0 |
Boolean | boolean | false |
Array | any[] | [] |
Object | `Record<string, any> \ | null` |
Function | (...args) => any | function() {} |
null | any | null |
Property Configuration Object
.property('count', {
type: Number,
value: 1, // Initial value (will be deep-copied)
// default: () => 1, // Factory function (recommended, no extra copy)
observer(newVal, oldVal) { console.log(`count: ${oldVal} -> ${newVal}`) },
comparer(newVal, oldVal) { return newVal !== oldVal },
})| Field | Type | Description |
|---|---|---|
type | PropertyType | Type constructor, defaults to null |
optionalTypes | PropertyType[] | Additional acceptable types |
value | Corresponds to type | Initial value (will be deep-copied) |
default | () => V | Initial value factory function (recommended). When both value and default exist, value is ignored |
observer | `((newVal, oldVal) => void) \ | string` |
comparer | (newVal, oldVal) => boolean | Custom comparison; returns true to indicate changed, defaults to !== |
reflectIdPrefix | boolean | Whether to add component id prefix when reflecting to DOM attribute |
Property observer only triggers after comparer determines the value has changed; data observers trigger whenever the field is set via setData, regardless of whether the value is the same.
init Function
Executed once per instance creation; defines private variables, methods, lifetimes, observers, etc.:
.init(function ({ self, setData, data, method, listener, lifetime, pageLifetime, observer, implement, relation }) {
let count = 0
lifetime('attached', () => { count += 1 })
observer('a', (newVal) => { console.log(newVal) })
const greet = method(() => { setData({ hello: `Hello #${count}!` }) })
const onTap = listener((e) => { console.log('tapped', e.detail) })
return { greet, onTap }
})init Parameter Utilities
| Utility | Description |
|---|---|
self | Component instance method caller, equivalent to this |
setData | Update data |
data | Current data reference |
method | Mark as a component method, bindable in templates |
listener | Mark as an event listener (TS type signature receives event object) |
lifetime | Register a lifetime |
pageLifetime | Register a page lifetime |
observer | Register a data observer |
implement | Implement a Trait Behavior |
relation | Declare component relations |
implement, relation, observer, lifetime, and pageLifetime can only be called during init execution; calling them afterward will throw an exception.
method and listener
Both can handle template event bindings. listener has clearer semantics, and its TS type signature receives event object parameters.
const increment = method(() => { setData({ count: data.count + 1 }) })
const onTap = listener((event) => { console.log(event.detail) })methods
Use .methods() to batch define methods mounted on this. It is recommended to use method wrapping in init — methods can call each other directly as function variables, and private functions are not exposed on the instance.
listeners
⚠️ Deprecated:listenersdeclarative event listening is deprecated. UseaddListenerimperatively to add event listeners instead.
Declaratively bind events on nodes in the Shadow Tree:
.definition({
listeners: {
'myDiv.tap': function (e) { /* ... */ },
'myChild.customEvent': function (e) { /* ... */ },
'someEvent': function (e) { /* Bound to Shadow Root */ },
'this.customEvent': function (e) { /* Bound to the component itself */ },
},
})Key format {id}.{event}; omitting the id binds to Shadow Root; id this binds to the component itself.
The recommended replacement is to use addListener in the init function or lifecycle callbacks:
export const myComponent = componentSpace.define()
.template(wxml(`
<div id="myDiv">
<child id="myChild" />
</div>
`))
.init(function ({ self, lifetime }) {
lifetime('attached', function () {
this.$.myDiv.addListener('tap', (e) => { /* ... */ })
this.$.myChild.addListener('customEvent', (e) => { /* ... */ })
this.$.shadowRoot.addListener('someEvent', (e) => { /* Bound to Shadow Root */ })
this.addListener('customEvent', (e) => { /* Bound to the component itself */ })
})
})
.registerComponent()⚠️ Theidinlistenerskeys only matches statically present nodes in the template. For dynamic nodes (e.g., nodes controlled bywx:if), theidmay not be correctly located. It is recommended to useaddListeneror bind events directly in the template withbind:xxx.
behaviors
Include a behavior to merge shared properties, methods, and lifetimes:
const sharedBehavior = componentSpace.define()
.property('shared', String)
.lifetime('attached', function () { console.log('shared attached') })
.registerBehavior()
export const myComponent = componentSpace.define()
.behavior(sharedBehavior)
.template(wxml(`<div>{{ shared }}</div>`))
.registerComponent()Fields ignored in behaviors: template, usingComponents, generics, placeholders, options.
Regular behaviors have weaker type inference in TS; prefer Trait Behaviors.
implement Trait Behavior
const greetTrait = componentSpace.defineTraitBehavior()
export const myComponent = componentSpace.define()
.implement(greetTrait, { greet() { return 'hello' } })
.registerComponent()
// Or in init
.init(({ implement }) => {
implement(greetTrait, { greet() { return 'hello' } })
})usingComponents
.usingComponents({
'my-child': childComponent, // Component definition object
'other-child': 'path/to/child', // String path
})generics (Abstract Nodes)
Certain nodes in the template are set as abstract, with the consumer specifying the implementation:
export const listComponent = componentSpace.define()
.generics({ item: true }) // No default implementation; with default: { default: comp }
.template(wxml(`<item />`))
.registerComponent()Usage: <list generic:item="my-item" />
placeholders
Lazily loaded components are first replaced with placeholders, then automatically replaced once registered:
.usingComponents({ child: 'lazy-components/child', placeholder: placeholderComponent })
.placeholders({ child: 'placeholder' })externalClasses
Allow consumers to pass in class names: .externalClasses(['my-class'])
In template: <div class="my-class" />; usage: <child my-class="some-class" />.
options
.options({
virtualHost: true, // Virtual host node
multipleSlots: true, // Multiple slots
dynamicSlots: true, // Dynamic slots
pureDataPattern: /^_/, // Pure data field regex
styleScope: myScope, // Style isolation scope
extraStyleScope: myScope, // Extra style scope
inheritStyleScope: true, // Inherit parent component's style scope
})relations
.relation('./child', {
type: 'child', // child / parent / descendant / ancestor, etc.
linked(target) { /* On linked */ },
unlinked(target) { /* On unlinked */ },
})See Component Interaction Reference for details.
Component Interaction Detailed Reference
usingComponents
export const myComponent = componentSpace.define()
.usingComponents({
'my-child': childComponent, // Component definition object
'other-child': 'path/to/child', // String path
})
.template(wxml(`<my-child /><other-child />`))
.registerComponent()slot
Child nodes of a parent component are inserted into specified positions in the child component's Shadow Tree via slots.
Three Modes
| Mode | Option | Description |
|---|---|---|
| Single slot | Default | One anonymous <slot /> |
| Multiple slots | multipleSlots: true | Multiple named slots |
| Dynamic slots | dynamicSlots: true | Slots can repeat in loops and pass data |
Single slot
// Child component
export const child = componentSpace.define()
.template(wxml(`<div><slot /></div>`))
.registerComponent()
// Parent component
.template(wxml(`<child><div>Inserted content</div></child>`))In single/multiple slot modes, even if the child component has no <slot />, the slot content is still created (triggers attached), just not rendered.
Multiple slots
// Child component
export const child = componentSpace.define()
.options({ multipleSlots: true })
.template(wxml(`
<div><slot name="header" /></div>
<slot name="footer" />
`))
.registerComponent()
// Parent component
.template(wxml(`
<child>
<div slot="header">Header</div>
<div slot="footer">Footer</div>
</child>
`))Single slot has more optimizations; do not activate multipleSlots when it's not needed.
Dynamic slots
Slots can repeat in loops and pass data to slot content:
// Child component
export const child = componentSpace.define()
.options({ dynamicSlots: true })
.template(wxml(`
<block wx:for="{{ list }}">
<slot list-index="{{ index }}" item="{{ item }}" />
</block>
`))
.data(() => ({ list: ['A', 'B', 'C'] }))
.registerComponent()
// Parent component receives data via slot:
.template(wxml(`
<child>
<div slot:item>{{ item }}</div>
</child>
`))slot: supports aliases: <div slot:listIndex="index">Item {{ index }}</div>
Unlike single/multiple slots, dynamic slot content is only created when the <slot /> is created, and destroyed when it is removed.
Regular Behaviors
A code-sharing mechanism for shared properties, methods, lifetimes, etc.
const sharedBehavior = componentSpace.define()
.property('shared', String)
.lifetime('attached', function () { console.log('shared attached') })
.registerBehavior()
export const myComponent = componentSpace.define()
.behavior(sharedBehavior)
.template(wxml(`<div>{{ shared }}</div>`))
.registerComponent()Fields ignored in behaviors: template, usingComponents/generics/placeholders, options.
Field Conflict Merge Strategy
| Field Type | Strategy |
|---|---|
| Properties/Methods | Later included overrides earlier; component's own takes priority |
| Data | Shallow merge (one level); non-objects overridden by the latter |
| Lifetimes/Page lifetimes/Data observers | Not overridden; executed in inclusion order |
For diamond inheritance, glass-easel automatically deduplicates; the same callback is merged only once.
Regular behaviors have weaker TS type inference; prefer Trait Behaviors.
Trait Behaviors
Similar to interfaces; define methods that must be implemented, with better TS type support.
const greetTrait = componentSpace.defineTraitBehavior()
// Implement
.implement(greetTrait, { greet() { return 'hello' } })
// Or in init
.init(({ implement }) => {
implement(greetTrait, { greet() { return 'hello' } })
})
// Usage
const impl = target.traitBehavior(greetTrait)
impl.greet()Component Relations (relations)
Used for tight logical connections between components; must be declared in pairs.
// form component
export const formComponent = componentSpace.define('component/form')
.relation('./input', {
type: 'descendant',
linked(target) { this.getRelationNodes('./input') },
unlinked(target) {},
})
.registerComponent()
// input component
export const inputComponent = componentSpace.define('component/input')
.relation('./form', {
type: 'ancestor',
linked(target) {},
linkFailed() { /* form not found */ },
})
.registerComponent()Relation Scope
| Scope | Meaning |
|---|---|
ancestor/descendant | Ancestor/Descendant |
parent/child | Cannot be separated by other component nodes |
parent-common-node/child-common-node | Cannot be separated by any node |
Relation Lifetime Callbacks
| Callback | Trigger Timing |
|---|---|
linked | Linked to a new component |
linkChanged | Related component moved |
unlinked | Related component removed |
linkFailed | Required ancestor not found |
Trait Behavior-based relations (Recommended)
const FormControl = componentSpace.defineTraitBehavior()
export const formComponent = componentSpace.define()
.init(({ relation }) => {
relation({
type: 'descendant',
target: FormControl,
linked(target) {
const impl = target.traitBehavior(FormControl)
impl.getName()
},
})
})
.registerComponent()
export const inputComponent = componentSpace.define()
.init(({ implement, relation }) => {
implement(FormControl, { getName() { return 'input' } })
relation({ type: 'ancestor', target: formComponent })
})
.registerComponent()Generics (Abstract Nodes)
Certain nodes in the template are set as abstract, with the consumer specifying the implementation:
export const listComponent = componentSpace.define()
.generics({ item: true }) // With default: { default: comp }
.template(wxml(`<item />`))
.registerComponent()Usage: <list generic:item="my-item" />
Placeholders
Lazily loaded components are first replaced with placeholders, then automatically replaced once registered:
.usingComponents({ child: 'lazy-components/child', placeholder: placeholderComponent })
.placeholders({ child: 'placeholder' })Style Isolation (styleScope)
Style is scoped within the component via class prefixes (handled during build by glass-easel-stylesheet-compiler).
const myStyleScope = componentSpace.styleScopeManager.register('my-prefix')
export const myComponent = componentSpace.define()
.options({ styleScope: myStyleScope })
.template(wxml(`<div class="header" />`))
.registerComponent()| Option | Description |
|---|---|
styleScope | Primary scope; classes only match prefixed styles |
extraStyleScope | Extra scope; matches both unprefixed and prefixed styles |
inheritStyleScope | Inherit parent component's style scope |
External Classes (externalClasses)
Allow consumers to pass in class names:
export const child = componentSpace.define()
.externalClasses(['my-class'])
.template(wxml(`<div class="my-class" />`))
.registerComponent()Usage: <child my-class="custom-style" />
Data Management Detailed Reference
setData
setData updates data and triggers rendering. In glass-easel, data updates are synchronous — after calling it, data and the interface are immediately updated within the same call stack.
.init(({ setData, lifetime }) => {
lifetime('attached', () => { setData({ message: 'updated!' }) })
})Path Syntax
Supports dot notation and bracket notation to directly update nested fields:
this.setData({
'obj.a': 3,
'obj.a[0]': 5,
'list[2].name': 'New name',
})Consecutive setData calls each independently trigger rendering and are not merged. Use groupUpdates or updateData + applyDataUpdates for batch updates.
Advanced Path Updates
replaceDataOnPath
Better performance than setData; path is in array form; requires manual applyDataUpdates:
this.replaceDataOnPath(['obj', 'a', 0], 3)
this.applyDataUpdates()spliceArrayDataOnPath
Array insertion and deletion, similar to Array.prototype.splice:
this.spliceArrayDataOnPath(['obj', 'arr'], 1, 2, [5, 6, 7])
// [1, 2, 3, 4] => [1, 5, 6, 7, 4]
this.applyDataUpdates()applyDataUpdates
Must be called after replaceDataOnPath/spliceArrayDataOnPath to apply changes. Multiple paths can be accumulated before applying all at once.
Combined Updates
groupUpdates
Combine multiple changes into a single batch; automatically applied after the callback:
this.groupUpdates(() => {
this.replaceDataOnPath(['a'], 3)
this.replaceDataOnPath(['b'], 5)
})updateData
Same format as setData but does not apply immediately; must be used with applyDataUpdates() or within groupUpdates:
this.groupUpdates(() => {
this.updateData({ a: 1 })
this.updateData({ b: 2 })
})groupUpdates + updateData has better performance than consecutive setData, triggering only one render.
Update Method Comparison
| Method | Immediate Apply | Path Format | Suitable Scenario |
|---|---|---|---|
setData({...}) | Yes | String | General purpose |
updateData({...}) | No, requires applyDataUpdates | String | Batch accumulation |
replaceDataOnPath(path, value) | No, requires applyDataUpdates | Array | Precise deep field updates |
spliceArrayDataOnPath(path, ...) | No, requires applyDataUpdates | Array | Array insert/delete |
groupUpdates(fn) | Auto after callback | Use above methods in callback | Batch combination |
applyDataUpdates() | Applies accumulated changes | — | Used with above methods |
Data Observers
Triggered when observed fields are set via setData/updateData; executed before data is applied to the template.
// Chaining API
.observer(['a', 'b'], function () {
this.updateData({ sum: this.data.a + this.data.b })
})
// In init
.init(({ self, observer }) => {
observer(['a', 'b'], () => {
self.updateData({ sum: self.data.a + self.data.b })
})
})Observing Sub-fields and Wildcards
.observer('obj.a, arr[2]', function () { /* Triggered when obj.a or arr[2] is set */ })
.observer('obj.**', function () { /* Triggered when any sub-field of obj is set */ })
.observer('**', function () { /* Triggered when any field is set */ })Trigger Rules
Data observers are triggered when a field is set, even if the value has not changed. This differs from property observer (which only triggers after the comparer determines a change).
Avoiding Infinite Loops
Do not set the fields being observed within the observer. The correct approach — observe source fields, update target fields:
// Correct: observe a and b, update sum
.observer(['a', 'b'], function () {
this.updateData({ sum: this.data.a + this.data.b })
})Data Updates Within Observers
Data updated via updateData in observer callbacks is only applied after the callback completes. setData behaves the same as updateData within observers.
Event System Detailed Reference
Event Model
Similar to the DOM event model, with capture and bubbling phases: 1. Capture phase (requires capturePhase option): Propagates from the outermost ancestor down to the target 2. Bubbling phase (requires bubbles option): Propagates from the target up to the ancestor
triggerEvent
Child component sends an event to the parent component:
self.triggerEvent(eventName, detail, options)| Parameter | Type | Description |
|---|---|---|
eventName | string | Event name |
detail | any | Accessed via e.detail |
options | object | Trigger options |
Trigger Options
| Option | Type | Default | Description |
|---|---|---|---|
bubbles | boolean | false | Whether to bubble |
composed | boolean | false | Whether to bubble across Shadow Root (requires bubbles as well) |
capturePhase | boolean | false | Whether to enable capture phase |
extraFields | Record<string, unknown> | — | Extra fields attached to the event object |
self.triggerEvent('customEvent', detail) // No bubbling
self.triggerEvent('customEvent', detail, { bubbles: true }) // Bubbling
self.triggerEvent('customEvent', detail, { bubbles: true, composed: true }) // Cross-component bubblingTemplate Event Binding
| Prefix | Stop Propagation | Mutually Exclusive | Phase |
|---|---|---|---|
bind: | No | No | Bubbling |
catch: | Yes | No | Bubbling |
mut-bind: | No | Yes | Bubbling |
capture-bind: | No | No | Capture |
capture-catch: | Yes | No | Capture |
capture-mut-bind: | No | Yes | Capture |
<div bind:tap="onTap">Click</div>
<div bind:tap="onOuterTap">
<button catch:tap="onInnerTap">Only triggers inner</button>
</div>It is recommended to always keep the colon (bind:tap) for better compilation optimization.
Declarative listeners
Declaratively bind events in the component definition:
.definition({
listeners: {
'myDiv.tap': function (e) { /* tap on id=myDiv */ },
'myChild.customEvent': function (e) { /* event on id=myChild */ },
'someEvent': function (e) { /* Event on Shadow Root */ },
'this.customEvent': function (e) { /* On the component itself */ },
},
})Key format {id}.{event}; omitting the id binds to Shadow Root; this binds to the component itself.
IDs in listeners only match static nodes. For dynamic nodes (e.g., wx:if), use bind: in templates instead.
addListener / removeListener
Imperatively add/remove listeners:
.init(function ({ self, lifetime }) {
lifetime('attached', () => {
self.addListener('customEvent', (e) => { console.log(e.detail) })
const child = self.getShadowRoot().getElementById('myChild')
child.addListener('customEvent', (e) => { console.log(e.detail) })
})
})addListener third parameter EventListenerOptions:
| Parameter | Default | Description |
|---|---|---|
final | false | Stops propagation after execution (similar to catch:) |
mutated | false | Marks as mutually exclusive after execution (similar to mut-bind:) |
capture | false | Listen in capture phase |
Remove: self.removeListener('customEvent', handler)
Mutually Exclusive Events mut-bind:
Does not stop propagation, but all mut-bind: bindings along the bubbling path are mutually exclusive: once one executes, subsequent mut-bind: bindings will not execute (bind: is not affected).
<div mut-bind:tap="onListItemTap">
<button mut-bind:tap="onButtonTap">Click button</button>
</div>
<!-- Only onButtonTap executes -->Manual marking in code: e.markMutated()
Event Marks mark:
e.mark collects marks from the target node and all its ancestors:
<block wx:for="{{ list }}">
<div mark:listIndex="{{ index }}">
<child mark:itemId="{{ item.id }}" bind:customEvent="onEvent" />
</div>
</block>const onEvent = listener((e) => {
e.mark.listIndex // Ancestor
e.mark.itemId // Target
})Event Object
| Property | Description |
|---|---|
e.detail | Event data (second parameter of triggerEvent) |
e.target | Source node that triggered the event |
e.target.dataset | Custom data: data of the source node |
e.currentTarget | Node that the listener is bound to |
e.mark | All mark: data |
e.markMutated() | Manually mark as mutually exclusive |
Lifetime Detailed Reference
Regular Lifetimes
| Lifetime | Trigger Timing | Trigger Count | Notes |
|---|---|---|---|
created | Instance just created | Once per instance | Not yet added to node tree; cannot find parent/sibling nodes |
attached | After being added to the page | At most once | Most common; suitable for initialization |
moved | After being moved in the node tree | Variable count in wx:for | |
detached | After being removed from the page | At most once | Should no longer operate on nodes or update data |
State Transitions
Create component → init → created → attached → [moved ↔ attached] → detached → DestroyedRegistration Methods
Chaining Method
export const myComponent = componentSpace.define()
.lifetime('attached', function () { console.log('attached') })
.lifetime('detached', function () { console.log('detached') })
.registerComponent()In init (Recommended)
export const myComponent = componentSpace.define()
.init(({ lifetime }) => {
lifetime('attached', () => { console.log('attached') })
lifetime('detached', () => { console.log('detached') })
})
.registerComponent()Definition API
export const myComponent = componentSpace.defineComponent({
lifetimes: {
attached() { console.log('attached') },
detached() { console.log('detached') },
},
})It is recommended to register in init, allowing sharing of closure variables with other logic. lifetime can only be called during init execution; calling it afterward will throw an exception.
Other Lifetimes
| Lifetime | Trigger Timing | Description |
|---|---|---|
ready | Component is ready | glass-easel does not trigger this automatically; requires this.triggerLifetime('ready', []) |
error | When a lifetime or event callback throws an exception | Parameter (err: unknown) |
listenerChange | When event listeners are added/removed | Parameter (isAdd, name, func, options); requires listenerChangeLifetimes: true |
workletChange | When a worklet value changes | Requires this.triggerWorkletChangeLifetime(name, value) |
.init(({ lifetime }) => {
lifetime('error', (err) => { console.error('Component error:', err) })
})Page Lifetimes (pageLifetime)
glass-easel does not trigger these proactively; they are triggered via triggerPageLifetime, which automatically propagates recursively to all descendant components.
// Register (recommended in init)
.init(({ pageLifetime }) => {
pageLifetime('show', () => { console.log('page show') })
pageLifetime('hide', () => { console.log('page hide') })
})
// Trigger
rootComponent.triggerPageLifetime('show', [])pageLifetime can only be called during init execution.
Template Syntax Detailed Reference
WXML syntax, an XML-like markup language. All tags must be properly closed (<div></div> or <div />).
Data Binding
{{ ... }} embeds expressions; data comes from data and property:
<div>{{ a }} + {{ b }} = {{ a + b }}</div>Supports arithmetic, comparison, logical, ternary, string concatenation, object and array literals:
<div>{{ { name: firstName + ' ' + lastName, age: age } }}</div>
<div>{{ [1, 2, a + b] }}</div>Conditional Branches
<div wx:if="{{ a > b }}"> a is greater than b </div>
<div wx:elif="{{ a < b }}"> a is less than b </div>
<div wx:else> a equals b </div>Use <block> to control multiple nodes (does not generate a real node):
<block wx:if="{{ show }}">
<span>First line</span>
<span>Second line</span>
</block>List Rendering
<div wx:for="{{ arr }}">Item {{ index }}: {{ item }}</div>Custom variable names: wx:for-index="i" wx:for-item="t"
wx:key
Provides a unique identifier to assist diffing and improve performance:
<block wx:for="{{ students }}" wx:key="id">
<div>{{ item.name }}</div>
</block>Note: Key values must be unique and be numbers or strings; keys do not help when only appending/removing at the end.
Event Binding
<div bind:tap="onTap">Click</div>
<child bind:customEvent="onCustomEvent" />Binding Prefixes
| Prefix | Stop Propagation | Mutually Exclusive | Description |
|---|---|---|---|
bind: | No | No | Normal binding |
catch: | Yes | No | Stops propagation |
mut-bind: | No | Yes | Mutually exclusive binding |
capture-bind: | No | No | Capture phase |
capture-catch: | Yes | No | Capture phase and stops propagation |
capture-mut-bind: | No | Yes | Capture phase mutually exclusive |
It is recommended to always keep the colon (bind:tap, not omitted) for better compilation optimization.
Two-way Binding
model: prefix enables two-way binding:
<child model:count="{{ parentCount }}" />
<textarea model:value="{{ inputText }}" />The model: expression must be an assignable data path, not a computed expression.
class Binding
<div class:selected class:disabled />
<div class:selected="{{ index === current }}" class:disabled="{{ !enabled }}" />When mixing class: with class=, class= must not contain data bindings.
style Binding
<div style:color="red" style:font-size="{{ size }}px" />When mixing style: with style=, style= must not contain data bindings.
Template Fragments
<!-- Definition -->
<template name="user-card">
<div class="card">
<div>{{ name }}</div>
<div>{{ age }} years old</div>
</div>
</template>
<!-- Usage -->
<template is="user-card" data="{{ name: 'John', age: 20 }}" />
<template is="user-card" data="{{ ...userInfo }}" />is supports dynamic switching via data binding: <template is="type{{ currentType }}" data="{{ value }}" />
Template Imports
<!-- import: imports template fragments -->
<import src="./shared.wxml" />
<template is="shared-template" data="{{ a: 1 }}" />
<!-- include: embeds an entire file -->
<include src="./header.wxml" />WXS Inline Scripts
<wxs module="utils" src="./utils.wxs" />
<div>{{ utils.formatDate(timestamp) }}</div>
<wxs module="math">
exports.sum = function (a, b) { return a + b }
</wxs>
<div>{{ math.sum(1, 2) }}</div>Temporary Variables let:
Only effective within the containing node and its descendants:
<block let:tempVar="{{ some.complex.data }}">
<div>{{ tempVar.name }}</div>
</block>dataset Attributes data:
Attach custom data to nodes, accessed via e.target.dataset:
<div data:userId="{{ user.id }}" data:userName="{{ user.name }}" bind:tap="onTap" />Also supports data- hyphenated syntax (first letter after hyphen is capitalized): <div data-user-id="123" /> → dataset.userId
Event Marks mark:
On event response, e.mark collects marks from the target node and all its ancestors:
<block wx:for="{{ list }}">
<div mark:listIndex="{{ index }}">
<child mark:itemId="{{ item.id }}" bind:customEvent="onEvent" />
</div>
</block>slot
<!-- Child component -->
<div><slot /></div>
<!-- Parent component -->
<child><div>Projected to slot position</div></child>For multiple slots, dynamic slots, and slot data passing, see Component Interaction Reference.
Attribute Change Listener change:
Bind a WXS function to listen for child component attribute changes:
<wxs module="bindUtils">
exports.onCountChange = function (newVal, oldVal, self, target) {
console.log('count changed:', newVal)
}
</wxs>
<child change:count="{{ bindUtils.onCountChange }}" count="{{ count }}" />Node ID
<div id="myDiv">Content</div>, can be found via self.$ or getShadowRoot().getElementById(). Also used to specify event targets in listeners.
Escape Characters
Outside data bindings, use XML Entities: > <. Inside data bindings, same as JS.