Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

组件子节点

Component Children

就像向 HTML 元素传递子节点一样,向组件传递子节点也是非常常见的需求。例如,假设我有一个增强了 HTML <form><FancyForm/> 组件,我需要某种方式来传递它的所有输入项。

It’s pretty common to want to pass children into a component, just as you can pass children into an HTML element. For example, imagine I have a <FancyForm/> component that enhances an HTML <form>. I need some way to pass all its inputs.

view! {
    <FancyForm>
        <fieldset>
            <label>
                "Some Input"
                <input type="text" name="something"/>
            </label>
        </fieldset>
        <button>"Submit"</button>
    </FancyForm>
}

在 Leptos 中如何实现呢?基本上有两种方式将组件传递给其他组件:

How can you do this in Leptos? There are basically two ways to pass components to other components:

  1. 渲染属性 (render props):本身是返回视图的函数的属性。

  2. children 属性:一个特殊的组件属性,包含你作为子节点传递给组件的任何内容。

  3. render props: properties that are functions that return a view

  4. the children prop: a special component property that includes anything you pass as a child to the component.

事实上,你已经在 <Show/> 组件中见过这两者的应用了:

In fact, you’ve already seen these both in action in the <Show/> component:

view! {
  <Show
    // `when` 是一个普通的属性
    // `when` is a normal prop
    when=move || value.get() > 5
    // `fallback` 是一个 "渲染属性":一个返回视图的函数
    // `fallback` is a "render prop": a function that returns a view
    fallback=|| view! { <Small/> }
  >
    // `<Big/>`(以及这里的任何其他内容)
    // 将被传递给 `children` 属性
    // `<Big/>` (and anything else here)
    // will be given to the `children` prop
    <Big/>
  </Show>
}

让我们定义一个接收一些子节点和渲染属性的组件。

Let’s define a component that takes some children and a render prop.

/// 在标记中显示一个 `render_prop` 和一些子节点。
/// Displays a `render_prop` and some children within markup.
#[component]
pub fn TakesChildren<F, IV>(
    /// 接收一个函数(类型为 F),该函数返回任何可以
    /// 转换为 View 的内容(类型为 IV)
    /// Takes a function (type F) that returns anything that can be
    /// converted into a View (type IV)
    render_prop: F,
    /// `children` 可以接收几种不同类型之一,每种类型
    /// 都是一个返回某种视图类型的函数
    /// `children` can take one of several different types, each of which
    /// is a function that returns some view type
    children: Children,
) -> impl IntoView
where
    F: Fn() -> IV,
    IV: IntoView,
{
    view! {
        <h1><code>"<TakesChildren/>"</code></h1>
        <h2>"Render Prop"</h2>
        {render_prop()}
        <hr/>
        <h2>"Children"</h2>
        {children()}
    }
}

render_propchildren 都是函数,所以我们可以通过调用它们来生成相应的视图。特别是 Children,它是 Box<dyn FnOnce() -> AnyView> 的别名。(难道你不庆幸我们把它命名为 Children 吗?)这里返回的 AnyView 是一个不透明的、类型擦除后的视图:你无法对它进行检查。还有各种其他的子节点类型:例如,ChildrenFragment 将返回一个 Fragment(片段),这是一个子节点可以被迭代的集合。

