impulse_thaw/field/
field.rs1use 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 #[prop(optional, into)]
12 label: MaybeProp<String>,
13 #[prop(optional, into)]
16 name: MaybeProp<String>,
17 #[prop(optional, into)]
19 orientation: Signal<FieldOrientation>,
20 #[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}