impulse_thaw/field/
field.rs

1use crate::Label;
2use leptos::{context::Provider, either::EitherOf3, prelude::*};
3use thaw_components::OptionComp;
4use thaw_utils::{class_list, mount_style};
5use uuid::Uuid;
6
7#[component]
8pub fn Field(
9    #[prop(optional, into)] class: MaybeProp<String>,
10    /// The label associated with the field.
11    #[prop(optional, into)]
12    label: MaybeProp<String>,
13    /// A string specifying a name for the input control.
14    /// This name is submitted along with the control's value when the form data is submitted.
15    #[prop(optional, into)]
16    name: MaybeProp<String>,
17    /// The orientation of the label relative to the field component.
18    #[prop(optional, into)]
19    orientation: Signal<FieldOrientation>,
20    ///Is this input field required
21    #[prop(optional, into)]
22    required: Signal<bool>,
23    children: Children,
24) -> impl IntoView {
25    mount_style("field", include_str!("./field.css"));
26    let id = StoredValue::new(Uuid::new_v4().to_string());
27    let validation_state = RwSignal::new(None::<FieldValidationState>);
28
29    view! {
30        <div class=class_list![
31            "thaw-field",
32            move || {
33                format!("thaw-field--{}", orientation.get().as_str())
34            },
35            move || {
36                validation_state.with(|state| {
37                    if let Some(state) = state {
38                        Some(format!("thaw-field--{}", state.as_str()))
39                    } else {
40                        None
41                    }
42                })
43            },
44            class
45        ]>
46            {
47                let label = label.clone();
48                move || {
49                    view! {
50                        <OptionComp value=label.get() let:label>
51                            <Label
52                                class="thaw-field__label"
53                                required=required
54                                attr:r#for=id.get_value()
55                            >
56                                {label}
57                            </Label>
58                        </OptionComp>
59                    }
60                }
61            }
62            <Provider value=FieldInjection {
63                id,
64                name,
65                label,
66                validation_state,
67            }>{children()}</Provider>
68            {move || {
69                view! {
70                    <OptionComp value=validation_state.get() let:validation_state>
71                        {match validation_state {
72                            FieldValidationState::Error(message) => {
73                                EitherOf3::A(
74                                    view! {
75                                        <div class="thaw-field__validation-message">
76                                            <span class="thaw-field__validation-message-icon thaw-field__validation-message-icon--error">
77                                                <svg
78                                                    fill="currentColor"
79                                                    aria-hidden="true"
80                                                    width="12"
81                                                    height="12"
82                                                    viewBox="0 0 12 12"
83                                                >
84                                                    <path
85                                                        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"
86                                                        fill="currentColor"
87                                                    ></path>
88                                                </svg>
89                                            </span>
90                                            {message}
91                                        </div>
92                                    },
93                                )
94                            }
95                            FieldValidationState::Success(message) => {
96                                EitherOf3::B(
97                                    view! {
98                                        <div class="thaw-field__validation-message">
99                                            <span class="thaw-field__validation-message-icon thaw-field__validation-message-icon--success">
100                                                <svg
101                                                    fill="currentColor"
102                                                    aria-hidden="true"
103                                                    width="12"
104                                                    height="12"
105                                                    viewBox="0 0 12 12"
106                                                >
107                                                    <path
108                                                        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"
109                                                        fill="currentColor"
110                                                    ></path>
111                                                </svg>
112                                            </span>
113                                            {message}
114                                        </div>
115                                    },
116                                )
117                            }
118                            FieldValidationState::Warning(message) => {
119                                EitherOf3::C(
120                                    view! {
121                                        <div class="thaw-field__validation-message">
122                                            <span class="thaw-field__validation-message-icon thaw-field__validation-message-icon--warning">
123                                                <svg
124                                                    fill="currentColor"
125                                                    aria-hidden="true"
126                                                    width="12"
127                                                    height="12"
128                                                    viewBox="0 0 12 12"
129                                                >
130                                                    <path
131                                                        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"
132                                                        fill="currentColor"
133                                                    ></path>
134                                                </svg>
135                                            </span>
136                                            {message}
137                                        </div>
138                                    },
139                                )
140                            }
141                        }}
142
143                    </OptionComp>
144                }
145            }}
146        </div>
147    }
148}
149
150#[derive(Clone)]
151pub(crate) struct FieldInjection {
152    id: StoredValue<String>,
153    name: MaybeProp<String>,
154    label: MaybeProp<String>,
155    validation_state: RwSignal<Option<FieldValidationState>>,
156}
157
158impl FieldInjection {
159    pub fn use_context() -> Option<Self> {
160        use_context()
161    }
162
163    pub fn id(&self) -> Option<String> {
164        if self.label.with(|l| l.is_some()) {
165            Some(self.id.get_value())
166        } else {
167            None
168        }
169    }
170
171    pub fn name(&self) -> Option<String> {
172        self.name.get()
173    }
174
175    pub fn use_id_and_name(
176        id: MaybeProp<String>,
177        name: MaybeProp<String>,
178    ) -> (Signal<Option<String>>, Signal<Option<String>>) {
179        let field_injection = Self::use_context();
180        let id = Signal::derive(move || {
181            if let Some(id) = id.get() {
182                return Some(id);
183            }
184
185            let Some(field_injection) = field_injection.as_ref() else {
186                return None;
187            };
188
189            field_injection.id()
190        });
191
192        let field_injection = Self::use_context();
193        let name = Signal::derive(move || {
194            if let Some(name) = name.get() {
195                return Some(name);
196            }
197
198            let Some(field_injection) = field_injection.as_ref() else {
199                return None;
200            };
201
202            field_injection.name()
203        });
204
205        (id, name)
206    }
207
208    pub fn update_validation_state(&self, state: Result<(), FieldValidationState>) {
209        let state = state.err();
210        self.validation_state.try_maybe_update(|validation_state| {
211            if validation_state == &state {
212                (false, ())
213            } else {
214                *validation_state = state;
215                (true, ())
216            }
217        });
218    }
219}
220
221#[derive(Debug, Default, Clone)]
222pub enum FieldOrientation {
223    Horizontal,
224    #[default]
225    Vertical,
226}
227
228impl FieldOrientation {
229    pub fn as_str(&self) -> &'static str {
230        match self {
231            Self::Horizontal => "horizontal",
232            Self::Vertical => "vertical",
233        }
234    }
235}
236
237#[derive(Debug, Clone, PartialEq)]
238pub enum FieldValidationState {
239    Error(String),
240    Success(String),
241    Warning(String),
242}
243
244impl FieldValidationState {
245    pub fn as_str(&self) -> &'static str {
246        match self {
247            Self::Error(_) => "error",
248            Self::Success(_) => "success",
249            Self::Warning(_) => "warning",
250        }
251    }
252}