feat: Textarea adds rules prop

This commit is contained in:
luoxiao 2024-08-22 15:46:44 +08:00 committed by luoxiaozero
parent db0148258d
commit f773fada0c
4 changed files with 92 additions and 15 deletions

View file

@ -38,6 +38,9 @@ view! {
<Checkbox label="Remember me" value="true"/> <Checkbox label="Remember me" value="true"/>
</CheckboxGroup> </CheckboxGroup>
</Field> </Field>
<Field label="Textarea" name="textarea">
<Textarea rules=vec![TextareaRule::required(true.into())]/>
</Field>
<Field name="radio"> <Field name="radio">
<RadioGroup rules=vec![RadioGroupRule::required(true.into())] > <RadioGroup rules=vec![RadioGroupRule::required(true.into())] >
<Radio label="0" value="false"/> <Radio label="0" value="false"/>

View file

@ -106,6 +106,9 @@ view! {
| Name | Type | Default | Description | | Name | Type | Default | Description |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| class | `MaybeProp<String>` | `Default::default()` | | | class | `MaybeProp<String>` | `Default::default()` | |
| id | `MaybeProp<String>` | `Default::default()` | |
| name | `MaybeProp<String>` | `Default::default()` | A string specifying a name for the input control. This name is submitted along with the control's value when the form data is submitted. |
| rules | `Vec<InputRule>` | `vec![]` | The rules to validate Field. |
| value | `Model<String>` | `Default::default()` | Set the input value. | | value | `Model<String>` | `Default::default()` | Set the input value. |
| allow_value | `Option<ArcOneCallback<String, bool>>` | `None` | Check the incoming value, if it returns false, input will not be accepted. | | allow_value | `Option<ArcOneCallback<String, bool>>` | `None` | Check the incoming value, if it returns false, input will not be accepted. |
| input_type | `MaybeSignal<InputType>` | `InputType::Text` | An input can have different text-based types based on the type of value the user will enter. | | input_type | `MaybeSignal<InputType>` | `InputType::Text` | An input can have different text-based types based on the type of value the user will enter. |

View file

@ -36,6 +36,9 @@ view! {
| Name | Type | Default | Description | | Name | Type | Default | Description |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| class | `MaybeProp<String>` | `Default::default()` | | | class | `MaybeProp<String>` | `Default::default()` | |
| id | `MaybeProp<String>` | `Default::default()` | |
| name | `MaybeProp<String>` | `Default::default()` | A string specifying a name for the input control. This name is submitted along with the control's value when the form data is submitted. |
| rules | `Vec<TextareaRule>` | `vec![]` | The rules to validate Field. |
| value | `Model<String>` | `Default::default()` | The value of the Textarea. | | value | `Model<String>` | `Default::default()` | The value of the Textarea. |
| allow_value | `Option<BoxOneCallback<String, bool>>` | `None` | Check the incoming value, if it returns false, input will not be accepted. | | allow_value | `Option<BoxOneCallback<String, bool>>` | `None` | Check the incoming value, if it returns false, input will not be accepted. |
| placeholder | `MaybeProp<String>` | `Default::default()` | Placeholder text for the input. | | placeholder | `MaybeProp<String>` | `Default::default()` | Placeholder text for the input. |

View file

@ -1,9 +1,19 @@
use crate::{FieldInjection, FieldValidationState, Rule};
use leptos::{ev, html, prelude::*}; use leptos::{ev, html, prelude::*};
use std::ops::Deref;
use thaw_utils::{class_list, mount_style, BoxOneCallback, ComponentRef, Model}; use thaw_utils::{class_list, mount_style, BoxOneCallback, ComponentRef, Model};
#[component] #[component]
pub fn Textarea( pub fn Textarea(
#[prop(optional, into)] class: MaybeProp<String>, #[prop(optional, into)] class: MaybeProp<String>,
#[prop(optional, into)] id: MaybeProp<String>,
/// A string specifying a name for the input control.
/// This name is submitted along with the control's value when the form data is submitted.
#[prop(optional, into)]
name: MaybeProp<String>,
/// The rules to validate Field.
#[prop(optional, into)]
rules: Vec<TextareaRule>,
/// The value of the Textarea. /// The value of the Textarea.
#[prop(optional, into)] #[prop(optional, into)]
value: Model<String>, value: Model<String>,
@ -29,7 +39,8 @@ pub fn Textarea(
// #[prop(attrs)] attrs: Vec<(&'static str, Attribute)>, // #[prop(attrs)] attrs: Vec<(&'static str, Attribute)>,
) -> impl IntoView { ) -> impl IntoView {
mount_style("textarea", include_str!("./textarea.css")); mount_style("textarea", include_str!("./textarea.css"));
let (id, name) = FieldInjection::use_id_and_name(id, name);
let validate = Rule::validate(rules, value, name);
let value_trigger = ArcTrigger::new(); let value_trigger = ArcTrigger::new();
let on_input = { let on_input = {
let value_trigger = value_trigger.clone(); let value_trigger = value_trigger.clone();
@ -42,35 +53,28 @@ pub fn Textarea(
} }
} }
value.set(input_value); value.set(input_value);
validate.run(Some(TextareaRuleTrigger::Input));
} }
}; };
let on_change = move |_| {
validate.run(Some(TextareaRuleTrigger::Change));
};
let on_internal_focus = move |ev| { let on_internal_focus = move |ev| {
if let Some(on_focus) = on_focus.as_ref() { if let Some(on_focus) = on_focus.as_ref() {
on_focus(ev); on_focus(ev);
} }
validate.run(Some(TextareaRuleTrigger::Focus));
}; };
let on_internal_blur = move |ev| { let on_internal_blur = move |ev| {
if let Some(on_blur) = on_blur.as_ref() { if let Some(on_blur) = on_blur.as_ref() {
on_blur(ev); on_blur(ev);
} }
validate.run(Some(TextareaRuleTrigger::Blur));
}; };
let textarea_ref = NodeRef::<html::Textarea>::new(); let textarea_ref = NodeRef::<html::Textarea>::new();
comp_ref.load(TextareaRef { textarea_ref }); comp_ref.load(TextareaRef { textarea_ref });
// #[cfg(debug_assertions)]
// {
// const INNER_ATTRS: [&str; 3] = ["class", "disabled", "placeholder"];
// attrs.iter().for_each(|attr| {
// if INNER_ATTRS.contains(&attr.0) {
// logging::warn!(
// "Thaw: The '{}' attribute already exists on elements inside the TextArea component, which may cause conflicts.",
// attr.0
// );
// }
// });
// }
view! { view! {
<span class=class_list![ <span class=class_list![
"thaw-textarea", "thaw-textarea",
@ -85,9 +89,12 @@ pub fn Textarea(
} }
on:input=on_input on:input=on_input
on:change=on_change
on:focus=on_internal_focus on:focus=on_internal_focus
on:blur=on_internal_blur on:blur=on_internal_blur
class="thaw-textarea__textarea" class="thaw-textarea__textarea"
id=id
name=name
disabled=move || disabled.get() disabled=move || disabled.get()
placeholder=move || placeholder.get() placeholder=move || placeholder.get()
node_ref=textarea_ref node_ref=textarea_ref
@ -136,3 +143,64 @@ impl TextareaResize {
} }
} }
} }
#[derive(Debug, Default, PartialEq, Clone, Copy)]
pub enum TextareaRuleTrigger {
#[default]
Blur,
Focus,
Input,
Change,
}
pub struct TextareaRule(Rule<String, TextareaRuleTrigger>);
impl TextareaRule {
pub fn required(required: MaybeSignal<bool>) -> Self {
Self::validator(move |value, name| {
if required.get_untracked() && value.is_empty() {
let message = name.get_untracked().map_or_else(
|| String::from("Please input!"),
|name| format!("Please input {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_empty() {
Err(FieldValidationState::Error(message.get_untracked()))
} else {
Ok(())
}
})
}
pub fn validator(
f: impl Fn(&String, Signal<Option<String>>) -> Result<(), FieldValidationState>
+ Send
+ Sync
+ 'static,
) -> Self {
Self(Rule::validator(f))
}
pub fn with_trigger(self, trigger: TextareaRuleTrigger) -> Self {
Self(Rule::with_trigger(self.0, trigger))
}
}
impl Deref for TextareaRule {
type Target = Rule<String, TextareaRuleTrigger>;
fn deref(&self) -> &Self::Target {
&self.0
}
}