之前写过按钮组件算是简单尝试,决定试下稍微复杂一点的评分(Rate)组件。实现部分依旧搬照Element UI的实现,主要是熟悉Rust以及Yew这个框架的使用。
组件的属性定义在YewButtonProps
这个结构体,里面的属性的默认值的定义使用了#[prop_or_default]
和#[prop_or(0.0)]
这样的属性宏。属性宏有点像Java里面的注解,很奇怪的是如同Java书籍很少讲注解一样,实际中大量使用注解。Rust也是,手头的《Rust程序设计》和《Rust权威指南》对于宏的讲解很少,《Rust程序设计》根本没有属性宏,《Rust权威指南》有提到属性宏,只是举个使用的例子,根本没有讲怎么实现。实际中我发现Rust这些框架中也是大量的使用这些东西。
#[derive(Clone, PartialEq, Properties)]
pub struct YewButtonProps {
#[prop_or_default]
pub disabled: bool,
#[prop_or_default]
pub on_change: Callback<f64>,
#[prop_or(0.0)]
pub value:f64,
#[prop_or(5)]
pub max:i32,
#[prop_or(vec!["el-icon-star-on".to_string(); 3])]
pub icon_classes:Vec<String>,
#[prop_or(vec!["#F7BA2A".to_string(), "#F7BA2A".to_string(), "#F7BA2A".to_string()])]
pub colors:Vec<String>,
// ...
}
视图渲染方面,Yew跟Vue都使用内嵌HTML这样的方式,只不过Vue是单独一个Template,所以这块代码变化不大。
fn view(&self, ctx: &Context<Self>) -> Html {
let show_text = ctx.props().show_text.clone();
let max = ctx.props().max.clone();
let show_score = ctx.props().show_score.clone();
let text_color = ctx.props().text_color.clone();
let span_style = if self.is_rate_disabled() {
"cursor: auto"
} else {
"cursor: pointer"
};
let mut span_vec = vec![];
for i in 0..max {
span_vec.push(html!{
<span class={"el-rate__item"} style={span_style}>
<i class={self.get_classes(i)} style={self.get_icon_style(i)} onmousemove={ctx.link().callback(move |e: MouseEvent| {
Msg::OnMouseMove((i, e))
})} onmouseleave={ctx.link().callback(move |e: MouseEvent| {
Msg::OnMouseLeave
})} onclick={ctx.link().callback(move |_e: MouseEvent| {
Msg::OnSelectValue(i)
})}>
if self.is_show_decimal_icon(i+1) {
<i class={self.get_decimal_icon_class()} style={self.get_decimal_style()}/>
}
</i>
</span>
});
}
html! {
<div onkeydown={ctx.link().callback(move|e:KeyboardEvent|{
Msg::OnKeydown(e)
})} class="el-rate" role="slider" tabindex="0" aria-valuemin="0" aria-valuemax={max.to_string()} aria-valuenow={self.current_value.to_string()} >
{span_vec}
if show_score || show_text {
<span class="el-rate__text" style={format!("color: {}", text_color)}>{self.get_text()}</span>
}
</div>
}
}
列表渲染
分开来看下,Yew中对于列表渲染方式,我使用的传统for循环式,当然也可以用函数式的。
let mut span_vec = vec![];
for i in 0..max {
span_vec.push(html!{
<span class={"el-rate__item"} style={span_style}>
<i class={self.get_classes(i)} style={self.get_icon_style(i)} onmousemove={ctx.link().callback(move |e: MouseEvent| {
Msg::OnMouseMove((i, e))
})} onmouseleave={ctx.link().callback(move |e: MouseEvent| {
Msg::OnMouseLeave
})} onclick={ctx.link().callback(move |_e: MouseEvent| {
Msg::OnSelectValue(i)
})}>
if self.is_show_decimal_icon(i+1) {
<i class={self.get_decimal_icon_class()} style={self.get_decimal_style()}/>
}
</i>
</span>
});
}
条件渲染
在Yew中比较简单。
if show_score || show_text {
<span class="el-rate__text" style={format!("color: {}", text_color)}>{self.get_text()}</span>
}
事件处理
这里主要用到了鼠标以及键盘相关的事件
<i class={self.get_classes(i)} style={self.get_icon_style(i)} onmousemove={ctx.link().callback(move |e: MouseEvent| {
Msg::OnMouseMove((i, e))
})} onmouseleave={ctx.link().callback(move |e: MouseEvent| {
Msg::OnMouseLeave
})} onclick={ctx.link().callback(move |_e: MouseEvent| {
Msg::OnSelectValue(i)
})}>
Yew中每个事件处理结尾都需要返回发送一个Msg,所以事件的处理我都放在update里面了。
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::OnMouseMove((index, e))=> {
if self.is_rate_disabled() {
return false;
}
if self.props.allow_half {
let element: Element = e.target_unchecked_into();
let mut target = None;
if target.is_none() {
target = Some(element);
}
if target.is_some() {
let offset_x = e.offset_x()*2;
let client_width = target.clone().unwrap().client_width();
self.pointer_at_left_half = offset_x <= client_width;
self.current_value = if self.pointer_at_left_half {
(index+1) as f64 - 0.5
} else {
(index+1) as f64
};
}
} else {
self.current_value = (index+1) as f64;
}
self.hover_index = index+1;
true
},
// ...
}
在处理鼠标事件时遇到了一个坑,我按照Element UI的实现把事件放在<span/>
标签上,结果发现获取到的e.offset_x()
的值一直有问题,后来在Vue代码上测试发现是没有穿透的问题,就是Yew的这个设置鼠标事件只会在当前这个标签上,不会传递到下面的元素,这应该是Yew自己的设置。
其他问题
依赖问题
主要跟web-sys
的有关系。
let element: Element = e.target_unchecked_into();
let mut target = None;
if element.class_list().contains("el-rate__item") {
target = element.query_selector(".el-rate__icon").unwrap();
}
以上代码中用到了Element
这个类型,然后我就在Cargo.toml
中加入。
[dependencies.web-sys]
version = "0.3.9"
# We need to enable the `DomRect` feature in order to use the
# `get_bounding_client_rect` method.
features = [
"console",
"Element",
# ...
]
结果在调用class_list
这个方法时,老提示没有这个方法,但是我点击文档进去是有这个这个方法的,当时有点懵住,后来想起之前写记事本时也遇到这个问题,class_list
这个方法会返回DomTokenList
这个类型,不起作用就是因为我没有把这个类型也加入的问题。
[dependencies.web-sys]
version = "0.3.9"
# We need to enable the `DomRect` feature in order to use the
# `get_bounding_client_rect` method.
features = [
"console",
"Element",
"DomTokenList",
# ...
]
其他
里面用到了一些数学的方法,我本来想着用Rust自带的来着,后来发现js_sys
提供了这些。
总结
基本上一天多时间,感觉再这么下去可以弄一个Element UI的Yew版了,主要是个时间的问题了。
评分组件完整代码,yew_rate.rs
。
use std::collections::HashMap;
use web_sys::Element;
use yew::prelude::*;
pub enum Msg {
OnMouseMove((i32, MouseEvent)),
OnSelectValue(i32),
OnMouseLeave,
OnKeydown(KeyboardEvent)
}
pub struct YewRate {
pointer_at_left_half:bool,
current_value:f64,
hover_index:i32,
props:YewButtonProps
}
struct ClassMapVaule {
pub value:String,
pub excluded: bool
}
#[derive(Clone, PartialEq, Properties)]
pub struct YewButtonProps {
#[prop_or_default]
pub disabled: bool,
#[prop_or_default]
pub on_change: Callback<f64>,
#[prop_or(0.0)]
pub value:f64,
#[prop_or(5)]
pub max:i32,
#[prop_or(vec!["el-icon-star-on".to_string(); 3])]
pub icon_classes:Vec<String>,
#[prop_or(vec!["#F7BA2A".to_string(), "#F7BA2A".to_string(), "#F7BA2A".to_string()])]
pub colors:Vec<String>,
#[prop_or("#C6D1DE".to_string())]
pub void_color:String,
#[prop_or("#EFF2F7".to_string())]
pub disabled_void_color:String,
#[prop_or("el-icon-star-on".to_string())]
pub disabled_void_icon_class:String,
#[prop_or("el-icon-star-off".to_string())]
pub void_icon_class:String,
#[prop_or("#1f2d3d".to_string())]
pub text_color:String,
#[prop_or(2)]
pub low_threshold:i32,
#[prop_or(4)]
pub high_threshold:i32,
#[prop_or(false)]
pub allow_half:bool,
#[prop_or(false)]
pub show_score:bool,
#[prop_or(false)]
pub show_text:bool,
#[prop_or(vec!["极差".to_string(), "失望".to_string(), "一般".to_string(), "满意".to_string(), "惊喜".to_string()])]
pub texts:Vec<String>
}
impl Component for YewRate {
type Message = Msg;
type Properties = YewButtonProps;
fn create(ctx: &Context<Self>) -> Self {
Self {
pointer_at_left_half:true,
current_value: ctx.props().value,
hover_index:-1,
props: ctx.props().clone()
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::OnMouseMove((index, e))=> {
if self.is_rate_disabled() {
return false;
}
if self.props.allow_half {
let element: Element = e.target_unchecked_into();
let mut target = None;
// 这段代码原本是Element UI的实现,但是Yew的鼠标移动事件并不能穿透,所以这段代码弃用
// if element.class_list().contains("el-rate__item") {
// target = element.query_selector(".el-rate__icon").unwrap();
// }
// if target.is_some()&& target.clone().unwrap().class_list().contains("el-rate__decimal") {
// target = target.clone().unwrap().parent_element();
// } else if element.class_list().contains("el-rate__decimal") {
// target = element.parent_element();
// }
if target.is_none() {
target = Some(element);
}
if target.is_some() {
let offset_x = e.offset_x()*2;
let client_width = target.clone().unwrap().client_width();
self.pointer_at_left_half = offset_x <= client_width;
self.current_value = if self.pointer_at_left_half {
(index+1) as f64 - 0.5
} else {
(index+1) as f64
};
}
} else {
self.current_value = (index+1) as f64;
}
self.hover_index = index+1;
true
},
Msg::OnMouseLeave=>{
if self.is_rate_disabled() {
return false;
}
if self.props.allow_half {
self.pointer_at_left_half = self.props.value != js_sys::Math::floor(self.props.value);
}
self.current_value = self.props.value;
true
},
Msg::OnSelectValue(i)=>{
if self.is_rate_disabled() {
return false;
}
if self.props.allow_half && self.pointer_at_left_half {
self.props.value = self.current_value;
} else {
self.current_value = (i+1) as f64;
self.props.value = self.current_value;
}
self.props.on_change.emit(self.props.value);
self.hover_index = -1;
true
},
Msg::OnKeydown(event) => {
if self.is_rate_disabled() {
return false;
}
let mut current_value = self.current_value;
let keycode = event.key_code();
if keycode == 38 ||keycode == 39 {
if self.props.allow_half {
current_value += 0.5;
} else {
current_value += 1.0;
}
event.stop_propagation();
event.prevent_default();
} else if keycode == 37 || keycode == 40 {
if self.props.allow_half {
current_value -= 0.5;
} else {
current_value -= 1.0;
}
event.stop_propagation();
event.prevent_default();
}
current_value = if current_value < 0.0 {
0.0
} else if current_value > self.props.max as f64{
self.props.max as f64
} else {
current_value
};
if current_value != self.current_value {
self.props.value = current_value;
self.current_value = self.props.value;
self.props.on_change.emit(self.props.value);
true
} else {
false
}
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let show_text = ctx.props().show_text.clone();
let max = ctx.props().max.clone();
let show_score = ctx.props().show_score.clone();
let text_color = ctx.props().text_color.clone();
let span_style = if self.is_rate_disabled() {
"cursor: auto"
} else {
"cursor: pointer"
};
let mut span_vec = vec![];
for i in 0..max {
span_vec.push(html!{
<span class={"el-rate__item"} style={span_style}>
<i class={self.get_classes(i)} style={self.get_icon_style(i)} onmousemove={ctx.link().callback(move |e: MouseEvent| {
Msg::OnMouseMove((i, e))
})} onmouseleave={ctx.link().callback(move |e: MouseEvent| {
Msg::OnMouseLeave
})} onclick={ctx.link().callback(move |_e: MouseEvent| {
Msg::OnSelectValue(i)
})}>
if self.is_show_decimal_icon(i+1) {
<i class={self.get_decimal_icon_class()} style={self.get_decimal_style()}/>
}
</i>
</span>
});
}
html! {
<div onkeydown={ctx.link().callback(move|e:KeyboardEvent|{
Msg::OnKeydown(e)
})} class="el-rate" role="slider" tabindex="0" aria-valuemin="0" aria-valuemax={max.to_string()} aria-valuenow={self.current_value.to_string()} >
{span_vec}
if show_score || show_text {
<span class="el-rate__text" style={format!("color: {}", text_color)}>{self.get_text()}</span>
}
</div>
}
}
}
impl YewRate {
pub fn get_active_color(&self)->String {
let s = self.get_value_from_map(js_sys::Math::ceil(self.current_value) as i32, self.get_color_map());
s
}
fn get_color_map(&self)->HashMap<i32, ClassMapVaule> {
let mut map = HashMap::new();
map.insert(self.props.low_threshold, ClassMapVaule{
value: self.props.colors[0].clone(),
excluded: false
});
map.insert(self.props.high_threshold, ClassMapVaule{
value: self.props.colors[1].clone(),
excluded: true
});
map.insert(self.props.max, ClassMapVaule{
value: self.props.colors[2].clone(),
excluded: false
});
map
}
fn get_class_map(&self)->HashMap<i32, ClassMapVaule> {
let mut map = HashMap::new();
map.insert(self.props.low_threshold, ClassMapVaule{
value: self.props.icon_classes[0].clone(),
excluded: false
});
map.insert(self.props.high_threshold, ClassMapVaule{
value: self.props.icon_classes[1].clone(),
excluded: true
});
map.insert(self.props.max, ClassMapVaule{
value: self.props.icon_classes[2].clone(),
excluded: false
});
map
}
fn get_value_from_map(&self, value: i32, map:HashMap<i32, ClassMapVaule>) -> String{
for (key, item) in &map {
if item.excluded {
if value>*key {
return item.value.clone();
}
} else {
if value<=*key {
return item.value.clone();
}
}
}
"".to_string()
}
pub fn get_icon_style(&self, item:i32) -> String {
let void_color = if self.is_rate_disabled() {
self.props.disabled_void_color.clone()
} else {
self.props.void_color.clone()
};
if item<self.current_value as i32 {
format!("color: {}; hover: {}", self.get_active_color(), self.hover_index==item)
} else {
format!("color: {}; hover: {}", void_color, self.hover_index==item)
}
}
pub fn get_active_class(&self) -> String {
let s = self.get_value_from_map(js_sys::Math::ceil(self.current_value) as i32, self.get_class_map());
s
}
pub fn get_void_class(&self) -> String {
if self.is_rate_disabled() {
self.props.disabled_void_icon_class.clone()
} else {
self.props.void_icon_class.clone()
}
}
pub fn get_classes(&self, item:i32)->Vec<String> {
let mut result = vec![];
let mut threshold = js_sys::Math::ceil(self.current_value) as i32;
if self.props.allow_half && self.current_value !=js_sys::Math::floor(self.current_value) {
threshold -=1;
}
for _ in (0..threshold) {
let get_active_class = self.get_active_class();
result.push(get_active_class);
}
for _ in (threshold..self.props.max) {
result.push(self.get_void_class());
}
vec!["el-rate__icon".to_string(), result[item as usize].clone()]
}
pub fn get_text(&self) ->String {
if self.props.show_score {
if self.is_rate_disabled() {
return format!("{}", self.props.value);
}
return format!("{}", self.current_value);
} else if self.props.show_text && self.current_value as i32 >0 {
let s = self.props.texts[(js_sys::Math::ceil(self.current_value) as usize)-1].clone();
return s;
}
"".to_string()
}
pub fn get_decimal_style(&self) -> String {
let width;
if self.is_rate_disabled() {
width = format!("{}%", self.get_value_decimal());
} else if self.props.allow_half {
width = "50%".to_string();
} else {
width = "".to_string();
}
format!("color: {}; width: {}", self.get_active_color(), width)
}
pub fn get_value_decimal(&self) -> f64 {
return self.props.value * 100.0 - js_sys::Math::floor(self.props.value) * 100.0;
}
pub fn is_show_decimal_icon(&self, item:i32) -> bool {
let show_when_disabled = self.is_rate_disabled() && self.get_value_decimal()>0.0 && (item as f64)-1.0<self.props.value && (item as f64) >self.props.value;
let show_when_allow_half = self.props.allow_half && self.pointer_at_left_half && (item as f64)-0.5<=self.current_value && (item as f64)>self.current_value;
return show_when_disabled || show_when_allow_half;
}
pub fn get_decimal_icon_class(&self) ->String {
let s = self.get_value_from_map(self.props.value as i32, self.get_class_map());
format!("el-rate__decimal {}", s)
}
pub fn is_rate_disabled(&self) -> bool {
// TODO 原始实现会有elForm的判断,目前没有实现
// return this.disabled || (this.elForm || {}).disabled;
return self.props.disabled;
}
}
使用方式代码,components_test.rs
。
use super::yew_button::YewButton;
use super::yew_rate::YewRate;
use gloo_console::log;
use yew::prelude::*;
pub enum Msg {
BtnClick,
OnRateValueChanged(f64)
}
pub struct ComponentsTest {}
impl Component for ComponentsTest {
type Message = Msg;
type Properties = ();
fn create(_ctx: &Context<Self>) -> Self {
Self {}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::BtnClick => {
log!("按钮点击");
false
},
Msg::OnRateValueChanged(v) =>{
log!("v: ", v);
false
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let on_clicked = ctx.link().callback(move |_e: MouseEvent| Msg::BtnClick);
html! {
<div style="margin-left: 100px;">
<h1>{ "组件测试" }</h1>
// <YewButton style="primary" title="按钮测试" on_clicked={on_clicked.clone()} />
// <br/>
<YewRate value={0.0} on_change={ctx.link().callback(|v| {
Msg::OnRateValueChanged(v)
})} show_text={true} allow_half={true}/>
</div>
}
}
}
完整工程工程见yew-lab
评论区
写评论还没有评论