html! 宏
基本用法
use euv::*;
html! {
div {
class: c_container()
h1 { "Hello, euv!" }
button {
onclick: move |_| { /* handle click */ }
"Click me"
}
}
}字符串属性
html! {
input {
r#type: "text"
placeholder: "Enter your name"
value: "default value"
}
}提示
Rust 关键字(如 type)需使用 r# 前缀:r#type: "text"
信号属性
let count: Signal<i32> = use_signal(|| 0);
html! {
span { count }
}事件属性
事件闭包签名为 FnMut(Event),其中 Event 是 web_sys::Event:
html! {
button {
onclick: move |event: Event| {
// 处理点击
}
"Click me"
}
}CSS 类属性
html! {
div {
class: c_container()
"Content"
}
}多 class 合并
同一元素上可以声明多个 class: 属性,框架会自动将它们合并为一个空格分隔的类名字符串:
html! {
div {
class: c_flex_row()
class: c_padding()
class: c_card()
"Multiple classes"
}
}提示
多个 class: 属性在编译时自动合并,效果等同于将所有类名拼接。如果其中任何一个 class 值是响应式 Signal,合并后的结果也会自动成为响应式属性。
多 style 合并
同一元素上可以声明多个 style: 属性,框架会自动将它们合并为完整的 CSS 样式字符串:
html! {
div {
style: {display: "flex"; gap: "10px";}
style: {padding: "20px"; background: "white";}
"Merged styles"
}
}提示
多个 style: 属性在编译时自动合并。如果其中任何一个 style 包含响应式 if 条件,合并后的结果也会自动成为响应式属性。
内联样式属性
支持两种语法——对象语法和表达式语法:
对象语法
html! {
div {
style: {display: "flex"; padding: "10px"; font_size: "14px";}
"Content"
}
}动态样式值
样式属性值支持花括号包裹的动态表达式:
html! {
div {
style: {background: {color};}
"Dynamic background"
}
}样式中的条件渲染
样式属性值同样支持 if 条件渲染,实现响应式的样式切换:
let is_active: Signal<bool> = use_signal(|| false);
html! {
div {
style: {
color: if { is_active.get() } { "#4f46e5".to_string() } else { "inherit".to_string() };
font_weight: if { is_active.get() } { "700".to_string() } else { "400".to_string() };
}
"Conditional styles"
}
}提示
样式属性值中的 if 条件会自动包装为响应式 Signal<String>,信号变化时样式自动更新。
提示
内联样式使用 snake_case,自动转换为 kebab-case(如 font_size → font-size)。
布尔属性
let agree: Signal<bool> = use_signal(|| true);
html! {
input {
r#type: "checkbox"
checked: agree
}
}自定义属性
html! {
div {
data_role: "container"
data_id: "12345"
aria_label: "Demo section"
"Custom attributes"
}
}提示
data_* 和 aria_* 属性自动转换为 data-* 和 aria-* 格式。使用 r# 前缀处理 Rust 保留字。
条件渲染
if/else
html! {
if {show.get()} {
div { "Visible" }
} else {
""
}
}else if 链
html! {
if {score.get() >= 90} {
div { "Excellent" }
} else if {score.get() >= 60} {
div { "Pass" }
} else {
div { "Fail" }
}
}match 表达式
html! {
match {route.get().as_str()} {
"/" => { page_home() }
"/about" => { page_about() }
_ => { page_not_found() }
}
}注意
if 的 else 分支不能省略,match 必须包含 _ 通配分支。
属性值条件渲染
if 条件渲染不仅可用于子节点位置,还可在属性值中使用,实现响应式的属性切换:
let is_active: Signal<bool> = use_signal(|| false);
html! {
div {
class: if { is_active.get() } { c_active() } else { c_inactive() }
"Content"
}
}样式中同样支持条件渲染:
html! {
div {
style: {
color: if { is_active.get() } { "#4f46e5".to_string() } else { "inherit".to_string() };
border_bottom: if { is_active.get() } { "2px solid #4f46e5".to_string() } else { "2px solid transparent".to_string() };
}
"Tab item"
}
}提示
属性值中的 if 条件会自动包装为响应式 Signal<String>,信号变化时属性值自动更新,无需手动订阅。
列表渲染(for 循环)
html! 宏支持 for 循环语法来渲染动态列表:
let items: Signal<Vec<String>> = use_signal(|| vec!["Rust".to_string(), "euv".to_string()]);
html! {
ul {
for item in {items.get()} {
li { item }
}
}
}带索引的循环:
html! {
ul {
for (index, item) in {items.get().iter().enumerate()} {
li { item }
}
}
}提示
for 循环中的迭代表达式需用花括号 {} 包裹。每次信号变化时,循环体会自动重新求值。
带 key 的列表渲染
为列表项添加 key 属性可以启用 Keyed Diffing,优化列表重排序时的 DOM 操作:
struct Item {
id: String,
name: String,
}
html! {
ul {
for item in {items.get()} {
li {
key: item.id
item.name
}
}
}
}提示
key 属性帮助框架识别哪些节点可以复用。当列表项的顺序变化时,Keyed Diffing 通过 key 匹配已有 DOM 节点,仅移动节点而不重建,提升性能。如果列表项没有 key,框架按位置逐一对比。
字符串字面量标签(Web Components)
html! 宏支持使用字符串字面量作为标签名,用于渲染自定义 HTML5 元素(如 Web Components)。字符串字面量标签始终生成 Tag::Element,不会被识别为组件:
html! {
"my-custom-element" {
class: c_container()
"Web Component content"
}
"paper-button" {
raised: true
"Material button"
}
}提示
字符串字面量标签适用于渲染 Web Components 或自定义 HTML5 元素。标识符标签(如 div、my_card)如果对应 #[component] 标记的函数,会被识别为组件;否则作为原生 HTML 元素处理。字符串字面量标签则始终作为原生 HTML 元素,即使存在同名组件函数。
动态标签
html! 宏支持动态标签语法 {tag_expr} { content },标签名在运行时根据信号值决定。标签可以是原生 HTML 元素,也可以是用户组件:
let tag_name: Signal<String> = use_signal(|| "div".to_string());
let content: Signal<String> = use_signal(|| "Hello, dynamic tag!".to_string());
html! {
div {
// 标签名由 tag_name 信号决定
{ tag_name.get() } {
"Hello, dynamic tag!"
}
}
}动态标签支持传入属性:
html! {
{ tag_name.get() } {
title: "Dynamic my_card"
onclick: move |_event: Event| { /* handle */ }
{ content.get() }
}
}提示
动态标签通过 {expr} 语法将标签名变为响应式,信号变化时自动重新渲染。如果动态标签名对应 #[component] 标记的函数,会被识别为组件;否则作为原生 HTML 元素处理。
注意
动态标签切换时,整个标签及其子树会被替换,不会进行增量 Diff。
动态属性键
html! 宏支持动态属性键语法 {key}: value,属性名在运行时根据变量值决定:
let dynamic_key: Signal<String> = use_signal(|| "data-custom".to_string());
let dynamic_value: Signal<String> = use_signal(String::new);
html! {
div {
{ dynamic_key.get() }: dynamic_value
class: c_demo()
"Dynamic attribute"
}
}静态属性键也支持表达式形式:
let static_key: String = "data-role".to_string();
let static_value: String = "container".to_string();
html! {
div {
{ static_key }: static_value
"Static key, static value"
}
}class! 宏同样支持动态 CSS 属性键:
let class_prop_key: Signal<String> = use_signal(|| "background".to_string());
let class_prop_value: Signal<String> = use_signal(|| "#4f46e5".to_string());
class! {
pub c_dynamic_demo(key: &str, value: &str) {
{key}: {value};
}
}
html! {
div {
class: c_dynamic_demo(&class_prop_key.get(), &class_prop_value.get())
"Dynamic CSS property"
}
}提示
动态属性键通过 {expr} 语法将属性名变为响应式,常用于 data-* 自定义属性或动态 CSS 属性等场景。属性键和属性值都可以是动态表达式。
嵌入表达式
动态表达式(响应式)
使用花括号 {expr} 包裹的表达式会自动包装为 DynamicNode,信号变化时自动重新渲染:
html! {
div {
{format!("Count: {}", count.get())}
}
}静态表达式
裸标识符(无花括号)通过 IntoNode 进行静态一次性转换,不会响应信号变化:
html! {
div {
count
}
}组件标签
使用 #[component] 属性宏标记的函数会被 html! 宏识别为自定义组件。在 html! 宏中使用同名标签即可调用该组件,框架自动将属性映射到 Props 结构体字段:
html! {
my_card {
title: "Card Title"
p { "Card content" }
}
}等价于调用 my_card(MyCardProps { title: "Card Title", children: html! { p { "Card content" } }, ..Default::default() })。
#[derive(Default)]
struct MyCardProps {
title: &'static str,
children: VirtualNode,
}
#[component]
pub fn my_card(node: VirtualNode<MyCardProps>) -> VirtualNode {
let MyCardProps { title, children, .. } = node.try_get_props().unwrap_or_default();
html! {
div {
h3 { title }
children
}
}
}提示
组件函数必须使用 #[component] 属性宏标记,html! 宏在编译时扫描项目源码查找所有 #[component] 标记的函数。未标记的标识符一律被视为原生 HTML 元素。组件的属性通过类型化 Props 结构体接收,未传递的字段使用 Default::default() 填充。