thaw/switch/
mod.rs

1use crate::{FieldInjection, FieldValidationState, Rule};
2use leptos::{html, prelude::*};
3use std::ops::Deref;
4use thaw_utils::{class_list, mount_style, Model};
5
6#[component]
7pub fn Switch(
8    #[prop(optional, into)] class: MaybeProp<String>,
9    #[prop(optional, into)] id: MaybeProp<String>,
10    /// A string specifying a name for the input control.
11    /// This name is submitted along with the control's value when the form data is submitted.
12    #[prop(optional, into)]
13    name: MaybeProp<String>,
14    /// A String specifying the value of the input control.
15    #[prop(optional, into)]
16    value: MaybeProp<String>,
17    /// The rules to validate Field.
18    #[prop(optional, into)]
19    rules: Vec<SwitchRule>,
20    /// Defines the controlled checked state of the Switch.
21    #[prop(optional, into)]
22    checked: Model<bool>,
23    /// The Switch's label.
24    #[prop(optional, into)]
25    label: MaybeProp<String>,
26) -> impl IntoView {
27    mount_style("switch", include_str!("./switch.css"));
28    let (id, name) = FieldInjection::use_id_and_name(id, name);
29    let validate = Rule::validate(rules, checked, name);
30    let id = Signal::derive(move || id.get().unwrap_or_else(|| uuid::Uuid::new_v4().to_string()));
31    let input_ref = NodeRef::<html::Input>::new();
32    let on_change = move |_| {
33        let input = input_ref.get_untracked().unwrap();
34        let did_update = checked.try_maybe_update(|checked| {
35            if *checked != input.checked() {
36                *checked = input.checked();
37                (true, true)
38            } else {
39                (false, false)
40            }
41        });
42        if did_update.unwrap_or_default() {
43            validate.run(Some(SwitchRuleTrigger::Change));
44        }
45    };
46
47    view! {
48        <div class=class_list!["thaw-switch", class]>
49            <input
50                class="thaw-switch__input"
51                role="switch"
52                type="checkbox"
53                id=id
54                name=name
55                value=move || value.get()
56                checked=checked.get_untracked()
57                prop:checked=move || { checked.get() }
58                node_ref=input_ref
59                on:change=on_change
60            />
61            <div aria-hidden="true" class="thaw-switch__indicator thaw-switch__button">
62                <svg
63                    fill="currentColor"
64                    aria-hidden="true"
65                    width="1em"
66                    height="1em"
67                    viewBox="0 0 20 20"
68                >
69                    <path d="M10 2a8 8 0 1 0 0 16 8 8 0 0 0 0-16Z" fill="currentColor"></path>
70                </svg>
71            </div>
72            {move || {
73                if let Some(label) = label.get() {
74                    view! {
75                        <label class="thaw-switch__label" for=id>
76                            {label}
77                        </label>
78                    }
79                        .into()
80                } else {
81                    None
82                }
83            }}
84        </div>
85    }
86}
87
88#[derive(Debug, Default, PartialEq, Clone, Copy)]
89pub enum SwitchRuleTrigger {
90    #[default]
91    Change,
92}
93
94pub struct SwitchRule(Rule<bool, SwitchRuleTrigger>);
95
96impl SwitchRule {
97    pub fn validator(
98        f: impl Fn(&bool, Signal<Option<String>>) -> Result<(), FieldValidationState>
99            + Send
100            + Sync
101            + 'static,
102    ) -> Self {
103        Self(Rule::validator(f))
104    }
105
106    pub fn with_trigger(self, trigger: SwitchRuleTrigger) -> Self {
107        Self(Rule::with_trigger(self.0, trigger))
108    }
109}
110
111impl Deref for SwitchRule {
112    type Target = Rule<bool, SwitchRuleTrigger>;
113
114    fn deref(&self) -> &Self::Target {
115        &self.0
116    }
117}