#leptos #view #macro

leptos-mview

Leptos的简洁视图宏

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模式

MIT/Apache

39KB

Leptos mview

crates.io

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

语法细节

元素

元素具有以下结构

  1. 元素 / 组件标签名 / 路径 (div, App, component::Codeblock)。
  2. 任何以点.或哈希#开头的前缀类或id。
  3. 属性和指令的空格分隔列表 (class="primary", on:click={...})。
  4. 可以是括号/圆括号中的子元素(如{ "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:来支持。

组件函数中参数的名称必须与插槽的名称相同,使用蛇形命名法。

使用SlotIf示例中定义的插槽

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=3class="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 有几种变体

请注意,Leptos 中的特殊属性 node_refref_refref_ 用来将元素绑定到变量,在这里等同于 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; }

参见:HTML 元素上的布尔属性

指令

一些特殊属性(通过 : 区分)称为 指令,具有特殊功能。它们都与 Leptos 具有相同的行怍。这些包括

  • class:class-name=[何时显示]
  • style:style-key=[style value]
  • on:event={move |ev|事件处理程序}
  • prop:属性-name={signal}
  • attr:name={}
  • clone:ident_to_clone
  • use:directive_nameuse: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;
};

这也可以通过在插槽上具有与上面组件相同的属性和类型的 classid 字段来支持。

子元素

您可能已经注意到,在指令属性的前一节中缺少了 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