8个版本
0.3.2 | 2024年6月25日 |
---|---|
0.3.1 | 2024年3月31日 |
0.3.0 | 2024年2月22日 |
0.2.3 | 2024年1月27日 |
0.1.0 | 2023年9月30日 |
#346 in Rust模式
39KB
Leptos mview
受maud启发的,为Leptos提供的一种替代的view!
宏。
示例
语法预览
use leptos::*;
use leptos_mview::mview;
#[component]
fn MyComponent() -> impl IntoView {
let (value, set_value) = create_signal(String::new());
let red_input = move || value().len() % 2 == 0;
mview! {
h1.title { "A great website" }
br;
input
type="text"
data-index=0
class:red={red_input}
prop:{value}
on:change={move |ev| {
set_value(event_target_value(&ev))
}};
Show
when=[!value().is_empty()]
fallback=[mview! { "..." }]
{
Await
future=[fetch_from_db(value())]
blocking
|db_info| {
p { "Things found: " strong { {*db_info} } "!" }
p { "Is bad: " f["{}", red_input()] }
}
}
}
}
async fn fetch_from_db(data: String) -> usize { data.len() }
示例说明
use leptos::*;
use leptos_mview::mview;
#[component]
fn MyComponent() -> impl IntoView {
let (value, set_value) = create_signal(String::new());
let red_input = move || value().len() % 2 == 0;
mview! {
// specify tags and attributes, children go in braces
// classes (and ids) can be added like CSS selectors.
// same as `h1 class="title"`
h1.title { "A great website" }
// elements with no children end with a semi-colon
br;
input
type="text"
data-index=0 // kebab-cased identifiers supported
class:red={red_input} // non-literal values must be wrapped in braces
prop:{value} // shorthand! same as `prop:value={value}`
on:change={move |ev| { // event handlers same as leptos
set_value(event_target_value(&ev))
}};
Show
// values wrapped in brackets `[body]` are expanded to `{move || body}`
when=[!value().is_empty()] // `{move || !value().is_empty()}`
fallback=[mview! { "..." }] // `{move || mview! { "..." }}`
{ // I recommend placing children like this when attributes are multi-line
Await
future=[fetch_from_db(value())]
blocking // expanded to `blocking=true`
// children take arguments with a 'closure'
// this is very different to `let:db_info` in Leptos!
|db_info| {
p { "Things found: " strong { {*db_info} } "!" }
// bracketed expansion works in children too!
// this one also has a special prefix to add `format!` into the expansion!
// {move || format!("{}", red_input()}
p { "Is bad: " f["{}", red_input()] }
}
}
}
}
// fake async function
async fn fetch_from_db(data: String) -> usize { data.len() }
目的
Leptos中的view!
宏通常是组件中最大的部分,在编写复杂组件时可能会变得非常长。此宏旨在尽可能简洁,尝试最小化不必要的标点符号/单词和缩短常见模式。
性能提示
目前,该宏扩展为类似builder语法,但在SSR模式下有一些性能缺点。这预计将在Leptos的新的渲染器(Leptos 0.7
)中得到修复,因此我不会实现这一点。
兼容性
此宏将与Leptos的最新稳定版本兼容。该宏使用::leptos::...
引用Leptos项,没有从这个crate重新导出任何项。因此,如果没有更改与视图相关的项,此crate很可能与任何Leptos版本兼容。
以下是我测试过的与之兼容的版本。该宏可能与更多版本的Leptos兼容。
leptos_mview 版本 |
兼容的leptos 版本 |
---|---|
0.1 |
0.5 |
0.2 |
0.5 , 0.6 |
0.3 |
0.6 |
语法细节
元素
元素具有以下结构
- 元素 / 组件标签名 / 路径 (
div
,App
,component::Codeblock
)。 - 任何以点
.
或哈希#
开头的前缀类或id。 - 属性和指令的空格分隔列表 (
class="primary"
,on:click={...}
)。 - 可以是括号/圆括号中的子元素(如
{ "hi!" }
或("hi")
),也可以是分号表示没有子元素(如;
)。
示例
mview! {
div.primary { strong { "hello world" } }
input type="text" on:input={handle_input};
MyComponent data=3 other="hi";
}
添加泛型与Leptos相同:直接在组件名称后添加,带或不带箭头。
#[component]
pub fn GenericComponent<S>(ty: PhantomData<S>) -> impl IntoView {
std::any::type_name::<S>()
}
#[component]
pub fn App() -> impl IntoView {
mview! {
// both with and without turbofish is supported
GenericComponent::<String> ty={PhantomData};
GenericComponent<usize> ty={PhantomData};
GenericComponent<i32> ty={PhantomData};
}
}
注意,由于保留语法,用于ids的#
前面必须有空格。
mview! {
nav #primary { "..." }
// not allowed: nav#primary { "..." }
}
使用选择器语法创建的类/ids可以与属性class="..."
和指令class:a-class={signal}
混合使用。
插槽
插槽(另一个示例)通过在父元素的子元素中给结构体前缀slot:
来支持。
组件函数中参数的名称必须与插槽的名称相同,使用蛇形命名法。
use leptos::*;
use leptos_mview::mview;
#[component]
pub fn App() -> impl IntoView {
let (count, set_count) = RwSignal::new(0).split();
let is_even = MaybeSignal::derive(move || count() % 2 == 0);
let is_div5 = MaybeSignal::derive(move || count() % 5 == 0);
let is_div7 = MaybeSignal::derive(move || count() % 7 == 0);
mview! {
SlotIf cond={is_even} {
slot:Then { "even" }
slot:ElseIf cond={is_div5} { "divisible by 5" }
slot:ElseIf cond={is_div7} { "divisible by 7" }
slot:Fallback { "odd" }
}
}
}
值
目前有3种主要类型的值可以传递
-
字面量可以直接传递给属性值(如
data=3
、class="main"
、checked=true
)。- 但是,子元素不接受字面量数字或布尔值,只接受字符串。
mview! { p { "this works " 0 " times: " true } }
- 但是,子元素不接受字面量数字或布尔值,只接受字符串。
-
其他所有内容都必须以块的形式传递,包括变量、闭包或表达式。
mview! { input class="main" checked=true madeup=3 type={input_type} on:input={move |_| handle_input(1)}; }
这不行
let input_type = "text"; // ❌ This is not valid! Wrap input_type in braces. mview! { input type=input_type }
-
用括号包裹的值(如
value=[a_bool().to_string()]
)是空的闭包块(move || ...
)的快捷方式(到value={move || a_bool().to_string()}
)。mview! { Show fallback=[()] // common for not wanting a fallback as `|| ()` when=[number() % 2 == 0] // `{move || number() % 2 == 0}` { "number + 1 = " [number() + 1] // works in children too! } }
-
注意,这始终展开为
move || ...
:对于任何接受参数的闭包,请使用完整的闭包块。mview! { input type="text" on:click=[log!("THIS DOESNT WORK")]; }
相反
mview! { input type="text" on:click={|_| log!("THIS WORKS!")}; }
-
括号中的值也可以有一些特殊前缀,以实现更多常见快捷方式!
- 目前,唯一的一个是
f
- 例如f["{:.2}", stuff()]
。添加一个f
将会在闭包中添加format!
。这相当于[format!("{:.2}", stuff())]
或者{move || format!("{:.2}", stuff())}
。
属性
键值属性
大多数属性都是 key=value
对。这里的 value
遵循上述规则。而 key
有几种变体
-
标准标识符:如
type
、an_attribute
、class
、id
等等都是有效的键。 -
短横线标识符:标识符可以是短横线格式,例如
data-value
、an-attribute
。-
注意:在 HTML 元素中,这将被直接放在元素上:
div data-index="0";
变成<div data-index="0"></div>
。在 组件 中,短横线会被转换为下划线,然后传递给组件构建器。例如,这个组件
#[component] fn Something(some_attribute: i32) -> impl IntoView { ... }
可以在其他地方这样使用
mview! { Something some-attribute=5; }
并且
some-attribute
将被传递到some_attribute
参数。
-
-
属性缩写:如果属性名和值相同,例如
class={class}
,你可以用{class}
来表示相同的意思。let class = "these are classes"; let id = "primary"; mview! { div {class} {id} { "this has 3 classes and id='primary'" } }
参见:短横线标识符与属性缩写
请注意,Leptos 中的特殊属性 node_ref
或 ref
或 _ref
或 ref_
用来将元素绑定到变量,在这里等同于 ref={variable}
。
布尔属性
另一个快捷方式是布尔属性可以不添加 =true
来编写。但要注意!checked
与 {checked}
非常不同。
// recommend usually adding #[prop(optional)] to all these
#[component]
fn LotsOfFlags(wide: bool, tall: bool, red: bool, curvy: bool, count: i32) -> impl IntoView {}
mview! { LotsOfFlags wide tall red=false curvy count=3; }
// same as...
mview! { LotsOfFlags wide=true tall=true red=false curvy=true count=3; }
指令
一些特殊属性(通过 :
区分)称为 指令,具有特殊功能。它们都与 Leptos 具有相同的行怍。这些包括
class:class-name=[何时显示]
style:style-key=[style value]
on:event={move |ev|事件处理程序}
prop:属性-name={signal}
attr:name={值}
clone:ident_to_clone
use:directive_name
或use:directive_name={params}
除了 clone
以外,所有这些指令还支持属性简写
let color = create_rw_signal("red".to_string());
let disabled = false;
mview! {
div style:{color} class:{disabled};
}
类(class
)和样式(style
)指令还支持使用字符串字面量,以支持更复杂的名称。请确保 class:
的字符串没有空格,否则将引发恐慌!
let yes = move || true;
mview! {
div class:"complex-[class]-name"={yes}
style:"doesn't-exist"="white";
}
请注意,use:
指令会自动对其参数调用 .into()
,与 Leptos 的行为一致。
特殊属性
您可以在组件上放置一些特殊属性来模拟仅适用于 HTML 元素的某些功能。
如果组件具有 class
属性,可以使用选择器语法 .some-class
和动态类 class:thing={signal}
传入!
#[component]
// the `class` parameter should have these attributes and type to work properly
fn TakesClasses(#[prop(optional, into)] class: TextProp) -> impl IntoView {
mview! {
// "my-component" will always be present, extra classes passed in will also be added
div.my-component class=[class.get()] { "..." }
}
}
// <div class="my-component extra-class">
mview! {
TakesClasses.extra-class;
};
建议仅传入静态类(即具有选择器或仅为纯 class="..."
),因为使用动态类需要每次信号更改时都构建一个新的字符串;尽管如此,仍然支持动态类。
let signal = RwSignal::new(true);
// <div class="my-component always-has-this special">
mview! {
TakesClasses.always-has-this class:special={signal};
}
signal.set(false);
// becomes <div class="my-component always-has-this">
与 HTML 元素上的 class:
语法有一点不同:传入的值必须是一个 Fn() -> bool
,而不能只是一个 bool
。
这也可以通过一个 id
属性来支持,用于转发 #my-id
,尽管不是响应式的。
#[component]
// the `id` parameter should have these attributes and type to work properly
fn TakesIds(#[prop(optional)] id: &'static str) -> impl IntoView {
mview! {
div {id} { "..." }
}
}
// <div id="my-unique-id">
mview! {
TakesIds #my-unique-id;
};
这也可以通过在插槽上具有与上面组件相同的属性和类型的 class
和 id
字段来支持。
子元素
您可能已经注意到,在指令属性的前一节中缺少了 let:data
属性!
这被替换为在子元素块之前的一个闭包。这样,您可以更轻松地传递多个参数给子元素。
mview! {
Await
future=[async { 3 }]
|monkeys| {
p { {*monkeys} " little monkeys, jumping on the bed." }
}
}
请注意,你通常需要在使用的数据前添加一个 *
。如果你忘记了,rust-analyser 会提示你在下面这样引用:*{monkeys}
。这显然是不正确的 - 应将其放在大括号内。(如果有人知道如何修复这个问题,欢迎贡献力量!)
子元素可以包裹在大括号或括号中,任选其一。
mview! {
p {
"my " strong("bold") " and " em("fancy") " text."
}
}
以下是上一节关于值的总结,以防你错过了:子元素可以是字面字符串(不是布尔值或数字!),包含 Rust 代码的块({*monkeys}
),或者闭包简写 [number() + 1]
。
闭包子元素也支持在插槽中使用,添加字段 children: Callback<T, View>
来使用它(T
是你想要的任何类型)。
其他详细信息
带属性简写的短横线命名标识符
如果属性简写中有短横线
-
在组件上,键和值都将转换为下划线。
let some_attribute = 5; mview! { Something {some-attribute}; } // same as... mview! { Something {some_attribute}; } // same as... mview! { Something some_attribute={some_attribute}; }
-
在 HTML 元素上,键将保持短横线,但值将转换为带下划线的标识符。
let aria_label = "a good label"; mview! { input {aria-label}; } // same as... mview! { input aria-label={aria_label}; }
HTML 元素上的布尔属性
注意来自 Leptos 的行为:将 HTML 属性设置为 true 时,会添加没有值关联的属性。
use leptos::view;
view! { <input type="checkbox" checked=true data-smth=true not-here=false /> }
变为 <input type="checkbox" checked data-smth />
,而不是 checked="true"
或 data-smth="true"
或 not-here="false"
。
要使属性值为 "true" 或 "false" 字符串,请在 bool 上使用 .to_string()
。如果你还在处理信号,请确保它在闭包中。
let boolean_signal = RwSignal::new(true);
mview! { input type="checkbox" checked=[boolean_signal().to_string()]; }
// or, if you prefer
mview! { input type="checkbox" checked=f["{}", boolean_signal()]; }
贡献
如果你有功能想法/要报告的错误/反馈,请随时创建 PR/issue :)
依赖项
~0.4–0.9MB
~20K SLoC