mirror of
https://github.com/adoyle0/thaw.git
synced 2025-01-23 06:19:22 -05:00
feat: RadioGroup adds rules prop
This commit is contained in:
parent
3ac0a6cd85
commit
3bba024540
7 changed files with 127 additions and 10 deletions
|
@ -21,7 +21,7 @@ view! {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### validate
|
### Validation rules
|
||||||
|
|
||||||
```rust demo
|
```rust demo
|
||||||
view! {
|
view! {
|
||||||
|
@ -38,6 +38,12 @@ view! {
|
||||||
<Checkbox label="Remember me" value="true"/>
|
<Checkbox label="Remember me" value="true"/>
|
||||||
</CheckboxGroup>
|
</CheckboxGroup>
|
||||||
</Field>
|
</Field>
|
||||||
|
<Field name="radio">
|
||||||
|
<RadioGroup rules=vec![RadioGroupRule::required(true.into())] >
|
||||||
|
<Radio label="0" value="false"/>
|
||||||
|
<Radio label="1" value="true"/>
|
||||||
|
</RadioGroup>
|
||||||
|
</Field>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
on:click={
|
on:click={
|
||||||
|
|
|
@ -2,6 +2,7 @@ use super::{FieldContextInjection, FieldInjection, FieldValidationState};
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use send_wrapper::SendWrapper;
|
use send_wrapper::SendWrapper;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
use thaw_utils::{Model, OptionModel};
|
||||||
|
|
||||||
type RuleValidator<T> = Box<dyn Fn(&T, Signal<Option<String>>) -> Result<(), FieldValidationState>>;
|
type RuleValidator<T> = Box<dyn Fn(&T, Signal<Option<String>>) -> Result<(), FieldValidationState>>;
|
||||||
|
|
||||||
|
@ -38,7 +39,8 @@ impl<T, Trigger> Rule<T, Trigger> {
|
||||||
name: Signal<Option<String>>,
|
name: Signal<Option<String>>,
|
||||||
) -> Callback<Option<Trigger>, bool>
|
) -> Callback<Option<Trigger>, bool>
|
||||||
where
|
where
|
||||||
V: WithUntracked<Value = T>,
|
V: RuleValueWithUntracked<T>,
|
||||||
|
// V: WithUntracked<Value = T>,
|
||||||
V: Send + Sync + Copy + 'static,
|
V: Send + Sync + Copy + 'static,
|
||||||
R: Deref<Target = Rule<T, Trigger>> + 'static,
|
R: Deref<Target = Rule<T, Trigger>> + 'static,
|
||||||
Trigger: PartialEq + 'static,
|
Trigger: PartialEq + 'static,
|
||||||
|
@ -69,7 +71,7 @@ impl<T, Trigger> Rule<T, Trigger> {
|
||||||
}
|
}
|
||||||
call_count += 1;
|
call_count += 1;
|
||||||
|
|
||||||
let state = value.with_untracked(|value| (rule.validator)(value, name));
|
let state = value.value_with_untracked(|value| (rule.validator)(value, name));
|
||||||
if state.is_err() {
|
if state.is_err() {
|
||||||
return Some(state);
|
return Some(state);
|
||||||
}
|
}
|
||||||
|
@ -93,3 +95,31 @@ impl<T, Trigger> Rule<T, Trigger> {
|
||||||
validate
|
validate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait RuleValueWithUntracked<T> {
|
||||||
|
fn value_with_untracked(
|
||||||
|
&self,
|
||||||
|
f: impl FnOnce(&T) -> Result<(), FieldValidationState>,
|
||||||
|
) -> Result<(), FieldValidationState>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Send + Sync> RuleValueWithUntracked<T> for Model<T> {
|
||||||
|
fn value_with_untracked(
|
||||||
|
&self,
|
||||||
|
f: impl FnOnce(&T) -> Result<(), FieldValidationState>,
|
||||||
|
) -> Result<(), FieldValidationState> {
|
||||||
|
self.with_untracked(move |v| f(v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Send + Sync + Clone> RuleValueWithUntracked<Option<T>> for OptionModel<T> {
|
||||||
|
fn value_with_untracked(
|
||||||
|
&self,
|
||||||
|
f: impl FnOnce(&Option<T>) -> Result<(), FieldValidationState>,
|
||||||
|
) -> Result<(), FieldValidationState> {
|
||||||
|
self.with_untracked(move |v| {
|
||||||
|
let v = v.map(|v| v.clone());
|
||||||
|
f(&v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -38,5 +38,5 @@ view! {
|
||||||
| -------- | --------------------- | -------------------- | -------------------------------------- |
|
| -------- | --------------------- | -------------------- | -------------------------------------- |
|
||||||
| class | `MaybeProp<String>` | `Default::default()` | |
|
| class | `MaybeProp<String>` | `Default::default()` | |
|
||||||
| value | `OptionModel<String>` | `Default::default()` | The selected Radio item in this group. |
|
| value | `OptionModel<String>` | `Default::default()` | The selected Radio item in this group. |
|
||||||
| name | `Option<String>` | `None` | The name of this radio group. |
|
| name | `MaybeProp<String>` | `None` | The name of this radio group. |
|
||||||
| children | `Children` | | |
|
| children | `Children` | | |
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
mod radio_group;
|
mod radio_group;
|
||||||
|
|
||||||
pub use radio_group::RadioGroup;
|
pub use radio_group::{RadioGroup, RadioGroupRule, RadioGroupRuleTrigger};
|
||||||
|
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use radio_group::RadioGroupInjection;
|
use radio_group::RadioGroupInjection;
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
.thaw-radio-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
.thaw-radio {
|
.thaw-radio {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
@ -1,25 +1,42 @@
|
||||||
|
use crate::{FieldInjection, FieldValidationState, Rule};
|
||||||
use leptos::{context::Provider, prelude::*};
|
use leptos::{context::Provider, prelude::*};
|
||||||
|
use std::ops::Deref;
|
||||||
use thaw_utils::{class_list, OptionModel};
|
use thaw_utils::{class_list, OptionModel};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn RadioGroup(
|
pub fn RadioGroup(
|
||||||
#[prop(optional, into)] class: MaybeProp<String>,
|
#[prop(optional, into)] class: MaybeProp<String>,
|
||||||
|
#[prop(optional, into)] id: MaybeProp<String>,
|
||||||
|
#[prop(optional, into)] rules: Vec<RadioGroupRule>,
|
||||||
/// The selected Radio item in this group.
|
/// The selected Radio item in this group.
|
||||||
#[prop(optional, into)]
|
#[prop(optional, into)]
|
||||||
value: OptionModel<String>,
|
value: OptionModel<String>,
|
||||||
/// The name of this radio group.
|
/// The name of this radio group.
|
||||||
#[prop(optional, into)]
|
#[prop(optional, into)]
|
||||||
name: Option<String>,
|
name: MaybeProp<String>,
|
||||||
children: Children,
|
children: Children,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
let name = name.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
|
let (id, name) = FieldInjection::use_id_and_name(id, name);
|
||||||
|
let validate = Rule::validate(rules, value, name);
|
||||||
|
|
||||||
|
Effect::new(move |prev: Option<()>| {
|
||||||
|
value.with(|_| {});
|
||||||
|
if prev.is_some() {
|
||||||
|
validate.run(Some(RadioGroupRuleTrigger::Change));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let name = Signal::derive(move || {
|
||||||
|
name.get()
|
||||||
|
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string())
|
||||||
|
});
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<Provider value=RadioGroupInjection { value, name }>
|
<Provider value=RadioGroupInjection { value, name }>
|
||||||
<div
|
<div
|
||||||
class=class_list!["thaw-radio-group", class]
|
class=class_list!["thaw-radio-group", class]
|
||||||
|
id=id
|
||||||
role="radiogroup"
|
role="radiogroup"
|
||||||
style="display: flex;align-items: flex-start"
|
|
||||||
>
|
>
|
||||||
{children()}
|
{children()}
|
||||||
</div>
|
</div>
|
||||||
|
@ -30,7 +47,7 @@ pub fn RadioGroup(
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) struct RadioGroupInjection {
|
pub(crate) struct RadioGroupInjection {
|
||||||
pub value: OptionModel<String>,
|
pub value: OptionModel<String>,
|
||||||
pub name: String,
|
pub name: Signal<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RadioGroupInjection {
|
impl RadioGroupInjection {
|
||||||
|
@ -38,3 +55,61 @@ impl RadioGroupInjection {
|
||||||
expect_context()
|
expect_context()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, PartialEq, Clone, Copy)]
|
||||||
|
pub enum RadioGroupRuleTrigger {
|
||||||
|
#[default]
|
||||||
|
Change,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RadioGroupRule(Rule<Option<String>, RadioGroupRuleTrigger>);
|
||||||
|
|
||||||
|
impl RadioGroupRule {
|
||||||
|
pub fn required(required: MaybeSignal<bool>) -> Self {
|
||||||
|
Self::validator(move |value, name| {
|
||||||
|
if required.get_untracked() && value.is_none() {
|
||||||
|
let message = name.get_untracked().map_or_else(
|
||||||
|
|| String::from("Please select!"),
|
||||||
|
|name| format!("Please select {name}!"),
|
||||||
|
);
|
||||||
|
Err(FieldValidationState::Error(message))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn required_with_message(
|
||||||
|
required: MaybeSignal<bool>,
|
||||||
|
message: MaybeSignal<String>,
|
||||||
|
) -> Self {
|
||||||
|
Self::validator(move |value, _| {
|
||||||
|
if required.get_untracked() && value.is_none() {
|
||||||
|
Err(FieldValidationState::Error(message.get_untracked()))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validator(
|
||||||
|
f: impl Fn(&Option<String>, Signal<Option<String>>) -> Result<(), FieldValidationState>
|
||||||
|
+ Send
|
||||||
|
+ Sync
|
||||||
|
+ 'static,
|
||||||
|
) -> Self {
|
||||||
|
Self(Rule::validator(f))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_trigger(self, trigger: RadioGroupRuleTrigger) -> Self {
|
||||||
|
Self(Rule::with_trigger(self.0, trigger))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for RadioGroupRule {
|
||||||
|
type Target = Rule<Option<String>, RadioGroupRuleTrigger>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ use leptos::reactive_graph::{
|
||||||
wrappers::read::Signal,
|
wrappers::read::Signal,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum OptionModel<T, S = SyncStorage>
|
pub enum OptionModel<T, S = SyncStorage>
|
||||||
where
|
where
|
||||||
T: 'static,
|
T: 'static,
|
||||||
|
@ -21,7 +22,7 @@ where
|
||||||
|
|
||||||
impl<T: Default + Send + Sync> Default for OptionModel<T> {
|
impl<T: Default + Send + Sync> Default for OptionModel<T> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new(Default::default())
|
Self::new_option(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue