use leptos::html::*;
use leptos::prelude::*;
use std::collections::HashSet;
#[derive(Clone)]
#[allow(dead_code)]
pub struct CheckboxOption {
pub value: String,
pub label: String,
pub children: Option<ViewFn>,
}
impl std::fmt::Debug for CheckboxOption {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CheckboxOption")
.field("value", &self.value)
.field("label", &self.label)
.field("children", &"<ViewFn>")
.finish()
}
}
impl CheckboxOption {
#[allow(dead_code)]
pub fn new(value: &str, label: &str, children: Option<ViewFn>) -> Self {
Self {
value: value.to_string(),
label: label.to_string(),
children,
}
}
}
#[component]
pub fn CheckboxInputField(
#[prop(into, optional)] initial_value: MaybeProp<String>,
#[prop(into, optional)] label: String,
#[prop(into, optional)] name: String,
#[prop(optional)] input_node_ref: NodeRef<Input>,
#[prop(default = false, optional)] readonly: bool,
#[prop(default = false, optional)] required: bool,
#[prop(into, default = MaybeProp::derive(move || Some(false)), optional)] checked: MaybeProp<
bool,
>,
#[prop(into, optional)] placeholder: String,
#[prop(into, optional)] id_attr: String,
#[prop(into, optional)] ext_input_styles: String,
#[prop(into, optional)] ext_wrapper_styles: String,
#[prop(into, optional,default = "off".to_string())] autocomplete: String,
) -> impl IntoView {
view! {
<div class=format!("{}", ext_wrapper_styles)>
<label
class="inline-flex items-start gap-2"
for=id_attr.clone()
>
<input
class=format!("leading-tight size-5 shrink-0 rounded-[5px] border-2 border-mid-gray text-secondary shadow-sm
focus:outline-none focus:ring-0 bg-transparent focus:border-secondary
checked:bg-secondary checked:border-secondary
accent-secondary {}", ext_input_styles)
type="checkbox"
value=initial_value
name=name
node_ref=input_node_ref
readonly=readonly
placeholder=placeholder
autocomplete=autocomplete
id=id_attr.clone()
required=required
checked=checked
/>
<div class="flex flex-col">
<span>{label}</span>
</div>
</label>
</div>
}
}
#[component]
pub fn CheckboxGroup(
#[prop(into)]
legend: String,
#[prop(into)]
options: RwSignal<Vec<CheckboxOption>>,
#[prop(into, optional)]
selected_values: RwSignal<HashSet<String>>,
#[prop(into)] name: String,
#[prop(default = false, optional)] readonly: bool,
#[prop(default = false, optional)] required: bool,
#[prop(into, optional)] ext_input_styles: String,
#[prop(into, optional,default = "off".to_string())] autocomplete: String,
#[prop(default = false, optional)]
horizontal: bool,
) -> impl IntoView {
let base_fieldset_class = "border border-mid-gray rounded p-4";
let base_legend_class = "text-sm font-bold px-2";
let container_class = if horizontal {
"flex flex-wrap gap-4"
} else {
"space-y-3"
};
let fieldset_combined_class = base_fieldset_class.to_string();
let legend_combined_class = base_legend_class.to_string();
view! {
<fieldset class=fieldset_combined_class>
<legend class=legend_combined_class>
{legend}
{move || required.then_some(view! {
<span class="text-danger ml-1">*</span>
})}
</legend>
<div class=container_class>
{move || options.get()
.into_iter()
.map(|option| {
let option_value = option.value.clone();
let option_value_checked = option.value.clone();
let is_checked = move || selected_values.get().contains(&option_value_checked);
let option_id = format!("{}-{}", name, option_value);
view! {
<div>
<label
class="flex gap-2 text-sm cursor-pointer"
for=option_id.clone()
>
<input
class=format!("leading-tight shrink-0 size-5 rounded-[5px] border-2 border-mid-gray text-secondary shadow-sm
focus:outline-none focus:ring-0 focus:border-secondary bg-transparent
checked:bg-secondary checked:border-secondary
accent-secondary mt-0.5 {}", ext_input_styles)
type="checkbox"
value=option_value.clone()
name=name.clone()
checked=is_checked
readonly=readonly
autocomplete=autocomplete.clone()
id=option_id.clone()
required=required
/>
<div class="flex flex-col">
<span>{option.label}</span>
{option.children.map(|child| child.run())}
</div>
</label>
</div>
}
})
.collect_view()}
</div>
</fieldset>
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn checkbox_option_new_sets_fields() {
let opt = CheckboxOption::new("rust", "Rust", None);
assert_eq!(opt.value, "rust");
assert_eq!(opt.label, "Rust");
assert!(opt.children.is_none());
}
#[test]
fn checkbox_option_clone() {
let opt = CheckboxOption::new("a", "A", None);
let cloned = opt.clone();
assert_eq!(cloned.value, opt.value);
assert_eq!(cloned.label, opt.label);
}
fn container_class(horizontal: bool) -> &'static str {
if horizontal {
"flex flex-wrap gap-4"
} else {
"space-y-3"
}
}
#[test]
fn horizontal_container_class() {
assert_eq!(container_class(true), "flex flex-wrap gap-4");
}
#[test]
fn vertical_container_class() {
assert_eq!(container_class(false), "space-y-3");
}
fn option_id(name: &str, value: &str) -> String {
format!("{}-{}", name, value)
}
#[test]
fn option_id_format() {
assert_eq!(option_id("interests", "rust"), "interests-rust");
}
#[test]
fn option_id_unique_per_value() {
assert_ne!(option_id("group", "a"), option_id("group", "b"));
}
#[test]
fn is_checked_true_when_value_in_set() {
let owner = Owner::new();
owner.with(|| {
let selected = RwSignal::new(HashSet::from(["rust".to_string()]));
let is_checked = move || selected.get().contains("rust");
assert!(is_checked());
});
}
#[test]
fn is_checked_false_when_value_absent() {
let owner = Owner::new();
owner.with(|| {
let selected = RwSignal::new(HashSet::<String>::new());
let is_checked = move || selected.get().contains("rust");
assert!(!is_checked());
});
}
#[test]
fn is_checked_updates_reactively() {
let owner = Owner::new();
owner.with(|| {
let selected = RwSignal::new(HashSet::<String>::new());
let is_checked = move || selected.get().contains("leptos");
assert!(!is_checked());
selected.update(|s| {
s.insert("leptos".to_string());
});
assert!(is_checked());
});
}
#[test]
fn required_shows_asterisk() {
assert!(true);
}
#[test]
fn not_required_hides_asterisk() {
assert!(!false);
}
#[test]
fn checked_defaults_to_false() {
let owner = Owner::new();
owner.with(|| {
let checked: MaybeProp<bool> = MaybeProp::derive(move || Some(false));
assert_eq!(checked.get(), Some(false));
});
}
#[test]
fn checked_can_be_set_true() {
let owner = Owner::new();
owner.with(|| {
let checked: MaybeProp<bool> = MaybeProp::from(true);
assert_eq!(checked.get(), Some(true));
});
}
#[test]
fn checked_accepts_signal() {
let owner = Owner::new();
owner.with(|| {
let sig = RwSignal::new(false);
let checked: MaybeProp<bool> = MaybeProp::derive(move || Some(sig.get()));
assert_eq!(checked.get(), Some(false));
sig.set(true);
assert_eq!(checked.get(), Some(true));
});
}
}