组件子节点
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:
-
渲染属性 (render props):本身是返回视图的函数的属性。
-
children属性:一个特殊的组件属性,包含你作为子节点传递给组件的任何内容。 -
render props: properties that are functions that return a view
-
the
childrenprop: 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_prop 和 children 都是函数,所以我们可以通过调用它们来生成相应的视图。特别是 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,因此这里需要Fn或FnMut,我们也提供了ChildrenFn和ChildrenMut别名。If you need a
FnorFnMuthere because you need to callchildrenmore than once, we also provideChildrenFnandChildrenMutaliases.
我们可以像这样使用该组件:
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
slotwithout 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 源代码
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)
}