组件子元素
将子元素传递到组件中是很常见的,就像您可以将子元素传递到 HTML 元素中一样。例如,想象我有一个增强 HTML <form> 的 <FancyForm/> 组件。我需要某种方法来传递所有的输入。
view! {
<FancyForm>
<fieldset>
<label>
"Some Input"
<input type="text" name="something"/>
</label>
</fieldset>
<button>"Submit"</button>
</FancyForm>
}
在 Leptos 中如何做到这一点?基本上有两种方法将组件传递给其他组件:
- render props:返回视图的函数属性
childrenprop:一个特殊的组件属性,包括您作为子元素传递给组件的任何内容。
实际上,您已经在 <Show/> 组件中看到了这两种方法的实际应用:
view! {
<Show
// `when` 是一个普通 prop
when=move || value.get() > 5
// `fallback` 是一个 "render prop":返回视图的函数
fallback=|| view! { <Small/> }
>
// `<Big/>` (以及这里的任何其他内容)
// 将被给予 `children` prop
<Big/>
</Show>
}
让我们定义一个接受一些子元素和 render prop 的组件。
/// 在标记内显示 `render_prop` 和一些子元素。
#[component]
pub fn TakesChildren<F, IV>(
/// 接受一个函数(类型 F),返回任何可以转换为 View 的内容(类型 IV)
render_prop: F,
/// `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()}
}
}
render_prop 和 children 都是函数,所以我们可以调用它们来生成适当的视图。特别是 children 是 Box<dyn FnOnce() -> AnyView> 的别名。(您不是很高兴我们将其命名为 Children 而不是这个吗?)这里返回的 AnyView 是一个不透明的、类型擦除的视图:您无法检查它。还有各种其他子元素类型:例如,ChildrenFragment 将返回一个 Fragment,这是一个可以迭代其子元素的集合。
如果您在这里需要
Fn或FnMut,因为您需要多次调用children,我们还提供了ChildrenFn和ChildrenMut别名。
我们可以这样使用组件:
view! {
<TakesChildren render_prop=|| view! { <p>"Hi, there!"</p> }>
// 这些被传递给 `children`
"Some text"
<span>"A span"</span>
</TakesChildren>
}
类型化子元素:Slots
到目前为止,我们讨论了具有单个 children prop 的组件,但有时创建具有不同类型的多个子元素的组件很有用。例如:
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。
#[slot] 宏将普通的 Rust 结构体注释为组件 slot:
// 用 `#[slot]` 注释的简单结构体,
// 期望子元素
#[slot]
struct Then {
children: ChildrenFn,
}
这个 slot 可以用作组件中的 prop:
#[component]
fn If(
condition: Signal<bool>,
// 组件 slot,应该通过 <Then slot> 语法传递
then_slot: Then,
) -> impl IntoView {
move || {
if condition.get() {
(then_slot.children)().into_any()
} else {
().into_any()
}
}
}
现在,If 组件期望一个 Then 类型的子元素。您需要用 slot:<prop_name> 注释使用的 slot:
view! {
<If condition=a_is_true>
// `If` 组件总是期望 `then_slot` 的 `Then` 子元素
<Then slot:then_slot>"Show content when a is true"</Then>
</If>
}
指定不带名称的
slot将默认选择的 slot 为结构体名称的蛇形命名版本。所以在这种情况下<Then slot>等同于<Then slot:then>。
有关完整示例,请参阅 slots 示例。
Slots 上的事件处理程序
事件处理程序不能直接在 slots 上指定,如下所示:
<ComponentWithSlot>
// ⚠️ 直接在 slot 上的事件处理程序 `on:click` 是不允许的
<SlotWithChildren slot:slot on:click=move |_| {}>
<h1>"Hello, World!"</h1>
</SlotWithChildren>
</ComponentWithSlot>
相反,将 slot 内容包装在常规元素中并在那里附加事件处理程序:
<ComponentWithSlot>
<SlotWithChildren slot:slot>
// ✅ 事件处理程序不是直接在 slot 上定义的
<div on:click=move |_| {}>
<h1>"Hello, World!"</h1>
</div>
</SlotWithChildren>
</ComponentWithSlot>
操作子元素
Fragment 类型基本上是包装 Vec<AnyView> 的方法。您可以将其插入到视图中的任何地方。
但您也可以直接访问这些内部视图来操作它们。例如,这是一个接受其子元素并将它们转换为无序列表的组件。
/// 将每个子元素包装在 `<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>
}
}
像这样调用它将创建一个列表:
view! {
<WrapsChildren>
"A"
"B"
"C"
</WrapsChildren>
}
CodeSandbox 源码
use leptos::prelude::*;
// 通常,您想要将某种子视图传递给另一个组件。
// 有两种基本模式来做到这一点:
// - "render props":创建一个接受创建视图的函数的组件 prop
// - `children` prop:一个特殊属性,包含作为组件子元素传递的内容
// 在您的视图中,而不是作为属性
#[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
// 对于组件 props,您可以简写
// `render_prop=render_prop` => `render_prop`
// (这对 HTML 元素属性不起作用)
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)
}