4.3 元素引用

> **本节目标**:掌握 `ref` 属性与 `ElementRef<T>` 的命令式访问能力,在需要直接操作 DOM 元素时游刃有余。

4.3.1 元素引用的定义

元素引用(Element Reference)让 ViewModel 可以直接访问 `.rml` 中的特定元素,进行命令式操作。

典型场景:

- 自动聚焦输入框 - 编程式触发按钮点击 - 读取输入框的当前值 - 控制元素的禁用/启用状态

<!-- .rml 中用 ref 命名元素 -->
<input ref="username_input" value={user_name} />
<button ref="submit_btn" on-click={submit}>提交</button>
// .rml.rs 中用 ElementRef 访问
#[derive(IModel)]
#[component]
pub struct MyView {
    pub user_name: SharedString,

    #[element]
    pub username_input: ElementRef<Input>,

    #[element]
    pub submit_btn: ElementRef<Button>,
}

impl MyView {
    #[on_loaded]
    pub fn on_loaded(&mut self, cx: &mut ViewContext<Self>) {
        // 自动聚焦输入框
        self.username_input.focus(cx);
    }
}

4.3.2 `ref` 属性

在 `.rml` 中用 `ref="name"` 为元素命名:

<input ref="username_input" />
<div ref="container">...</div>
<button ref="submit_btn">提交</button>

命名约定

- 使用 `snake_case` - 名称应描述元素的用途,而非类型 - ✅ `username_input`、`submit_btn`、`container` - ❌ `input1`、`button`、`div`

ref 的唯一性

同一视图内,每个 `ref` 名称必须唯一:

<!-- ❌ 重复的 ref 名 -->
<input ref="input" />
<input ref="input" />

4.3.3 `ElementRef<T>` 类型

`ElementRef<T>` 是 RML 提供的元素引用类型,`T` 是被引用元素的类型:

use rml::prelude::*;

#[derive(IModel)]
#[component]
pub struct MyView {
    #[element]
    pub username_input: ElementRef<Input>,      // 引用 Input 组件

    #[element]
    pub submit_btn: ElementRef<Button>,         // 引用 Button 组件

    #[element]
    pub container: ElementRef<Div>,             // 引用 div 元素
}

支持的元素类型

| 元素类型 | 对应 `.rml` 标签 | | ------------- | ---------------------- | | `Div` | `<div>` | | `Input` | `<input>` | | `TextArea` | `<textarea>` | | `Button` | `<button>` | | `Label` | `<label>`、`<p>`、`<h1>` | | `Image` | `<img>` | | `Checkbox` | `<input type="checkbox">` |

4.3.4 `#[element]` 属性

`#[element]` 标记字段为元素引用,让 RML 编译器自动关联 `ref` 名称与字段名:

#[derive(IModel)]
#[component]
pub struct MyView {
    #[element]
    pub username_input: ElementRef<Input>,
    // 字段名 "username_input" 自动关联 .rml 中的 ref="username_input"
}

字段名与 ref 名的对应

默认情况下,字段名与 `ref` 名相同:

// 字段名 username_input ↔ ref="username_input"
#[element]
pub username_input: ElementRef<Input>,
<input ref="username_input" />

显式指定 ref 名

如果需要字段名与 ref 名不同,可以显式指定:

#[element(ref = "user_input")]
pub username_input: ElementRef<Input>,
<input ref="user_input" />

4.3.5 ElementRef 的初始化

`ElementRef` 实现了 `Default`,可以在构造函数中用 `Default::default()` 初始化:

impl MyView {
    pub fn new() -> Self {
        Self {
            user_name: SharedString::default(),
            username_input: ElementRef::default(),  // 初始为空
            submit_btn: ElementRef::default(),
        }
    }
}

视图加载时,RML Runtime 会自动填充 `ElementRef`,关联到实际的 UI 元素。

4.3.6 ElementRef 的常用方法

`focus()`

让元素获得焦点:

self.username_input.focus(cx);

`blur()`

让元素失去焦点:

self.username_input.blur(cx);

`set_disabled()`

设置元素的禁用状态:

self.submit_btn.set_disabled(true, cx);

`set_value()`

直接设置元素的值(绕过双向绑定):

self.username_input.set_value("预设值".into(), cx);

`value()`

读取元素的当前值:

let current_value = self.username_input.value();

`is_visible() / set_visible()`

控制元素的可见性:

if !self.submit_btn.is_visible() {
    self.submit_btn.set_visible(true, cx);
}

4.3.7 使用场景

场景一:自动聚焦

#[on_loaded]
pub fn on_loaded(&mut self, cx: &mut ViewContext<Self>) {
    // 视图加载后自动聚焦用户名输入框
    self.username_input.focus(cx);
}

场景二:编程式提交

<input ref="search_input" value={search_text} onkeydown={on_key_down} />
#[command]
pub fn on_key_down(&mut self, ev: &KeyDownEvent, cx: &mut ViewContext<Self>) {
    if ev.key == Key::Enter {
        // 回车键触发搜索
        self.perform_search(cx);
    }
}

fn perform_search(&mut self, cx: &mut ViewContext<Self>) {
    // ... 搜索逻辑
    // 搜索完成后清空输入框
    self.search_input.set_value(SharedString::default(), cx);
}

场景三:条件禁用

#[command]
pub fn submit(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
    if self.user_name.is_empty() {
        return;
    }

    // 禁用提交按钮,防止重复提交
    self.submit_btn.set_disabled(true, cx);
    cx.notify();

    // 异步提交
    cx.spawn(|this, mut cx| async move {
        let result = submit_to_server().await;
        let _ = this.update(&mut cx, |this, cx| {
            this.submit_btn.set_disabled(false, cx);
            this.handle_submit_result(result, cx);
        });
    }).detach();
}

场景四:滚动控制

<div ref="message_list" class="message-list">
    <div each={msg in messages} key={msg.id}>{msg.text}</div>
</div>
#[command]
pub fn send_message(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
    // ... 添加新消息

    // 滚动到底部
    self.message_list.scroll_to_bottom(cx);
}

4.3.8 ElementRef 的生命周期

`ElementRef` 的生命周期与视图绑定:

1. ViewModel::new() → ElementRef::default()(空引用)
2. 视图首次渲染 → RML Runtime 填充 ElementRef
3. 用户交互 → 通过 ElementRef 操作元素
4. 视图卸载 → ElementRef 自动失效

⚠️ **注意**:在 `#[on_loaded]` 之前,`ElementRef` 是空的,不能调用其方法。`#[on_loaded]` 是最早可以安全使用 `ElementRef` 的时机。

4.3.9 ElementRef vs 数据绑定

| 特性 | ElementRef | 数据绑定 | | ------ | ------------------ | ---------------- | | 范式 | 命令式 | 声明式 | | 适用场景 | 编程式操作(聚焦、滚动、禁用) | 数据显示与同步 | | 性能 | 直接调用,无订阅开销 | 订阅 + diff | | 可读性 | 逻辑分散在命令方法中 | 集中在 `.rml` 中 | | 维护性 | 较低,需追踪命令调用 | 较高,一目了然 |

选择建议

- **优先用数据绑定**:能用 `{field}` 或 `value={field}` 解决的,不要用 `ElementRef` - **命令式操作用 ElementRef**:聚焦、滚动、编程式触发等场景 - **避免滥用**:不要用 `ElementRef` 替代数据绑定

// ❌ 滥用 ElementRef
self.username_input.set_value(self.user_name.clone(), cx);

// ✅ 用数据绑定
// .rml: <input value={user_name} />

4.3.10 完整示例:登录表单

// views/login.rml.rs
use rml::prelude::*;

#[derive(IModel)]
#[component]
pub struct LoginView {
    pub username: SharedString,
    pub password: SharedString,
    pub error_message: Option<SharedString>,
    pub is_submitting: bool,

    #[element]
    pub username_input: ElementRef<Input>,

    #[element]
    pub password_input: ElementRef<Input>,

    #[element]
    pub submit_btn: ElementRef<Button>,
}

impl LoginView {
    pub fn new() -> Self {
        Self {
            username: SharedString::default(),
            password: SharedString::default(),
            error_message: None,
            is_submitting: false,
            username_input: ElementRef::default(),
            password_input: ElementRef::default(),
            submit_btn: ElementRef::default(),
        }
    }

    #[on_loaded]
    pub fn on_loaded(&mut self, cx: &mut ViewContext<Self>) {
        // 自动聚焦用户名输入框
        self.username_input.focus(cx);
    }

    #[computed]
    pub fn can_submit(&self) -> bool {
        !self.username.is_empty()
            && !self.password.is_empty()
            && !self.is_submitting
    }

    #[command]
    pub fn submit(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
        if !self.can_submit() {
            return;
        }

        self.is_submitting = true;
        self.error_message = None;
        self.submit_btn.set_disabled(true, cx);
        cx.notify();

        let username = self.username.clone();
        let password = self.password.clone();

        cx.spawn(|this, mut cx| async move {
            let result = authenticate(&username, &password).await;
            let _ = this.update(&mut cx, |this, cx| {
                this.is_submitting = false;
                this.submit_btn.set_disabled(false, cx);

                match result {
                    Ok(_) => {
                        // 登录成功,跳转...
                    }
                    Err(e) => {
                        this.error_message = Some(e.to_string().into());
                        // 密码错误时聚焦密码框
                        this.password_input.focus(cx);
                        this.password_input.select_all(cx);
                    }
                }
                cx.notify();
            });
        }).detach();
    }

    #[command]
    pub fn on_key_down(&mut self, ev: &KeyDownEvent, cx: &mut ViewContext<Self>) {
        if ev.key == Key::Enter {
            if self.username.is_empty() {
                self.username_input.focus(cx);
            } else if self.password.is_empty() {
                self.password_input.focus(cx);
            } else {
                self.submit(&ClickEvent::default(), cx);
            }
        }
    }
}

async fn authenticate(username: &str, password: &str) -> Result<(), AuthError> {
    // 模拟异步认证
    Ok(())
}
<!-- views/login.rml -->
<div class="login-form">
    <h1>登录</h1>

    <input
        ref="username_input"
        value={username}
        placeholder="用户名"
        onkeydown={on_key_down}
    />

    <input
        ref="password_input"
        type="password"
        value={password}
        placeholder="密码"
        onkeydown={on_key_down}
    />

    <p if={error_message.is_some()} class="error">{error_message}</p>

    <button
        ref="submit_btn"
        on-click={submit}
        disabled={!can_submit}
    >
        {if is_submitting { "登录中..." } else { "登录" }}
    </button>
</div>

4.3.11 小结

元素引用是 RML 的命令式逃生舱:

- **`ref` 属性**:在 `.rml` 中为元素命名 - **`ElementRef<T>`**:在 ViewModel 中持有元素引用 - **`#[element]`**:标记字段为元素引用 - **常用方法**:`focus()`、`blur()`、`set_disabled()`、`set_value()`

记住:**优先用数据绑定,命令式操作才用 ElementRef**。滥用 ElementRef 会破坏 RML 的声明式优势。

下一节 → [4.4 命令系统](./command-system.md)