6.4 组件属性
> **本节目标**:完整掌握组件属性的类型、传递方式、默认值、验证和双向绑定。
6.4.1 属性的类型
组件属性可以是任何 `Clone` 类型:
基础类型
#[derive(IModel)]
#[component]
pub struct Button {
pub text: SharedString, // 字符串
pub count: i32, // 整数
pub rate: f64, // 浮点数
pub disabled: bool, // 布尔
pub size: SharedString, // 枚举(用字符串表示)
}
集合类型
#[derive(IModel)]
#[component]
pub struct List {
pub items: Vec<Item>, // 列表
pub selected_ids: HashSet<u64>, // 集合
}
Option 类型
#[derive(IModel)]
#[component]
pub struct Avatar {
pub src: SharedString,
pub alt: Option<SharedString>, // 可选属性
}
自定义类型
#[derive(IModel, Clone)]
pub struct User {
pub id: u64,
pub name: SharedString,
pub email: SharedString,
}
#[derive(IModel)]
#[component]
pub struct UserCard {
pub user: User, // 自定义类型
}
枚举类型
#[derive(Clone, PartialEq)]
pub enum ButtonVariant {
Primary,
Secondary,
Danger,
Ghost,
}
#[derive(IModel)]
#[component]
pub struct Button {
pub variant: ButtonVariant, // 枚举
}
6.4.2 属性的传递
字面量传递
<Button text="提交" variant="primary" size="large" disabled={true} />
表达式传递
<Button text={button_label} disabled={is_loading} />
字段引用传递
<UserCard user={current_user} />
<Avatar src={user.avatar} alt={user.name} />
布尔属性的简写
<!-- 完整写法 -->
<Button disabled={true}>提交</Button>
<!-- 简写(仅 true 可省略) -->
<Button disabled>提交</Button>
复杂表达式
<Button
text={if is_loading { "加载中..." } else { "提交" }}
disabled={is_loading || !is_form_valid}
/>
6.4.3 属性的默认值
通过构造函数
impl Button {
pub fn new() -> Self {
Self {
text: SharedString::default(),
variant: "primary".into(), // 默认主要样式
size: "medium".into(), // 默认中等尺寸
disabled: false, // 默认启用
}
}
}
通过 `#[prop]` 属性
#[derive(IModel)]
#[component]
pub struct Button {
pub text: SharedString,
#[prop(default = "primary")]
pub variant: SharedString,
#[prop(default = "medium")]
pub size: SharedString,
#[prop(default = false)]
pub disabled: bool,
}
在 `.rml` 中使用默认值
<!-- 使用默认值 -->
<Button text="提交" />
<!-- 等价于 -->
<Button text="提交" variant="primary" size="medium" disabled={false} />
6.4.4 属性的响应式
当属性变化时,组件会自动重新渲染:
<!-- 父视图 -->
<div>
<p>当前用户: {user_name}</p>
<Button text={button_text} disabled={is_loading} />
</div>
#[derive(IModel)]
#[component]
pub struct ParentView {
pub user_name: SharedString,
pub button_text: SharedString,
pub is_loading: bool,
}
impl ParentView {
#[command]
pub fn submit(&mut self, _: &ClickEvent, cx: &mut Context<Self>) {
self.is_loading = true;
self.button_text = "加载中...".into();
cx.notify();
cx.spawn(|this, mut cx| async move {
// 异步操作...
let _ = this.update(&mut cx, |this, cx| {
this.is_loading = false;
this.button_text = "提交".into();
cx.notify();
});
}).detach();
}
}
监听属性变化
用 `#[on_prop_change]` 监听属性变化:
#[derive(IModel)]
#[component]
pub struct DataView {
pub data_id: u64,
pub data: Option<Data>,
}
impl DataView {
#[on_prop_change(data_id)]
pub fn on_data_id_change(&mut self, cx: &mut Context<Self>) {
self.load_data(self.data_id, cx);
}
fn load_data(&mut self, id: u64, cx: &mut Context<Self>) {
cx.spawn(|this, mut cx| async move {
let data = fetch_data(id).await;
let _ = this.update(&mut cx, |this, cx| {
this.data = Some(data);
cx.notify();
});
}).detach();
}
}
6.4.5 双向绑定属性
组件可以实现双向绑定,让父视图通过 `value={field}` / `checked={field}` 绑定:
实现双向绑定
#[derive(IModel)]
#[component]
pub struct Counter {
pub value: i32,
pub min: i32,
pub max: i32,
}
impl TwoWayBinding for Counter {
type Value = i32;
fn get_value(&self) -> Self::Value {
self.value
}
fn set_value(&mut self, value: Self::Value, cx: &mut Context<Self>) {
self.value = value.clamp(self.min, self.max);
cx.notify();
}
}
在父视图中使用
<div>
<p>当前值: {my_count}</p>
<Counter value={my_count} min={0} max={100} />
</div>
#[derive(IModel)]
#[component]
pub struct MyView {
pub my_count: i32,
}
双向绑定的数据流
父视图 my_count ──属性──▶ Counter value
父视图 my_count ◀──事件── Counter value
1. 父视图的 `my_count` 变化 → 更新 Counter 的 `value` 2. Counter 内部修改 `value` → 同步到父视图的 `my_count`
6.4.6 属性的验证
构造时验证
impl ProgressBar {
pub fn new(value: f64) -> Self {
Self {
value: value.clamp(0.0, 100.0), // 验证范围
}
}
}
Setter 验证
impl ProgressBar {
pub fn set_value(&mut self, value: f64, cx: &mut Context<Self>) {
self.value = value.clamp(0.0, 100.0);
cx.notify();
}
}
`#[on_prop_change]` 验证
#[on_prop_change(value)]
pub fn on_value_change(&mut self, cx: &mut Context<Self>) {
if self.value < 0.0 || self.value > 100.0 {
self.value = self.value.clamp(0.0, 100.0);
cx.notify();
}
}
6.4.7 属性的设计原则
1. 属性应尽量少
// ✅ 精简的属性
#[component]
pub struct Button {
pub text: SharedString,
pub variant: SharedString,
pub size: SharedString,
pub disabled: bool,
}
// ❌ 过多的属性
#[component]
pub struct Button {
pub text: SharedString,
pub text_color: SharedString,
pub bg_color: SharedString,
pub border_color: SharedString,
pub padding: f64,
pub margin: f64,
pub font_size: f64,
pub font_weight: f64,
// ... 太多了
}
2. 用枚举而非字符串
// ✅ 枚举
pub enum ButtonVariant {
Primary,
Secondary,
Danger,
Ghost,
}
// ❌ 字符串
pub variant: SharedString, // 容易拼写错误
3. 用组合而非继承
// ✅ 组合
#[component]
pub struct UserCard {
pub user: User,
pub avatar_size: AvatarSize,
}
// ❌ 继承
pub struct UserCardWithAvatar : UserCard { ... }
4. 提供合理的默认值
impl Button {
pub fn new() -> Self {
Self {
text: SharedString::default(),
variant: ButtonVariant::Primary, // 默认主要
size: ButtonSize::Medium, // 默认中等
disabled: false, // 默认启用
}
}
}
6.4.8 完整示例:进度条组件
// components/progress_bar.rml.rs
use rml::prelude::*;
#[derive(Clone, PartialEq)]
pub enum ProgressVariant {
Primary,
Success,
Warning,
Danger,
}
impl Default for ProgressVariant {
fn default() -> Self {
Self::Primary
}
}
#[derive(IModel)]
#[component]
pub struct ProgressBar {
pub value: f64,
pub max: f64,
pub variant: ProgressVariant,
pub show_label: bool,
pub label_format: SharedString,
}
impl ProgressBar {
pub fn new(value: f64) -> Self {
Self {
value: value.clamp(0.0, 100.0),
max: 100.0,
variant: ProgressVariant::default(),
show_label: true,
label_format: "{value}%".into(),
}
}
#[computed]
pub fn percentage(&self) -> f64 {
(self.value / self.max * 100.0).clamp(0.0, 100.0)
}
#[computed]
pub fn label(&self) -> SharedString {
self.label_format
.replace("{value}", &format!("{:.1}", self.percentage))
.into()
}
#[computed]
pub fn variant_class(&self) -> &'static str {
match self.variant {
ProgressVariant::Primary => "progress-primary",
ProgressVariant::Success => "progress-success",
ProgressVariant::Warning => "progress-warning",
ProgressVariant::Danger => "progress-danger",
}
}
}
impl TwoWayBinding for ProgressBar {
type Value = f64;
fn get_value(&self) -> Self::Value {
self.value
}
fn set_value(&mut self, value: Self::Value, cx: &mut Context<Self>) {
self.value = value.clamp(0.0, self.max);
cx.notify();
}
}
<!-- components/progress_bar.rml -->
<div class="progress-container">
<div class="progress-bar {variant_class}">
<div class="progress-fill" style="width: {percentage}%"></div>
</div>
<span if={show_label} class="progress-label">{label}</span>
</div>
使用进度条
<!-- views/upload_view.rml -->
<div class="upload-view">
<h1>文件上传</h1>
<input type="file" onchange={handle_file_select} />
<div if={is_uploading}>
<ProgressBar
value={upload_progress}
variant={upload_variant}
show_label={true}
/>
<button on-click={cancel_upload}>取消</button>
</div>
<div if={upload_complete}>
<p>上传完成!</p>
<ProgressBar value={100} variant="success" />
</div>
</div>
// views/upload_view.rml.rs
use rml::prelude::*;
use crate::components::progress_bar::{ProgressBar, ProgressVariant};
#[derive(IModel)]
#[component]
pub struct UploadView {
pub upload_progress: f64,
pub is_uploading: bool,
pub upload_complete: bool,
pub upload_variant: ProgressVariant,
}
impl UploadView {
pub fn new() -> Self {
Self {
upload_progress: 0.0,
is_uploading: false,
upload_complete: false,
upload_variant: ProgressVariant::Primary,
}
}
#[command]
pub fn handle_file_select(&mut self, ev: &ChangeEvent, cx: &mut Context<Self>) {
self.is_uploading = true;
self.upload_progress = 0.0;
self.upload_complete = false;
self.upload_variant = ProgressVariant::Primary;
cx.notify();
// 模拟上传
cx.spawn(|this, mut cx| async move {
for i in 1..=100 {
cx.background_executor()
.timer(std::time::Duration::from_millis(50))
.await;
let _ = this.update(&mut cx, |this, cx| {
this.upload_progress = i as f64;
cx.notify();
});
}
let _ = this.update(&mut cx, |this, cx| {
this.is_uploading = false;
this.upload_complete = true;
this.upload_variant = ProgressVariant::Success;
cx.notify();
});
}).detach();
}
#[command]
pub fn cancel_upload(&mut self, _: &ClickEvent, cx: &mut Context<Self>) {
self.is_uploading = false;
self.upload_progress = 0.0;
self.upload_variant = ProgressVariant::Danger;
cx.notify();
}
}
6.4.9 小结
组件属性是组件的输入接口:
- **类型**:基础类型、集合、Option、自定义类型、枚举 - **传递**:字面量、表达式、字段引用、布尔简写 - **默认值**:构造函数或 `#[prop(default = ...)]` - **响应式**:属性变化自动重新渲染 - **双向绑定**:实现 `TwoWayBinding` trait - **验证**:构造时、Setter、`#[on_prop_change]`
下一节 → [6.5 组件组合](./composition.md)