5.4 自定义事件

> **本节目标**:掌握自定义组件如何声明和触发事件,让组件可被父视图监听和响应。

5.4.1 自定义事件的定义

自定义事件是组件向父视图通信的机制。当组件内部发生某些事情时(如用户操作、状态变化),可以通过自定义事件通知父视图。

组件内部触发事件 → 事件冒泡到父视图 → 父视图处理事件

5.4.2 声明自定义事件

在组件中用 `Option<Arc<dyn Fn(...)>>` 字段声明事件回调:

#[derive(IModel)]
#[component]
pub struct SearchBox {
    pub query: SharedString,
    pub on_search: Option<Arc<dyn Fn(&SearchEvent)>>,
    pub on_clear: Option<Arc<dyn Fn(&ClickEvent)>>,
}

事件回调的签名

// 无参数事件
Option<Arc<dyn Fn()>>

// 带事件对象的事件
Option<Arc<dyn Fn(&MyEvent)>>

// 带多个参数的事件
Option<Arc<dyn Fn(SharedString, &MyEvent)>>

5.4.3 定义事件对象

自定义事件可以携带自定义的事件对象:

#[derive(Clone)]
pub struct SearchEvent {
    pub query: SharedString,
    pub timestamp: chrono::DateTime<chrono::Local>,
}

事件对象的设计

| 字段类型 | 用途 | | ------------- | --------------- | | `SharedString` | 文本数据 | | `u64`、`i32` 等 | 数字数据 | | `bool` | 布尔标志 | | 自定义枚举 | 状态、类型 | | 时间戳 | 事件发生时间 |

5.4.4 触发事件

在组件内部通过回调触发事件:

impl SearchBox {
    pub fn new() -> Self {
        Self {
            query: SharedString::default(),
            on_search: None,
            on_clear: None,
        }
    }

    #[command]
    pub fn perform_search(&mut self, _: &ClickEvent, cx: &mut Context<Self>) {
        if let Some(callback) = &self.on_search {
            callback(&SearchEvent {
                query: self.query.clone(),
                timestamp: chrono::Local::now(),
            });
        }
    }

    #[command]
    pub fn clear(&mut self, ev: &ClickEvent, cx: &mut Context<Self>) {
        self.query = SharedString::default();
        cx.notify();

        if let Some(callback) = &self.on_clear {
            callback(ev);
        }
    }
}

5.4.5 在父视图中监听事件

父视图通过 `on_*` 属性监听子组件的事件:

<div class="search-panel">
    <SearchBox
        on_search={handle_search}
        on_clear={handle_clear}
    />
    <p if={search_results.is_empty()}>暂无结果</p>
    <ul>
        <li each={result in search_results} key={result.id}>
            {result.title}
        </li>
    </ul>
</div>
#[derive(IModel)]
#[component]
pub struct SearchPanel {
    pub search_results: Vec<SearchResult>,
}

impl SearchPanel {
    #[command]
    pub fn handle_search(&mut self, ev: &SearchEvent, cx: &mut Context<Self>) {
        // ev.query 是搜索框的输入值
        self.perform_search(&ev.query, cx);
    }

    #[command]
    pub fn handle_clear(&mut self, _: &ClickEvent, cx: &mut Context<Self>) {
        self.search_results.clear();
        cx.notify();
    }

    fn perform_search(&mut self, query: &str, cx: &mut Context<Self>) {
        // 执行搜索...
        cx.notify();
    }
}

5.4.6 事件命名约定

自定义事件的命名遵循 `on_*` 前缀:

| 事件字段 | 在 `.rml` 中的属性 | 触发时机 | | ------------- | ---------------- | ------------ | | `on_search` | `on_search={...}` | 执行搜索 | | `on_clear` | `on_clear={...}` | 清空内容 | | `on_change` | `on_change={...}` | 值变化 | | `on_select` | `on_select={...}` | 选择项 | | `on_submit` | `on_submit={...}` | 提交 |

命名规则

- 字段名用 `on_` 前缀 + 动词 - 用 `snake_case` - 描述事件的发生时机,而非处理方式

