mirror of
https://github.com/adoyle0/thaw.git
synced 2025-02-08 19:03:09 -05:00
feat: Combobox adds rules prop
This commit is contained in:
parent
d33b4e27e8
commit
f7b91eeb46
5 changed files with 112 additions and 5 deletions
|
@ -1,13 +1,19 @@
|
||||||
use super::listbox::{listbox_keyboard_event, Listbox};
|
use super::listbox::{listbox_keyboard_event, Listbox};
|
||||||
use crate::_aria::use_active_descendant;
|
use crate::{FieldInjection, FieldValidationState, Rule, _aria::use_active_descendant};
|
||||||
use leptos::{context::Provider, ev, html, prelude::*};
|
use leptos::{context::Provider, ev, html, prelude::*};
|
||||||
use std::collections::HashMap;
|
use std::{collections::HashMap, ops::Deref};
|
||||||
use thaw_components::{Binder, Follower, FollowerPlacement, FollowerWidth};
|
use thaw_components::{Binder, Follower, FollowerPlacement, FollowerWidth};
|
||||||
use thaw_utils::{add_event_listener, class_list, mount_style, Model, VecModel, VecModelWithValue};
|
use thaw_utils::{add_event_listener, class_list, mount_style, Model, VecModel, VecModelWithValue};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Combobox(
|
pub fn Combobox(
|
||||||
#[prop(optional, into)] class: MaybeProp<String>,
|
#[prop(optional, into)] class: MaybeProp<String>,
|
||||||
|
#[prop(optional, into)] id: MaybeProp<String>,
|
||||||
|
#[prop(optional, into)] rules: Vec<ComboboxRule>,
|
||||||
|
/// 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)] value: Model<String>,
|
#[prop(optional, into)] value: Model<String>,
|
||||||
/// Selected option.
|
/// Selected option.
|
||||||
#[prop(optional, into)]
|
#[prop(optional, into)]
|
||||||
|
@ -24,6 +30,8 @@ pub fn Combobox(
|
||||||
children: Children,
|
children: Children,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
mount_style("combobox", include_str!("./combobox.css"));
|
mount_style("combobox", include_str!("./combobox.css"));
|
||||||
|
let (id, name) = FieldInjection::use_id_and_name(id, name);
|
||||||
|
let validate = Rule::validate(rules, selected_options, name);
|
||||||
let trigger_ref = NodeRef::<html::Div>::new();
|
let trigger_ref = NodeRef::<html::Div>::new();
|
||||||
let input_ref = NodeRef::<html::Input>::new();
|
let input_ref = NodeRef::<html::Input>::new();
|
||||||
let listbox_ref = NodeRef::<html::Div>::new();
|
let listbox_ref = NodeRef::<html::Div>::new();
|
||||||
|
@ -53,6 +61,8 @@ pub fn Combobox(
|
||||||
}
|
}
|
||||||
e.stop_propagation();
|
e.stop_propagation();
|
||||||
selected_options.set(vec![]);
|
selected_options.set(vec![]);
|
||||||
|
value.set(String::new());
|
||||||
|
validate.run(Some(ComboboxRuleTrigger::Change));
|
||||||
});
|
});
|
||||||
on_cleanup(move || handler.remove());
|
on_cleanup(move || handler.remove());
|
||||||
});
|
});
|
||||||
|
@ -115,6 +125,7 @@ pub fn Combobox(
|
||||||
selected_options,
|
selected_options,
|
||||||
options,
|
options,
|
||||||
is_show_listbox,
|
is_show_listbox,
|
||||||
|
validate,
|
||||||
};
|
};
|
||||||
let (set_listbox, active_descendant_controller) =
|
let (set_listbox, active_descendant_controller) =
|
||||||
use_active_descendant(move |el| el.class_list().contains("thaw-combobox-option"));
|
use_active_descendant(move |el| el.class_list().contains("thaw-combobox-option"));
|
||||||
|
@ -136,6 +147,7 @@ pub fn Combobox(
|
||||||
VecModelWithValue::Vec(_) => value.set(String::new()),
|
VecModelWithValue::Vec(_) => value.set(String::new()),
|
||||||
});
|
});
|
||||||
active_descendant_controller.blur();
|
active_descendant_controller.blur();
|
||||||
|
validate.run(Some(ComboboxRuleTrigger::Blur));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -170,9 +182,11 @@ pub fn Combobox(
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
aria-expanded="true"
|
aria-expanded=move || if is_show_listbox.get() { "true" } else { "false" }
|
||||||
role="combobox"
|
role="combobox"
|
||||||
class="thaw-combobox__input"
|
class="thaw-combobox__input"
|
||||||
|
id=id
|
||||||
|
name=name
|
||||||
prop:value=move || { value.get() }
|
prop:value=move || { value.get() }
|
||||||
placeholder=move || placeholder.get()
|
placeholder=move || placeholder.get()
|
||||||
disabled=move || disabled.get()
|
disabled=move || disabled.get()
|
||||||
|
@ -274,6 +288,7 @@ pub(crate) struct ComboboxInjection {
|
||||||
selected_options: VecModel<String>,
|
selected_options: VecModel<String>,
|
||||||
options: StoredValue<HashMap<String, (String, String, MaybeSignal<bool>)>>,
|
options: StoredValue<HashMap<String, (String, String, MaybeSignal<bool>)>>,
|
||||||
is_show_listbox: RwSignal<bool>,
|
is_show_listbox: RwSignal<bool>,
|
||||||
|
validate: Callback<Option<ComboboxRuleTrigger>, bool>,
|
||||||
pub multiselect: bool,
|
pub multiselect: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -327,5 +342,65 @@ impl ComboboxInjection {
|
||||||
}
|
}
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
});
|
});
|
||||||
|
self.validate.run(Some(ComboboxRuleTrigger::Change));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, PartialEq, Clone, Copy)]
|
||||||
|
pub enum ComboboxRuleTrigger {
|
||||||
|
#[default]
|
||||||
|
Change,
|
||||||
|
Blur,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ComboboxRule(Rule<Vec<String>, ComboboxRuleTrigger>);
|
||||||
|
|
||||||
|
impl ComboboxRule {
|
||||||
|
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 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_empty() {
|
||||||
|
Err(FieldValidationState::Error(message.get_untracked()))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validator(
|
||||||
|
f: impl Fn(&Vec<String>, Signal<Option<String>>) -> Result<(), FieldValidationState>
|
||||||
|
+ Send
|
||||||
|
+ Sync
|
||||||
|
+ 'static,
|
||||||
|
) -> Self {
|
||||||
|
Self(Rule::validator(f))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_trigger(self, trigger: ComboboxRuleTrigger) -> Self {
|
||||||
|
Self(Rule::with_trigger(self.0, trigger))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for ComboboxRule {
|
||||||
|
type Target = Rule<Vec<String>, ComboboxRuleTrigger>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,7 @@ pub fn Listbox(
|
||||||
data-thaw-id=config_provider.id().clone()
|
data-thaw-id=config_provider.id().clone()
|
||||||
node_ref=listbox_ref
|
node_ref=listbox_ref
|
||||||
role="listbox"
|
role="listbox"
|
||||||
|
on:mousedown=|e| e.prevent_default()
|
||||||
>
|
>
|
||||||
{children()}
|
{children()}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -44,6 +44,12 @@ view! {
|
||||||
<Radio label="1" value="true"/>
|
<Radio label="1" value="true"/>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</Field>
|
</Field>
|
||||||
|
<Field name="combobox">
|
||||||
|
<Combobox rules=vec![ComboboxRule::required(true.into())] placeholder="Select an animal" clearable=true>
|
||||||
|
<ComboboxOption value="cat" text="Cat"/>
|
||||||
|
<ComboboxOption value="dog" text="Dog" />
|
||||||
|
</Combobox>
|
||||||
|
</Field>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
on:click={
|
on:click={
|
||||||
|
|
|
@ -2,7 +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, OptionModelWithValue};
|
use thaw_utils::{Model, OptionModel, OptionModelWithValue, VecModel, VecModelWithValue};
|
||||||
|
|
||||||
type RuleValidator<T> = Box<dyn Fn(&T, Signal<Option<String>>) -> Result<(), FieldValidationState>>;
|
type RuleValidator<T> = Box<dyn Fn(&T, Signal<Option<String>>) -> Result<(), FieldValidationState>>;
|
||||||
|
|
||||||
|
@ -129,3 +129,28 @@ impl RuleValueWithUntracked<Option<String>> for OptionModel<String> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl RuleValueWithUntracked<Vec<String>> for VecModel<String> {
|
||||||
|
fn value_with_untracked(
|
||||||
|
&self,
|
||||||
|
f: impl FnOnce(&Vec<String>) -> Result<(), FieldValidationState>,
|
||||||
|
) -> Result<(), FieldValidationState> {
|
||||||
|
self.with_untracked(move |v| match v {
|
||||||
|
VecModelWithValue::T(v) => {
|
||||||
|
if v.is_empty() {
|
||||||
|
f(&vec![])
|
||||||
|
} else {
|
||||||
|
f(&vec![v.clone()])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VecModelWithValue::Option(v) => {
|
||||||
|
if let Some(v) = v {
|
||||||
|
f(&vec![v.clone()])
|
||||||
|
} else {
|
||||||
|
f(&vec![])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VecModelWithValue::Vec(v) => f(v),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ where
|
||||||
|
|
||||||
impl<T: Default + Send + Sync> Default for VecModel<T> {
|
impl<T: Default + Send + Sync> Default for VecModel<T> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new(Default::default())
|
Self::new_option(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue