组件组合模式

> **本节目标**:统一理解 RML 中「谁包裹谁」「子节点是 UI 还是元数据」三类组合模式,避免与 KeyBinding 类似的语法偏差。

模式总览

| 模式 | 典型组件 | 触发/宿主关系 | 子节点语义 | |------|----------|---------------|------------| | **声明式元数据子节点(焦点)** | `KeyBinding` → `Input` | 快捷键挂在焦点宿主上 | 子节点不渲染,仅 codegen 元数据 | | **声明式元数据子节点(作用域)** | `Shortcut` → `ShortcutScope` | 快捷键挂在面板/页面作用域 | `<Shortcut/>` 不渲染;其余为内容 | | **slot 触发器** | `Dialog`、`Popover`、`Sheet`、`HoverCard`、`Notification` | `slot="trigger"` 标记触发器 | 其余兄弟为内容 | | **首子节点触发器** | `dropdown-menu`、`context-menu` | 第一个非 `menu-item` 子节点为触发器 | 其余为菜单项 |

三种模式**并存**,由各类组件的 codegen 分别实现;不要混用语法(例如不要用 KeyBinding 包裹 div 代替 ShortcutScope)。

---

1. 声明式元数据子节点(KeyBinding — 焦点宿主)

快捷键**必须**写在焦点控件**内部**:

<Input ref="field" placeholder="编辑">
    <KeyBinding key="Ctrl+S" on-press={on_save} />
</Input>

**实现**:`crates/engine/src/compiler/components/key_binding/attach.rs` + 各焦点宿主 translator。

**规则**:

- 宿主仅接受 `<KeyBinding>` 子节点 - KeyBinding 作为子节点时必须自闭合(无嵌套内容) - 不支持外层 `<KeyBinding>…</KeyBinding>` 包裹写法

详见 [key-binding.md](./reference/key-binding.md)。

---

1b. 声明式元数据子节点(ShortcutScope — 作用域级)

作用域快捷键**必须**写在 `ShortcutScope` **内部**:

<ShortcutScope>
    <Shortcut key="Ctrl+G" on-press={on_global_save} />
    <div>...</div>
</ShortcutScope>

**实现**:`crates/engine/src/compiler/components/shortcut_scope/attach.rs`。

**规则**:

- `ShortcutScope` 至少一个 `<Shortcut/>` + 一个内容子节点 - `<Shortcut>` 必须自闭合;单独使用 `<Shortcut>` 编译报错 - 不支持 KeyBinding 包裹 div 等替代写法

详见 [key-binding.md](./reference/key-binding.md)。

---

2. slot 触发器(浮层 / 对话框)

**标准**:显式 `slot="trigger"` 标记触发元素。

<Dialog open={dialog_open} title="确认">
    <Button slot="trigger" label="打开" />
    <p>对话框正文</p>
    <template slot="footer">
        <Button label="确定" on-click={on_ok} />
    </template>
</Dialog>

适用:`Dialog`、`AlertDialog`、`Popover`、`Sheet`、`HoverCard`、`Notification` 等。

---

3. 首子节点触发器(菜单)

**标准**:第一个非菜单项子元素为触发器,**不使用** `slot="trigger"`。

<dropdown-menu>
    <Button label="操作" />
    <menu-item label="复制" on-click={on_copy} />
    <menu-item label="粘贴" on-click={on_paste} />
</dropdown-menu>

实现:`compiler/menu/children.rs` 的 `partition_menu_children`。

---

Demo 案例布局规范(CaseDocPage)

案例页演示区应使用 **`demo-section`** 分场景,避免多个 `<Card>` 纵向黏连:

<CaseDocPage title={t("case.xxx.title")} ...>
    <template slot="demo">
        <div class="demo-section">
            <h3>场景一</h3>
            <p>说明文字</p>
            <!-- 演示组件 -->
        </div>
        <div class="demo-section">
            <h3>场景二</h3>
            ...
        </div>
    </template>
</CaseDocPage>

- 多场景用 `demo-section`,不用多个并列 `<Card title="...">` - 子元素间距优先用父级 `gap`,避免 `style="margin-top: …"` - 案例标题走 `t("case.*.title")`;场景说明可用中文硬编码(见 demo 规范)

详见 [demo-case-conventions.md](../11-cookbook/demo-case-conventions.md)。

---

语法规范速查

| 能力 | ✅ RML 写法 | ❌ 错误写法 | |------|------------|------------| | 条件渲染 | `if={cond}` | `r:if`、`v-if` | | 列表 | `each={x in xs}` `key={x.id}` | `r:each`、`v-for` | | 双向绑定 | `value={field}` | `r:model`、`v-model` | | 事件 | `on-click={fn}` | `onclick`、`on:click` | | Input 变更 | `on-change={fn}` | `onchange`(无连字符) |

完整命名规范见 [syntax-philosophy.md](../02-syntax/syntax-philosophy.md) 与 Skill [01-naming-conventions.md](../../.trae/skills/rml-component/01-naming-conventions.md)。