Skip to content

Commit

Permalink
Feat/rating (#369)
Browse files Browse the repository at this point in the history
* feat: adds Rating component

* feat: Improve Rating

* feat: adds RatingDisplay component

* feat: Rating adds rules prop

* fix: build nightly
  • Loading branch information
luoxiaozero authored Jan 25, 2025
1 parent af87d61 commit 6ffc1d2
Show file tree
Hide file tree
Showing 14 changed files with 702 additions and 0 deletions.
2 changes: 2 additions & 0 deletions demo/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ fn TheRouter() -> impl IntoView {
<Route path=path!("/popover") view=PopoverMdPage />
<Route path=path!("/progress-bar") view=ProgressBarMdPage />
<Route path=path!("/radio") view=RadioMdPage />
<Route path=path!("/rating") view=RatingMdPage />
<Route path=path!("/rating-display") view=RatingDisplayMdPage />
<Route path=path!("/scrollbar") view=ScrollbarMdPage />
<Route path=path!("/select") view=SelectMdPage />
<Route path=path!("/skeleton") view=SkeletonMdPage />
Expand Down
10 changes: 10 additions & 0 deletions demo/src/pages/components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,16 @@ pub(crate) fn gen_nav_data() -> Vec<NavGroupOption> {
value: "/components/radio",
label: "Radio",
},
NavItemOption {
group: Some("Rating"),
value: "/components/rating",
label: "Rating",
},
NavItemOption {
group: Some("Rating"),
value: "/components/rating-display",
label: "RatingDisplay",
},
NavItemOption {
group: None,
value: "/components/scrollbar",
Expand Down
2 changes: 2 additions & 0 deletions demo_markdown/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ pub fn include_md(_token_stream: proc_macro::TokenStream) -> proc_macro::TokenSt
"PopoverMdPage" => "../../thaw/src/popover/docs/mod.md",
"ProgressBarMdPage" => "../../thaw/src/progress_bar/docs/mod.md",
"RadioMdPage" => "../../thaw/src/radio/docs/mod.md",
"RatingMdPage" => "../../thaw/src/rating/docs/mod.md",
"RatingDisplayMdPage" => "../../thaw/src/rating/docs/rating_display.md",
"ScrollbarMdPage" => "../../thaw/src/scrollbar/docs/mod.md",
"SelectMdPage" => "../../thaw/src/select/docs/mod.md",
"SkeletonMdPage" => "../../thaw/src/skeleton/docs/mod.md",
Expand Down
3 changes: 3 additions & 0 deletions thaw/src/field/docs/mod.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ view! {
<Radio label="1" value="true"/>
</RadioGroup>
</Field>
<Field label="Rating" name="rating">
<Rating rules=vec![RatingRule::required(true.into())] />
</Field>
<Field label="Combobox" name="combobox">
<Combobox rules=vec![ComboboxRule::required(true.into())] placeholder="Select an animal" clearable=true>
<ComboboxOption value="cat" text="Cat"/>
Expand Down
12 changes: 12 additions & 0 deletions thaw/src/field/rule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,18 @@ impl<T: Send + Sync> RuleValueWithUntracked<T> for Model<T> {
}
}

impl RuleValueWithUntracked<Option<f32>> for OptionModel<f32> {
fn value_with_untracked(
&self,
f: impl FnOnce(&Option<f32>) -> Result<(), FieldValidationState>,
) -> Result<(), FieldValidationState> {
self.with_untracked(move |v| match v {
OptionModelWithValue::T(v) => f(&Some(v.clone())),
OptionModelWithValue::Option(v) => f(v),
})
}
}

impl RuleValueWithUntracked<Option<String>> for OptionModel<String> {
fn value_with_untracked(
&self,
Expand Down
2 changes: 2 additions & 0 deletions thaw/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ mod pagination;
mod popover;
mod progress_bar;
mod radio;
mod rating;
mod scrollbar;
mod select;
mod skeleton;
Expand Down Expand Up @@ -94,6 +95,7 @@ pub use pagination::*;
pub use popover::*;
pub use progress_bar::*;
pub use radio::*;
pub use rating::*;
pub use scrollbar::*;
pub use select::*;
pub use skeleton::*;
Expand Down
69 changes: 69 additions & 0 deletions thaw/src/rating/docs/mod.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Rating

```rust demo
let value = RwSignal::new(0.0);

view! {
{move || value.get()}
<Rating value />
}
```

### Step

```rust demo
let value = RwSignal::new(3.5);

view! {
{move || value.get()}
<Rating value step=0.5 />
}
```

### Max

```rust demo
let value = RwSignal::new(5.0);

view! {
<Rating value max=10 />
}
```

### Size

```rust demo
view! {
<Flex vertical=true inline=true>
<Rating value=3.0 size=RatingSize::Small/>
<Rating value=3.0 size=RatingSize::Medium/>
<Rating value=3.0 size=RatingSize::Large/>
<Rating value=3.0 size=RatingSize::ExtraLarge/>
</Flex>
}
```

### Color

```rust demo
view! {
<Flex vertical=true inline=true>
<Rating />
<Rating color=RatingColor::Brand/>
</Flex>
}
```

### Rating Props

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| class | `MaybeProp<String>` | `Default::default()` | |
| id | `MaybeProp<String>` | `Default::default()` | |
| name | `MaybeProp<String>` | `Default::default()` | Name for the Radio inputs. If not provided, one will be automatically generated. |
| rules | `Vec<RatingRule>` | `vec![]` | The rules to validate Field. |
| value | `OptionModel<f32>` | `None` | The value of the rating. |
| max | `Signal<u8>` | `5` | The max value of the rating. This controls the number of rating items displayed. Must be a whole number greater than 1. |
| step | `Signal<f32>` | `1.0` | Sets the precision to allow half-filled shapes in Rating. |
| size | `Signal<RatingSize>` | `RatingSize::ExtraLarge` | Sets the size of the Rating items. |
| color | `Signal<RatingColor>` | `RatingColor::Neutral` | Rating color. |
59 changes: 59 additions & 0 deletions thaw/src/rating/docs/rating_display.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# RatingDisplay

The value controls the number of filled stars, and is written out next to the RatingDisplay. The number of filled stars is rounded to the nearest half-star.

```rust demo
view! {
<Flex vertical=true inline=true>
<RatingDisplay value=1.0 />
<RatingDisplay value=3.7 />
<RatingDisplay value=3.9 />
<RatingDisplay value=4.0 />
<RatingDisplay value=5.0 />
</Flex>
}
```

### Max

```rust demo
let value = RwSignal::new(5.0);

view! {
<RatingDisplay value max=10 />
}
```

### Size

```rust demo
view! {
<Flex vertical=true inline=true>
<RatingDisplay value=3.0 size=RatingSize::Small/>
<RatingDisplay value=3.0 size=RatingSize::Medium/>
<RatingDisplay value=3.0 size=RatingSize::Large/>
<RatingDisplay value=3.0 size=RatingSize::ExtraLarge/>
</Flex>
}
```

### Color

```rust demo
view! {
<Flex vertical=true inline=true>
<RatingDisplay value=3.0/>
<RatingDisplay color=RatingColor::Brand value=3.0/>
</Flex>
}
```

### Rating Props

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| class | `MaybeProp<String>` | `Default::default()` | |
| value | `Signal<f32>` | `0.0` | The value of the rating. |
| max | `Signal<u8>` | `5` | The max value of the rating. This controls the number of rating items displayed. Must be a whole number greater than 1. |
| size | `Signal<RatingSize>` | `RatingSize::Medium` | Sets the size of the Rating items. |
| color | `Signal<RatingColor>` | `RatingColor::Neutral` | Rating color. |
6 changes: 6 additions & 0 deletions thaw/src/rating/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
mod rating;
mod rating_display;
mod rating_item;

pub use rating::*;
pub use rating_display::*;
108 changes: 108 additions & 0 deletions thaw/src/rating/rating/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
mod types;

pub use types::*;

use super::rating_item::RatingItem;
use crate::{FieldInjection, Rule};
use leptos::{context::Provider, prelude::*};
use thaw_utils::{class_list, mount_style, OptionModel};
use wasm_bindgen::JsCast;
use web_sys::{Event, EventTarget, HtmlInputElement, MouseEvent};

#[component]
pub fn Rating(
#[prop(optional, into)] class: MaybeProp<String>,
#[prop(optional, into)] id: MaybeProp<String>,
#[prop(optional, into)] rules: Vec<RatingRule>,
/// Name for the Radio inputs. If not provided, one will be automatically generated.
#[prop(optional, into)]
name: MaybeProp<String>,
/// The value of the rating.
#[prop(optional, into)]
value: OptionModel<f32>,
/// The max value of the rating. This controls the number of rating items displayed.
/// Must be a whole number greater than 1.
#[prop(default = 5.into(), into)]
max: Signal<u8>,
/// Sets the precision to allow half-filled shapes in Rating.
#[prop(default = 1.0.into(), into)]
step: Signal<f32>,
/// Sets the size of the Rating items.
#[prop(default = RatingSize::ExtraLarge.into(), into)]
size: Signal<RatingSize>,
/// Rating color.
#[prop(optional, into)]
color: Signal<RatingColor>,
) -> impl IntoView {
mount_style("rating", include_str!("./rating.css"));
let (id, name) = FieldInjection::use_id_and_name(id, name);
let validate = Rule::validate(rules, value, name);

let name = Memo::new(move |_| {
name.get()
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string())
});
let hovered_value = RwSignal::new(None::<f32>);

let on_change = move |e: Event| {
if let Some(el) = is_rating_radio_item(e.target().unwrap(), &name.read()) {
if let Ok(new_value) = el.value().parse::<f32>() {
value.set(Some(new_value));
validate.run(Some(RatingRuleTrigger::Change));
}
}
};

let on_mouseover = move |e: MouseEvent| {
if let Some(el) = is_rating_radio_item(e.target().unwrap(), &name.read()) {
if let Ok(new_value) = el.value().parse::<f32>() {
hovered_value.set(Some(new_value));
}
}
};

let on_mouseleave = move |_| {
hovered_value.set(None);
};

view! {
<div
role="radiogroup"
class=class_list!["thaw-rating", class]
id=id
on:change=on_change
on:mouseover=on_mouseover
on:mouseleave=on_mouseleave
>
<Provider value=RatingInjection {
value,
hovered_value,
name,
step,
size,
color,
interactive: true,
}>
{move || {
let mut max = max.get();
if max < 2 {
max = 2;
}
(0..max)
.into_iter()
.map(|i| {
view! { <RatingItem value=i + 1 /> }
})
.collect_view()
}}
</Provider>
</div>
}
}

fn is_rating_radio_item(target: EventTarget, name: &String) -> Option<HtmlInputElement> {
target
.dyn_into::<HtmlInputElement>()
.ok()
.filter(|el| el.type_() == "radio" && &el.name() == name)
}
Loading

0 comments on commit 6ffc1d2

Please sign in to comment.