render_prop and children are both functions, so we can call them to generate the appropriate views. Children, in particular, is an alias for Box<dyn FnOnce() -> AnyView>. (Aren't you glad we named it Children instead?) The AnyView returned here is an opaque, type-erased view: you can’t do anything to inspect it. There are a variety of other child types: for example, ChildrenFragment will return a Fragment, which is a collection whose children can be iterated over.

如果你需要多次调用 children,因此这里需要 FnFnMut,我们也提供了 ChildrenFnChildrenMut 别名。

If you need a Fn or FnMut here because you need to call children more than once, we also provide ChildrenFn and ChildrenMut aliases.

我们可以像这样使用该组件:

We can use the component like this:

view! {
    <TakesChildren render_prop=|| view! { <p>"Hi, there!"</p> }>
        // 这些内容被传递给 `children`
        // these get passed to `children`
        "Some text"
        <span>"A span"</span>
    </TakesChildren>
}

类型化子节点:插槽 (Slots)

Typed Children: Slots

到目前为止,我们讨论的是具有单个 children 属性的组件,但有时创建一个具有多个不同类型子节点的组件会很有帮助。例如:

So far, we have discussed components with a single children prop, but sometimes it is helpful to create a component with multiple children of different types. For example:

view! {
    <If condition=a_is_true>
        <Then>"Show content when a is true"</Then>
        <ElseIf condition=b_is_true>"b is true"</ElseIf>
        <ElseIf condition=c_is_true>"c is true"</ElseIf>
        <Else>"None of the above are true"</Else>
    </If>
}

If 组件始终期望有一个 Then 子节点,可选的多个 ElseIf 子节点以及一个可选的 Else 子节点。为了处理这种情况,Leptos 提供了 slot(插槽)。

The If component always expects a Then child, optionally multiple ElseIf children and an optional Else child. To handle this, Leptos provides slots.

#[slot] 宏将一个普通的 Rust 结构体标注为组件插槽:

The #[slot] macro annotates a plain Rust struct as component slot:

// 一个标注了 `#[slot]` 的简单结构体,
// 它期望有子节点
// A simple struct annotated with `#[slot]`,
// which expects children
#[slot]
struct Then {
    children: ChildrenFn,
}

这个插槽可以作为组件中的属性使用:

This slot can be used as a prop in a component:

#[component]
fn If(
    condition: Signal<bool>,
    // 组件插槽,应通过 <Then slot> 语法传递
    // Component slot, should be passed through the <Then slot> syntax
    then_slot: Then,
) -> impl IntoView {
    move || {
        if condition.get() {
            (then_slot.children)().into_any()
        } else {
            ().into_any()
        }
    }
}

现在,If 组件期望一个 Then 类型的子节点。你需要使用 slot:<prop_name> 来标注所使用的插槽:

Now, the If component expects a child of type Then. You would need to annotate the used slot with slot:<prop_name>:

view! {
    <If condition=a_is_true>
        // `If` 组件始终期望 `then_slot` 属性有一个 `Then` 子节点
        // The `If` component always expects a `Then` child for `then_slot`
        <Then slot:then_slot>"Show content when a is true"</Then>
    </If>
}

指定不带名称的 slot 将默认选择该插槽,名称为结构体名称的蛇形命名(snake case)版本。因此在这种情况下,<Then slot> 等同于 <Then slot:then>

Specifying slot without a name will default the chosen slot as the snake case version of the struct name. So in this case <Then slot> would be equivalent to <Then slot:then>.

完整示例请参见 插槽示例

For the complete example, see slots examples.

插槽上的事件处理程序

Event handlers on slots

不能像这样直接在插槽上指定事件处理程序:

Event handlers cannot be specified directly on slots like this:

<ComponentWithSlot>
    // ⚠️ 不允许直接在插槽上使用事件处理程序 `on:click`
    // ⚠️ Event handler `on:click` directly on slot is not allowed
    <SlotWithChildren slot:slot on:click=move |_| {}> 
        <h1>"Hello, World!"</h1>
    </SlotWithChildren>
</ComponentWithSlot>

相反,应将插槽内容包裹在一个普通元素中,并在那里附加事件处理程序:

Instead, wrap the slot content in a regular element and attach event handlers there:

<ComponentWithSlot>
    <SlotWithChildren slot:slot>
        // ✅ 事件处理程序未直接定义在插槽上
        // ✅ Event handler is not defined directly on slot
        <div on:click=move |_| {}>
            <h1>"Hello, World!"</h1>
        </div>
    </SlotWithChildren>
</ComponentWithSlot>

操作子节点

Manipulating Children

Fragment 类型本质上是包装 Vec<AnyView> 的一种方式。你可以将其插入视图中的任何位置。

The Fragment type is basically a way of wrapping a Vec<AnyView>. You can insert it anywhere into your view.

但你也可以直接访问那些内部视图进行操作。例如,下面是一个接收其子节点并将它们转换成无序列表的组件。

But you can also access those inner views directly to manipulate them. For example, here’s a component that takes its children and turns them into an unordered list.

/// 将每个子节点包裹在 `<li>` 中并嵌入到 `<ul>` 中。
/// Wraps each child in an `<li>` and embeds them in a `<ul>`.
#[component]
pub fn WrapsChildren(children: ChildrenFragment) -> impl IntoView {
    // children() 返回一个 `Fragment`,它有一个
    // `nodes` 字段,其中包含一个 Vec<View>
    // 这意味着我们可以遍历子节点来创建一些新的东西!
    // children() returns a `Fragment`, which has a
    // `nodes` field that contains a Vec<View>
    // this means we can iterate over the children
    // to create something new!
    let children = children()
        .nodes
        .into_iter()
        .map(|child| view! { <li>{child}</li> })
        .collect::<Vec<_>>();

    view! {
        <h1><code>"<WrapsChildren/>"</code></h1>
        // 将包装好的子节点放入 UL 中
        // wrap our wrapped children in a UL
        <ul>{children}</ul>
    }
}

像这样调用它将创建一个列表:

Calling it like this will create a list:

view! {
    <WrapsChildren>
        "A"
        "B"
        "C"
    </WrapsChildren>
}

在线示例

点击打开 CodeSandbox。

Click to open CodeSandbox.

CodeSandbox 源代码
use leptos::prelude::*;

// 通常,你希望将某种子视图传递给另一个组件。
// 有两种基本模式可以实现这一点:
// - "渲染属性":创建一个组件属性,它接收一个创建视图的函数
// - `children` 属性:一个特殊的属性,包含作为视图中组件子节点传递的内容,
//   而不是作为普通属性

#[component]
pub fn App() -> impl IntoView {
    let (items, set_items) = signal(vec![0, 1, 2]);
    let render_prop = move || {
        let len = move || items.read().len();
        view! {
            <p>"Length: " {len}</p>
        }
    };

    view! {
        // 此组件仅显示两种类型的子节点,并将它们嵌入到其他标记中
        <TakesChildren
            // 对于组件属性,你可以使用简写形式
            // `render_prop=render_prop` => `render_prop`
            // (这不适用于 HTML 元素特性 (attributes))
            render_prop
        >
            // 这些看起来就像 HTML 元素的子节点
            <p>"Here's a child."</p>
            <p>"Here's another child."</p>
        </TakesChildren>
        <hr/>
        // 此组件实际上会遍历并包裹子节点
        <WrapsChildren>
            <p>"Here's a child."</p>
            <p>"Here's another child."</p>
        </WrapsChildren>
    }
}

/// 在标记中显示一个 `render_prop` 和一些子节点。
#[component]
pub fn TakesChildren<F, IV>(
    /// 接收一个函数(类型为 F),该函数返回任何可以
    /// 转换为 View 的内容(类型为 IV)
    render_prop: F,
    /// `children` 接收 `Children` 类型
    /// 这是 `Box<dyn FnOnce() -> Fragment>` 的别名
    /// ……难道你不庆幸我们把它命名为 `Children` 吗?
    children: Children,
) -> impl IntoView
where
    F: Fn() -> IV,
    IV: IntoView,
{
    view! {
        <h1><code>"<TakesChildren/>"</code></h1>
        <h2>"Render Prop"</h2>
        {render_prop()}
        <hr/>
        <h2>"Children"</h2>
        {children()}
    }
}

/// 将每个子节点包裹在 `<li>` 中并嵌入到 `<ul>` 中。
#[component]
pub fn WrapsChildren(children: ChildrenFragment) -> impl IntoView {
    // children() 返回一个 `Fragment`,它有一个
    // `nodes` 字段,其中包含一个 Vec<View>
    // 这意味着我们可以遍历子节点来创建一些新的东西!
    let children = children()
        .nodes
        .into_iter()
        .map(|child| view! { <li>{child}</li> })
        .collect::<Vec<_>>();

    view! {
        <h1><code>"<WrapsChildren/>"</code></h1>
        // 将包装好的子节点放入 UL 中
        <ul>{children}</ul>
    }
}

fn main() {
    leptos::mount::mount_to_body(App)
}