// ✅ 好的命名
pub on_search: Option<Arc<dyn Fn(&SearchEvent)>>,
pub on_item_selected: Option<Arc<dyn Fn(u64)>>,

// ❌ 不好的命名
pub callback: Option<Arc<dyn Fn(&SearchEvent)>>,  // 太泛化
pub handle_search: Option<Arc<dyn Fn(&SearchEvent)>>,  // handle_ 前缀冗余

5.4.7 事件的参数传递

无参数事件

pub on_close: Option<Arc<dyn Fn()>>,
<Dialog on_close={handle_close} />
#[command]
pub fn handle_close(&mut self, _: &CloseEvent, cx: &mut Context<Self>) {
    self.is_dialog_open = false;
    cx.notify();
}

带参数事件

pub on_select: Option<Arc<dyn Fn(u64, &SelectEvent)>>,
<Dropdown on_select={handle_select} />
#[command]
pub fn handle_select(&mut self, id: u64, ev: &SelectEvent, cx: &mut Context<Self>) {
    self.selected_id = Some(id);
    cx.notify();
}

带事件对象的事件

pub on_change: Option<Arc<dyn Fn(&ChangeEvent)>>,
<Input on_change={handle_change} />

5.4.8 事件的冒泡

自定义事件默认会冒泡,可以通过 `stop_propagation()` 阻止:

<div on-click={on_outer_click}>
    <SearchBox on_search={on_search} />
</div>
// SearchBox 内部
#[command]
pub fn perform_search(&mut self, ev: &ClickEvent, cx: &mut Context<Self>) {
    // 触发 on_search 事件
    if let Some(callback) = &self.on_search {
        callback(&SearchEvent { /* ... */ });
    }

    // 阻止冒泡,不触发外部 div 的 on-click
    ev.stop_propagation();
}

5.4.9 完整示例:自定义对话框组件

// components/dialog.rml.rs
use rml::prelude::*;

#[derive(Clone)]
pub struct DialogCloseEvent {
    pub reason: CloseReason,
}

#[derive(Clone)]
pub enum CloseReason {
    UserClosed,
    ConfirmClicked,
    CancelClicked,
}

#[derive(IModel)]
#[component]
pub struct Dialog {
    pub title: SharedString,
    pub is_open: bool,
    pub on_close: Option<Arc<dyn Fn(&DialogCloseEvent)>>,
    pub on_confirm: Option<Arc<dyn Fn(&ClickEvent)>>,
    pub on_cancel: Option<Arc<dyn Fn(&ClickEvent)>>,
}

impl Dialog {
    pub fn new(title: impl Into<SharedString>) -> Self {
        Self {
            title: title.into(),
            is_open: false,
            on_close: None,
            on_confirm: None,
            on_cancel: None,
        }
    }

    pub fn open(&mut self, cx: &mut Context<Self>) {
        self.is_open = true;
        cx.notify();
    }

    pub fn close(&mut self, reason: CloseReason, cx: &mut Context<Self>) {
        self.is_open = false;
        cx.notify();

        if let Some(callback) = &self.on_close {
            callback(&DialogCloseEvent { reason });
        }
    }

    #[command]
    pub fn on_confirm_click(&mut self, ev: &ClickEvent, cx: &mut Context<Self>) {
        if let Some(callback) = &self.on_confirm {
            callback(ev);
        }
        self.close(CloseReason::ConfirmClicked, cx);
    }

    #[command]
    pub fn on_cancel_click(&mut self, ev: &ClickEvent, cx: &mut Context<Self>) {
        if let Some(callback) = &self.on_cancel {
            callback(ev);
        }
        self.close(CloseReason::CancelClicked, cx);
    }

    #[command]
    pub fn on_overlay_click(&mut self, _: &ClickEvent, cx: &mut Context<Self>) {
        self.close(CloseReason::UserClosed, cx);
    }

    #[command]
    pub fn on_content_click(&mut self, ev: &ClickEvent, _cx: &mut Context<Self>) {
        ev.stop_propagation();  // 阻止冒泡,不关闭对话框
    }
}
<!-- components/dialog.rml -->
<div if={is_open} class="dialog-overlay" on-click={on_overlay_click}>
    <div class="dialog-content" on-click={on_content_click}>
        <h2 class="dialog-title">{title}</h2>
        <div class="dialog-body">
            <slot></slot>
        </div>
        <div class="dialog-footer">
            <button on-click={on_cancel_click}>取消</button>
            <button on-click={on_confirm_click}>确认</button>
        </div>
    </div>
</div>

使用对话框

<!-- views/user_view.rml -->
<div>
    <button on-click={show_delete_dialog}>删除用户</button>

    <Dialog
        title="确认删除"
        on_close={handle_dialog_close}
        on_confirm={handle_delete_confirm}
        on_cancel={handle_delete_cancel}
    >
        <p>确定要删除用户 {user_name} 吗?此操作不可撤销。</p>
    </Dialog>
</div>
// views/user_view.rml.rs
#[derive(IModel)]
#[component]
pub struct UserView {
    pub user_name: SharedString,
    pub delete_dialog: Entity<Dialog>,
}

impl UserView {
    pub fn new(cx: &mut AppContext) -> Self {
        Self {
            user_name: "张三".into(),
            delete_dialog: cx.new_model(|_| Dialog::new("确认删除")),
        }
    }

    #[command]
    pub fn show_delete_dialog(&mut self, _: &ClickEvent, cx: &mut Context<Self>) {
        self.delete_dialog.update(cx, |dialog, cx| dialog.open(cx));
    }

    #[command]
    pub fn handle_delete_confirm(&mut self, _: &ClickEvent, cx: &mut Context<Self>) {
        // 执行删除
        self.delete_user(cx);
    }

    #[command]
    pub fn handle_delete_cancel(&mut self, _: &ClickEvent, _cx: &mut Context<Self>) {
        // 取消,无需额外处理
    }

    #[command]
    pub fn handle_dialog_close(&mut self, ev: &DialogCloseEvent, cx: &mut Context<Self>) {
        match ev.reason {
            CloseReason::ConfirmClicked => {
                // 已在 handle_delete_confirm 处理
            }
            CloseReason::CancelClicked => {
                // 用户取消
            }
            CloseReason::UserClosed => {
                // 用户点击遮罩关闭
            }
        }
    }

    fn delete_user(&mut self, cx: &mut Context<Self>) {
        // 删除逻辑...
        cx.notify();
    }
}

5.4.10 自定义事件的注意事项

1. 事件回调是 `Option`

事件回调字段必须是 `Option`,因为父视图可能不监听:

// ✅ Option 类型
pub on_search: Option<Arc<dyn Fn(&SearchEvent)>>,

// ❌ 非 Option,父视图必须监听
pub on_search: Arc<dyn Fn(&SearchEvent)>,

2. 触发前检查 `Some`

// ✅ 检查后再调用
if let Some(callback) = &self.on_search {
    callback(&event);
}

// ❌ 直接调用,可能 panic
(self.on_search.as_ref().unwrap())(&event);

3. 事件对象实现 `Clone`

事件对象需要实现 `Clone`,因为可能被多次传递:

#[derive(Clone)]  // ← 必须
pub struct SearchEvent {
    pub query: SharedString,
}

4. 避免事件循环

// ❌ 事件循环:A 触发 B,B 触发 A
impl ComponentA {
    fn on_event(&mut self) {
        self.trigger_b();  // 触发 B
    }
}

impl ComponentB {
    fn on_event(&mut self) {
        self.trigger_a();  // 触发 A → 死循环
    }
}

5.4.11 小结

自定义事件是组件通信的核心机制:

- **声明**:用 `Option<Arc<dyn Fn(...)>>` 字段 - **触发**:在组件内部检查 `Some` 后调用回调 - **监听**:父视图用 `on_*` 属性绑定命令 - **冒泡**:自定义事件默认冒泡,可 `stop_propagation` - **命名**:`on_` 前缀 + 动词,`snake_case`

掌握自定义事件,你就能设计出可复用、可组合的组件。

下一节 → [5.5 防抖与节流](./debounce-throttle.md)