yao/sui/docs/event-handling.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

8.5 KiB
Raw Blame History

Event Handling

SUI provides a declarative event binding system with state management and component communication.

Event Binding

Basic Events

Use s:on-<event> to bind events:

<button s:on-click="handleClick">Click Me</button>
<input s:on-input="handleInput" />
<form s:on-submit="handleSubmit">...</form>

Common Events

Attribute Event Description
s:on-click click Mouse click
s:on-dblclick dblclick Double click
s:on-input input Input value change
s:on-change change Value changed
s:on-submit submit Form submission
s:on-focus focus Element focused
s:on-blur blur Element lost focus
s:on-keydown keydown Key pressed
s:on-keyup keyup Key released
s:on-mouseenter mouseenter Mouse entered
s:on-mouseleave mouseleave Mouse left

Multiple Events

<input
  s:on-input="handleInput"
  s:on-focus="handleFocus"
  s:on-blur="handleBlur"
  s:on-keydown="handleKeydown"
/>

Passing Data

Data Attributes

Use s:data-* to pass string data:

<button
  s:on-click="deleteItem"
  s:data-id="{{ item.id }}"
  s:data-name="{{ item.name }}"
>
  Delete
</button>

JSON Data

Use s:json-* to pass complex data:

<button
  s:on-click="editItem"
  s:json-item="{{ item }}"
  s:json-options="{{ { confirm: true, redirect: '/list' } }}"
>
  Edit
</button>

Event Handlers

Handler Signature

function Page(component: HTMLElement) {
  this.root = component;

  this.handleClick = (event: Event, data: any, context: EventContext) => {
    // event - The DOM event
    // data - Combined data from s:data-* and s:json-*
    // context - Event context with element references
  };
}

EventContext

interface EventContext {
  rootElement: HTMLElement; // Component root element
  targetElement: HTMLElement; // Element that triggered the event
}

Example

<div class="item-list">
  <div s:for="{{ items }}" s:for-item="item">
    <span>{{ item.name }}</span>
    <button
      s:on-click="deleteItem"
      s:data-id="{{ item.id }}"
      s:json-item="{{ item }}"
    >
      Delete
    </button>
  </div>
</div>
function ItemList(component: HTMLElement) {
  this.root = component;

  this.deleteItem = async (event: Event, data: any, context: EventContext) => {
    const id = data.id; // String from s:data-id
    const item = data.item; // Object from s:json-item

    if (confirm(`Delete ${item.name}?`)) {
      await this.backend.ApiDeleteItem(id);
      context.targetElement.closest(".item").remove();
    }
  };
}

State Management

State Object

function Counter(component: HTMLElement) {
  this.root = component;
  this.state = new __sui_state(this);

  // Initial state
  this.state.Set("count", 0);
}

State Watchers

React to state changes with watchers:

function Counter(component: HTMLElement) {
  this.root = component;
  this.state = new __sui_state(this);

  // Define watchers
  this.watch = {
    count: (value: number, state: State) => {
      this.root.querySelector(".count").textContent = value;
    },

    items: (value: any[], state: State) => {
      this.renderItems(value);
    },
  };

  this.increment = () => {
    const count = this.state.Get("count") || 0;
    this.state.Set("count", count + 1); // Triggers watcher
  };
}

Stop Propagation

Prevent state changes from bubbling to parent:

this.watch = {
  localState: (value: any, state: State) => {
    // Handle locally
    this.updateUI(value);

    // Stop propagation to parent components
    state.stopPropagation();
  },
};

Store (Data Attributes)

Store manages data-* attributes on the component:

Basic Usage

function Card(component: HTMLElement) {
  this.root = component;
  this.store = new __sui_store(component);

  // Get/Set string values
  const id = this.store.Get("id");
  this.store.Set("id", "123");

  // Get/Set JSON values
  const items = this.store.GetJSON("items");
  this.store.SetJSON("items", [{ id: 1 }, { id: 2 }]);
}

Component Data

Get data from BeforeRender:

// Backend returns: { user: { name: "John" }, settings: {...} }
const data = this.store.GetData();
console.log(data.user.name); // "John"

Custom Events

Emit Events

function ItemCard(component: HTMLElement) {
  this.root = component;

  this.selectItem = () => {
    const item = this.store.GetJSON("item");

    // Emit custom event
    this.emit("item:selected", { item });
  };
}

Listen to Events

function ItemList(component: HTMLElement) {
  this.root = component;

  // Listen to child events
  this.root.addEventListener("item:selected", (e: CustomEvent) => {
    const { item } = e.detail;
    console.log("Selected:", item);
  });
}

State Change Events

Parent components can listen to state changes:

function Parent(component: HTMLElement) {
  this.root = component;

  this.root.addEventListener("state:change", (e: CustomEvent) => {
    const { key, value, target } = e.detail;
    console.log(`State ${key} changed to ${value} in`, target);
  });
}

Form Handling

Form Submit

<form s:on-submit="handleSubmit">
  <input name="email" type="email" required />
  <input name="password" type="password" required />
  <button type="submit">Login</button>
</form>
function LoginForm(component: HTMLElement) {
  this.root = component;

  this.handleSubmit = async (event: Event) => {
    event.preventDefault();

    const form = event.target as HTMLFormElement;
    const formData = new FormData(form);

    const email = formData.get("email");
    const password = formData.get("password");

    try {
      await this.backend.ApiLogin(email, password);
      window.location.href = "/dashboard";
    } catch (error) {
      alert("Login failed");
    }
  };
}

Input Binding

<input type="text" s:on-input="handleInput" s:data-field="name" />
function Form(component: HTMLElement) {
  this.root = component;
  this.formData = {};

  this.handleInput = (event: Event, data: any) => {
    const input = event.target as HTMLInputElement;
    this.formData[data.field] = input.value;
  };
}

Keyboard Events

<input s:on-keydown="handleKeydown" s:on-keyup="handleKeyup" />
function Search(component: HTMLElement) {
  this.root = component;

  this.handleKeydown = (event: KeyboardEvent) => {
    if (event.key === "Enter") {
      this.search();
    }

    if (event.key === "Escape") {
      this.clear();
    }
  };
}

Complete Example

<div class="todo-app">
  <form s:on-submit="addTodo">
    <input
      name="title"
      placeholder="Add todo..."
      s:on-keydown="handleKeydown"
    />
    <button type="submit">Add</button>
  </form>

  <ul class="todo-list">
    <li s:for="{{ todos }}" s:for-item="todo">
      <input
        type="checkbox"
        s:on-change="toggleTodo"
        s:data-id="{{ todo.id }}"
        s:attr-checked="{{ todo.completed }}"
      />
      <span class="{{ todo.completed ? 'completed' : '' }}">
        {{ todo.title }}
      </span>
      <button s:on-click="deleteTodo" s:data-id="{{ todo.id }}">×</button>
    </li>
  </ul>
</div>
function TodoApp(component: HTMLElement) {
  this.root = component;
  this.state = new __sui_state(this);
  this.store = new __sui_store(component);

  this.watch = {
    todos: (todos: any[]) => {
      this.render("todoList", { todos });
    },
  };

  this.addTodo = async (event: Event) => {
    event.preventDefault();
    const form = event.target as HTMLFormElement;
    const input = form.querySelector("input") as HTMLInputElement;

    if (input.value.trim()) {
      const todo = await this.backend.ApiAddTodo(input.value);
      const todos = this.state.Get("todos") || [];
      this.state.Set("todos", [...todos, todo]);
      input.value = "";
    }
  };

  this.toggleTodo = async (event: Event, data: any) => {
    const checkbox = event.target as HTMLInputElement;
    await this.backend.ApiToggleTodo(data.id, checkbox.checked);
  };

  this.deleteTodo = async (event: Event, data: any) => {
    await this.backend.ApiDeleteTodo(data.id);
    const todos = this.state.Get("todos").filter((t) => t.id !== data.id);
    this.state.Set("todos", todos);
  };
}