mirror of
https://github.com/adoyle0/thaw.git
synced 2025-01-23 06:19:22 -05:00
feat: Input adds rules prop
This commit is contained in:
parent
a45354c852
commit
615c33a5c8
7 changed files with 402 additions and 23 deletions
|
@ -37,6 +37,7 @@ chrono = "0.4.38"
|
||||||
palette = "0.7.6"
|
palette = "0.7.6"
|
||||||
num-traits = "0.2.19"
|
num-traits = "0.2.19"
|
||||||
send_wrapper = "0.6"
|
send_wrapper = "0.6"
|
||||||
|
slotmap = "1.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
csr = ["leptos/csr", "thaw_components/csr", "thaw_utils/csr"]
|
csr = ["leptos/csr", "thaw_components/csr", "thaw_utils/csr"]
|
||||||
|
|
|
@ -20,3 +20,30 @@ view! {
|
||||||
</Field>
|
</Field>
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### validate
|
||||||
|
|
||||||
|
```rust demo
|
||||||
|
view! {
|
||||||
|
<form>
|
||||||
|
<FieldContextProvider>
|
||||||
|
<Field label="Example field">
|
||||||
|
<Input />
|
||||||
|
</Field>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
on:click={
|
||||||
|
let field_context = FieldContextInjection::expect_context();
|
||||||
|
move |e| {
|
||||||
|
if !field_context.validate() {
|
||||||
|
e.prevent_default();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
"Submit"
|
||||||
|
</button>
|
||||||
|
</FieldContextProvider>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
|
@ -26,3 +26,39 @@
|
||||||
padding-bottom: var(--spacingVerticalSNudge);
|
padding-bottom: var(--spacingVerticalSNudge);
|
||||||
padding-top: var(--spacingVerticalSNudge);
|
padding-top: var(--spacingVerticalSNudge);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.thaw-field__validation-message {
|
||||||
|
padding-left: calc(12px + var(--spacingHorizontalXS));
|
||||||
|
margin-top: var(--spacingVerticalXXS);
|
||||||
|
color: var(--colorNeutralForeground3);
|
||||||
|
font-family: var(--fontFamilyBase);
|
||||||
|
font-size: var(--fontSizeBase200);
|
||||||
|
font-weight: var(--fontWeightRegular);
|
||||||
|
line-height: var(--lineHeightBase200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-field__validation-message-icon {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: calc(-12px - var(--spacingHorizontalXS));
|
||||||
|
margin-right: var(--spacingHorizontalXS);
|
||||||
|
line-height: 0;
|
||||||
|
vertical-align: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-field__validation-message-icon > svg {
|
||||||
|
display: inline;
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-field__validation-message-icon--success {
|
||||||
|
color: var(--colorPaletteGreenForeground1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-field__validation-message-icon--error {
|
||||||
|
color: var(--colorPaletteRedForeground1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-field__validation-message-icon--warning {
|
||||||
|
color: var(--colorPaletteDarkOrangeForeground1);
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use leptos::{context::Provider, prelude::*};
|
use leptos::{context::Provider, either::EitherOf3, prelude::*};
|
||||||
use thaw_components::OptionComp;
|
use thaw_components::OptionComp;
|
||||||
use thaw_utils::{class_list, mount_style};
|
use thaw_utils::{class_list, mount_style};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
@ -6,13 +6,18 @@ use uuid::Uuid;
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Field(
|
pub fn Field(
|
||||||
#[prop(optional, into)] class: MaybeProp<String>,
|
#[prop(optional, into)] class: MaybeProp<String>,
|
||||||
#[prop(optional, into)] label: MaybeProp<String>,
|
/// The label associated with the field.
|
||||||
|
#[prop(optional, into)]
|
||||||
|
label: MaybeProp<String>,
|
||||||
#[prop(optional, into)] name: MaybeProp<String>,
|
#[prop(optional, into)] name: MaybeProp<String>,
|
||||||
#[prop(optional, into)] orientation: MaybeSignal<FieldOrientation>,
|
/// The orientation of the label relative to the field component.
|
||||||
|
#[prop(optional, into)]
|
||||||
|
orientation: MaybeSignal<FieldOrientation>,
|
||||||
children: Children,
|
children: Children,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
mount_style("field", include_str!("./field.css"));
|
mount_style("field", include_str!("./field.css"));
|
||||||
let id = StoredValue::new(Uuid::new_v4().to_string());
|
let id = StoredValue::new(Uuid::new_v4().to_string());
|
||||||
|
let validation_state = RwSignal::new(None::<FieldValidationState>);
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class=class_list![
|
<div class=class_list![
|
||||||
|
@ -22,16 +27,84 @@ pub fn Field(
|
||||||
},
|
},
|
||||||
class
|
class
|
||||||
]>
|
]>
|
||||||
|
{
|
||||||
|
let label = label.clone();
|
||||||
|
move || {
|
||||||
|
view! {
|
||||||
|
<OptionComp value=label.get() let:label>
|
||||||
|
<label class="thaw-field__label" for=id.get_value()>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
</OptionComp>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
<Provider value=FieldInjection { id, name, label, validation_state }>{children()}</Provider>
|
||||||
{move || {
|
{move || {
|
||||||
view! {
|
view! {
|
||||||
<OptionComp value=label.get() let:label>
|
<OptionComp value=validation_state.get() let:validation_state>
|
||||||
<label class="thaw-field__label" for=id.get_value()>
|
{
|
||||||
{label}
|
match validation_state {
|
||||||
</label>
|
FieldValidationState::Error(message) => EitherOf3::A(view! {
|
||||||
|
<div class="thaw-field__validation-message">
|
||||||
|
<span class="thaw-field__validation-message-icon thaw-field__validation-message-icon--error">
|
||||||
|
<svg
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
width="12"
|
||||||
|
height="12"
|
||||||
|
viewBox="0 0 12 12">
|
||||||
|
<path
|
||||||
|
d="M6 11A5 5 0 1 0 6 1a5 5 0 0 0 0 10Zm-.75-2.75a.75.75 0 1 1 1.5 0 .75.75 0 0 1-1.5 0Zm.26-4.84a.5.5 0 0 1 .98 0l.01.09v2.59a.5.5 0 0 1-1 0V3.41Z"
|
||||||
|
fill="currentColor">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
{message}
|
||||||
|
</div>
|
||||||
|
}),
|
||||||
|
FieldValidationState::Success(message) => EitherOf3::B(view! {
|
||||||
|
<div class="thaw-field__validation-message">
|
||||||
|
<span class="thaw-field__validation-message-icon thaw-field__validation-message-icon--success">
|
||||||
|
<svg
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
width="12"
|
||||||
|
height="12"
|
||||||
|
viewBox="0 0 12 12">
|
||||||
|
<path
|
||||||
|
d="M1 6a5 5 0 1 1 10 0A5 5 0 0 1 1 6Zm7.35-.9a.5.5 0 1 0-.7-.7L5.5 6.54 4.35 5.4a.5.5 0 1 0-.7.7l1.5 1.5c.2.2.5.2.7 0l2.5-2.5Z"
|
||||||
|
fill="currentColor">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
{message}
|
||||||
|
</div>
|
||||||
|
}),
|
||||||
|
FieldValidationState::Warning(message) => EitherOf3::C(view! {
|
||||||
|
<div class="thaw-field__validation-message">
|
||||||
|
<span class="thaw-field__validation-message-icon thaw-field__validation-message-icon--warning">
|
||||||
|
<svg
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
width="12"
|
||||||
|
height="12"
|
||||||
|
viewBox="0 0 12 12">
|
||||||
|
<path
|
||||||
|
d="M5.21 1.46a.9.9 0 0 1 1.58 0l4.09 7.17a.92.92 0 0 1-.79 1.37H1.91a.92.92 0 0 1-.79-1.37l4.1-7.17ZM5.5 4.5v1a.5.5 0 0 0 1 0v-1a.5.5 0 0 0-1 0ZM6 6.75a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z"
|
||||||
|
fill="currentColor">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
{message}
|
||||||
|
</div>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</OptionComp>
|
</OptionComp>
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
<Provider value=FieldInjection { id, name }>{children()}</Provider>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,11 +113,69 @@ pub fn Field(
|
||||||
pub(crate) struct FieldInjection {
|
pub(crate) struct FieldInjection {
|
||||||
id: StoredValue<String>,
|
id: StoredValue<String>,
|
||||||
name: MaybeProp<String>,
|
name: MaybeProp<String>,
|
||||||
|
label: MaybeProp<String>,
|
||||||
|
validation_state: RwSignal<Option<FieldValidationState>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FieldInjection {
|
impl FieldInjection {
|
||||||
pub fn expect_context() -> Self {
|
pub fn use_context() -> Option<Self> {
|
||||||
expect_context()
|
use_context()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn id(&self) -> Option<String> {
|
||||||
|
if self.label.with(|l| l.is_some()) {
|
||||||
|
Some(self.id.get_value())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> Option<String> {
|
||||||
|
self.name.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn use_id_and_name(
|
||||||
|
id: MaybeProp<String>,
|
||||||
|
name: MaybeProp<String>,
|
||||||
|
) -> (Signal<Option<String>>, Signal<Option<String>>) {
|
||||||
|
let field_injection = Self::use_context();
|
||||||
|
let id = Signal::derive(move || {
|
||||||
|
if let Some(id) = id.get() {
|
||||||
|
return Some(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(field_injection) = field_injection.as_ref() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
field_injection.id()
|
||||||
|
});
|
||||||
|
|
||||||
|
let field_injection = Self::use_context();
|
||||||
|
let name = Signal::derive(move || {
|
||||||
|
if let Some(name) = name.get() {
|
||||||
|
return Some(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(field_injection) = field_injection.as_ref() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
field_injection.name()
|
||||||
|
});
|
||||||
|
|
||||||
|
(id, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_validation_state(&self, state: Option<FieldValidationState>) {
|
||||||
|
self.validation_state.try_maybe_update(|validation_state| {
|
||||||
|
if validation_state == &state {
|
||||||
|
(false, ())
|
||||||
|
} else {
|
||||||
|
*validation_state = state;
|
||||||
|
(true, ())
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,3 +194,10 @@ impl FieldOrientation {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum FieldValidationState {
|
||||||
|
Error(String),
|
||||||
|
Success(String),
|
||||||
|
Warning(String),
|
||||||
|
}
|
||||||
|
|
61
thaw/src/field/field_context_provider.rs
Normal file
61
thaw/src/field/field_context_provider.rs
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
use leptos::{context::Provider, prelude::*};
|
||||||
|
use slotmap::{DefaultKey, SlotMap};
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn FieldContextProvider(children: Children) -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<Provider value=FieldContextInjection::new()>{children()}</Provider>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct FieldContextInjection(
|
||||||
|
StoredValue<SlotMap<DefaultKey, (Signal<Option<String>>, Callback<(), bool>)>>,
|
||||||
|
);
|
||||||
|
|
||||||
|
impl FieldContextInjection {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self(StoredValue::new(SlotMap::new()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expect_context() -> Self {
|
||||||
|
expect_context()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn use_context() -> Option<Self> {
|
||||||
|
use_context()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn register_field(&self, name: Signal<Option<String>>, validate: impl Fn() -> bool + Send + Sync + 'static) {
|
||||||
|
let mut key = None;
|
||||||
|
self.0
|
||||||
|
.update_value(|map| key = Some(map.insert((name, Callback::from(move |_| validate())))));
|
||||||
|
|
||||||
|
let map = self.0.clone();
|
||||||
|
Owner::on_cleanup(move || {
|
||||||
|
map.update_value(|map| map.remove(key.unwrap()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate(&self) -> bool {
|
||||||
|
self.0.with_value(|map| {
|
||||||
|
for (_, (_, validate)) in map.iter() {
|
||||||
|
if !validate.run(()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_field(&self, name: String) -> bool {
|
||||||
|
self.0.with_value(|map| {
|
||||||
|
for (_, (n, validate)) in map.iter() {
|
||||||
|
if n.get_untracked().as_ref() == Some(&name) && !validate.run(()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,5 @@
|
||||||
mod field;
|
mod field;
|
||||||
|
mod field_context_provider;
|
||||||
|
|
||||||
pub use field::*;
|
pub use field::*;
|
||||||
|
pub use field_context_provider::*;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::{FieldContextInjection, FieldInjection, FieldValidationState};
|
||||||
use leptos::{ev, html, prelude::*};
|
use leptos::{ev, html, prelude::*};
|
||||||
use thaw_utils::{
|
use thaw_utils::{
|
||||||
class_list, mount_style, ArcOneCallback, BoxOneCallback, ComponentRef, Model, OptionalProp,
|
class_list, mount_style, ArcOneCallback, BoxOneCallback, ComponentRef, Model, OptionalProp,
|
||||||
|
@ -20,6 +21,12 @@ pub struct InputSuffix {
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Input(
|
pub fn Input(
|
||||||
#[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>,
|
||||||
|
#[prop(optional, into)] rules: Vec<InputRule>,
|
||||||
/// Set the input value.
|
/// Set the input value.
|
||||||
#[prop(optional, into)]
|
#[prop(optional, into)]
|
||||||
value: Model<String>,
|
value: Model<String>,
|
||||||
|
@ -53,12 +60,43 @@ pub fn Input(
|
||||||
// #[prop(attrs)] attrs: Vec<(&'static str, Attribute)>,
|
// #[prop(attrs)] attrs: Vec<(&'static str, Attribute)>,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
mount_style("input", include_str!("./input.css"));
|
mount_style("input", include_str!("./input.css"));
|
||||||
|
let (id, name) = FieldInjection::use_id_and_name(id, name);
|
||||||
|
let field_injection = FieldInjection::use_context();
|
||||||
|
let rules = StoredValue::new(rules);
|
||||||
|
let validate = Callback::new(move |trigger| {
|
||||||
|
let state = rules.with_value(move |rules| {
|
||||||
|
if rules.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut rules_iter = rules.iter();
|
||||||
|
loop {
|
||||||
|
let Some(rule) = rules_iter.next() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(state) = rule.call_validator(trigger, value) {
|
||||||
|
return Some(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let rt = state.is_some();
|
||||||
|
if let Some(field_injection) = field_injection.as_ref() {
|
||||||
|
field_injection.update_validation_state(state);
|
||||||
|
};
|
||||||
|
rt
|
||||||
|
});
|
||||||
|
if let Some(field_context) = FieldContextInjection::use_context() {
|
||||||
|
field_context.register_field(name, move || validate.run(None));
|
||||||
|
}
|
||||||
|
|
||||||
let parser_none = parser.is_none();
|
let parser_none = parser.is_none();
|
||||||
let on_input = {
|
let on_input = {
|
||||||
let allow_value = allow_value.clone();
|
let allow_value = allow_value.clone();
|
||||||
move |e| {
|
move |e| {
|
||||||
if !parser_none {
|
if !parser_none {
|
||||||
|
validate.run(Some(InputRuleTrigger::Input));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let input_value = event_target_value(&e);
|
let input_value = event_target_value(&e);
|
||||||
|
@ -69,10 +107,12 @@ pub fn Input(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
value.set(input_value);
|
value.set(input_value);
|
||||||
|
validate.run(Some(InputRuleTrigger::Input));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let on_change = move |e| {
|
let on_change = move |e| {
|
||||||
let Some(parser) = parser.as_ref() else {
|
let Some(parser) = parser.as_ref() else {
|
||||||
|
validate.run(Some(InputRuleTrigger::Change));
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some(parsed_input_value) = parser(event_target_value(&e)) else {
|
let Some(parsed_input_value) = parser(event_target_value(&e)) else {
|
||||||
|
@ -86,6 +126,7 @@ pub fn Input(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
value.set(parsed_input_value);
|
value.set(parsed_input_value);
|
||||||
|
validate.run(Some(InputRuleTrigger::Change));
|
||||||
};
|
};
|
||||||
let is_focus = RwSignal::new(false);
|
let is_focus = RwSignal::new(false);
|
||||||
let on_internal_focus = move |ev| {
|
let on_internal_focus = move |ev| {
|
||||||
|
@ -93,12 +134,14 @@ pub fn Input(
|
||||||
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(InputRuleTrigger::Focus));
|
||||||
};
|
};
|
||||||
let on_internal_blur = move |ev| {
|
let on_internal_blur = move |ev| {
|
||||||
is_focus.set(false);
|
is_focus.set(false);
|
||||||
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(InputRuleTrigger::Blur));
|
||||||
};
|
};
|
||||||
|
|
||||||
let input_ref = NodeRef::<html::Input>::new();
|
let input_ref = NodeRef::<html::Input>::new();
|
||||||
|
@ -127,19 +170,6 @@ pub fn Input(
|
||||||
input_value = None;
|
input_value = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// #[cfg(debug_assertions)]
|
|
||||||
// {
|
|
||||||
// const INNER_ATTRS: [&str; 4] = ["type", "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 Input component, which may cause conflicts.",
|
|
||||||
// attr.0
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
let prefix_if_ = input_prefix.as_ref().map_or(false, |prefix| prefix.if_);
|
let prefix_if_ = input_prefix.as_ref().map_or(false, |prefix| prefix.if_);
|
||||||
let suffix_if_ = input_suffix.as_ref().map_or(false, |suffix| suffix.if_);
|
let suffix_if_ = input_suffix.as_ref().map_or(false, |suffix| suffix.if_);
|
||||||
|
|
||||||
|
@ -162,7 +192,9 @@ pub fn Input(
|
||||||
}}
|
}}
|
||||||
|
|
||||||
<input
|
<input
|
||||||
|
id=id
|
||||||
type=move || input_type.get().as_str()
|
type=move || input_type.get().as_str()
|
||||||
|
name=name
|
||||||
value=input_value
|
value=input_value
|
||||||
prop:value=move || {
|
prop:value=move || {
|
||||||
let value = value.get();
|
let value = value.get();
|
||||||
|
@ -247,3 +279,85 @@ impl InputRef {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, PartialEq, Clone, Copy)]
|
||||||
|
pub enum InputRuleTrigger {
|
||||||
|
#[default]
|
||||||
|
Blur,
|
||||||
|
Focus,
|
||||||
|
Input,
|
||||||
|
Change,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum InputRuleValidator {
|
||||||
|
Required(MaybeSignal<bool>),
|
||||||
|
RequiredMessage(MaybeSignal<bool>, MaybeSignal<String>),
|
||||||
|
Validator(Callback<String, Result<(), FieldValidationState>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct InputRule {
|
||||||
|
validator: InputRuleValidator,
|
||||||
|
trigger: InputRuleTrigger,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputRule {
|
||||||
|
pub fn required(required: MaybeSignal<bool>) -> Self {
|
||||||
|
Self {
|
||||||
|
trigger: Default::default(),
|
||||||
|
validator: InputRuleValidator::Required(required),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn required_with_message(
|
||||||
|
required: MaybeSignal<bool>,
|
||||||
|
message: MaybeSignal<String>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
trigger: Default::default(),
|
||||||
|
validator: InputRuleValidator::RequiredMessage(required, message),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validator(f: impl Fn(&String) -> Result<(), FieldValidationState> + Send + Sync + 'static) -> Self {
|
||||||
|
Self {
|
||||||
|
trigger: Default::default(),
|
||||||
|
validator: InputRuleValidator::Validator(Callback::from(move |v| f(&v))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_trigger(mut self, trigger: InputRuleTrigger) -> Self {
|
||||||
|
self.trigger = trigger;
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn call_validator(
|
||||||
|
&self,
|
||||||
|
trigger: Option<InputRuleTrigger>,
|
||||||
|
value: Model<String>,
|
||||||
|
) -> Result<(), FieldValidationState> {
|
||||||
|
if let Some(trigger) = trigger {
|
||||||
|
if self.trigger != trigger {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
value.with_untracked(|value| match &self.validator {
|
||||||
|
InputRuleValidator::Required(required) => {
|
||||||
|
if required.get_untracked() && value.is_empty() {
|
||||||
|
Err(FieldValidationState::Error(String::from("")))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InputRuleValidator::RequiredMessage(required, message) => {
|
||||||
|
if required.get_untracked() && value.is_empty() {
|
||||||
|
Err(FieldValidationState::Error(message.get_untracked()))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InputRuleValidator::Validator(f) => f.run(value.clone()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue