leptos_form_core/form_component/impls/
collections.rs

1use crate::components::*;
2use crate::*;
3use ::core::ops::*;
4use ::indexmap::IndexMap;
5use ::std::rc::Rc;
6
7/// Configuration for a Vec of FormFields
8#[derive(Clone, Default, Derivative, TypedBuilder)]
9#[builder(field_defaults(default, setter(into)))]
10#[derivative(Debug)]
11pub struct VecConfig<Config: Default> {
12    /// item level configuration
13    /// "item" refers to the inner type of Vec for which this config is being applied
14    pub item: Config,
15    /// class to be placed on container element which wraps both the label and the item
16    #[builder(setter(strip_option))]
17    pub item_container_class: Option<Oco<'static, str>>,
18    /// custom class to be passed into each item's FormField props
19    #[builder(setter(strip_option))]
20    pub item_class: Option<Oco<'static, str>>,
21    /// custom style to be passed into each item's FormField props
22    #[builder(setter(strip_option))]
23    pub item_style: Option<Oco<'static, str>>,
24    /// label configuration for each item
25    #[builder(setter(strip_option))]
26    pub item_label: Option<VecItemLabel>,
27    /// Vec size control for this form
28    pub size: VecConfigSize,
29    /// configuration for an add button at the end of the listed items
30    pub add: Adornment,
31    /// configuration for the remove buttons adorning each field
32    pub remove: Adornment,
33}
34
35/// Label configuration which will be set for each item
36/// in a Vec of FormFields
37#[derive(Clone, Debug, Default, TypedBuilder)]
38#[builder(field_defaults(default, setter(into)))]
39pub struct VecItemLabel {
40    /// custom class to set for each label
41    #[builder(setter(strip_option))]
42    pub class: Option<Oco<'static, str>>,
43    /// notation variant for the item label (determines the label content)
44    pub notation: Option<VecItemLabelNotation>,
45    /// punctuation style for the item label
46    pub punctuation: Option<VecItemLabelPunctuation>,
47    /// custom style to set for each label
48    #[builder(setter(strip_option))]
49    pub style: Option<Oco<'static, str>>,
50}
51
52/// When a label is configured to be set for a Vec of form fields,
53/// the label used will be produced using one of the below notations.
54#[derive(Clone, Copy, Debug, Default)]
55pub enum VecItemLabelNotation {
56    CapitalLetter,
57    Letter,
58    #[default]
59    Number,
60}
61
62/// Punctuation applied to the vec item label. Note that this configuration
63/// is a no-op wihtout the use of VecItemLabelNotation.
64#[derive(Clone, Copy, Debug, Default)]
65pub enum VecItemLabelPunctuation {
66    Parenthesis,
67    #[default]
68    Period,
69}
70
71#[derive(Clone, Copy, Debug, Default)]
72pub enum VecConfigSize {
73    Bounded {
74        min: Option<usize>,
75        max: Option<usize>,
76    },
77    Const(usize),
78    #[default]
79    Unbounded,
80}
81
82#[derive(Clone, Default, Derivative)]
83#[derivative(Debug)]
84pub enum Adornment {
85    None,
86    #[default]
87    Default,
88    Component(#[derivative(Debug = "ignore")] AdornmentComponent),
89    Spec(AdornmentSpec),
90}
91
92/// A Component type which accepts two arguments:
93/// - an on:click callback which accepts a MouseEvent argument
94/// - a derived signal returning a style string which should
95///   be placed on the top level component's `style:opacity` prop
96pub type AdornmentComponent = Rc<dyn Fn(Rc<dyn Fn(web_sys::MouseEvent)>, StyleSignal) -> View + 'static>;
97
98#[derive(Clone, Debug, TypedBuilder)]
99#[builder(field_defaults(default, setter(into)))]
100pub struct AdornmentSpec {
101    #[builder(setter(strip_option))]
102    pub class: Option<Oco<'static, str>>,
103    #[builder(default = 24)]
104    pub height: usize,
105    #[builder(setter(strip_option))]
106    pub style: Option<Oco<'static, str>>,
107    pub text: Option<Oco<'static, str>>,
108    #[builder(default = 24)]
109    pub width: usize,
110}
111
112impl Default for AdornmentSpec {
113    fn default() -> Self {
114        Self {
115            class: None,
116            height: 24,
117            style: None,
118            text: None,
119            width: 24,
120        }
121    }
122}
123
124impl<T: DefaultHtmlElement> DefaultHtmlElement for Vec<T> {
125    type El = Vec<T::El>;
126}
127
128#[derive(Clone, Copy, Debug)]
129pub struct VecSignalItem<Signal> {
130    id: usize,
131    signal: Signal,
132}
133
134impl<T, El> FormField<Vec<El>> for Vec<T>
135where
136    T: Clone + FormField<El>,
137    <T as FormField<El>>::Signal: Clone + std::fmt::Debug,
138{
139    type Config = VecConfig<<T as FormField<El>>::Config>;
140    type Signal = FormFieldSignal<IndexMap<usize, VecSignalItem<<T as FormField<El>>::Signal>>>;
141
142    fn default_signal(config: &Self::Config, initial: Option<Self>) -> Self::Signal {
143        FormFieldSignal::new_with_default_value(initial.map(|x| {
144            x.into_iter()
145                .enumerate()
146                .map(|(i, initial)| {
147                    (
148                        i,
149                        VecSignalItem {
150                            id: i,
151                            signal: T::default_signal(&config.item, Some(initial)),
152                        },
153                    )
154                })
155                .collect::<IndexMap<_, _>>()
156        }))
157    }
158    fn is_initial_value(signal: &Self::Signal) -> bool {
159        signal.initial.with(|initial| {
160            signal.value.with(|value| match initial.as_ref() {
161                Some(initial) => {
162                    let no_keys_changed = value.len() == initial.len()
163                        && value.last().map(|item| item.0) == initial.last().map(|item| item.0);
164                    if !no_keys_changed {
165                        return false;
166                    }
167                    value.iter().all(|(_, item)| T::is_initial_value(&item.signal))
168                }
169                None => value.is_empty(),
170            })
171        })
172    }
173    fn into_signal(self, config: &Self::Config, initial: Option<Self>) -> Self::Signal {
174        let has_initial = initial.is_some();
175        let mut initial = initial
176            .map(|x| x.into_iter().map(Some).collect::<Vec<_>>())
177            .unwrap_or_default();
178        if initial.len() < self.len() {
179            initial.append(&mut vec![None; self.len() - initial.len()]);
180        }
181        let value = self
182            .into_iter()
183            .zip(initial)
184            .enumerate()
185            .map(|(i, (item, initial))| {
186                (
187                    i,
188                    VecSignalItem {
189                        id: i,
190                        signal: item.into_signal(&config.item, initial),
191                    },
192                )
193            })
194            .collect::<IndexMap<_, _>>();
195        let initial = has_initial.then(|| value.clone());
196        FormFieldSignal::new(value, initial)
197    }
198    fn try_from_signal(signal: Self::Signal, config: &Self::Config) -> Result<Self, FormError> {
199        signal.with(|value| {
200            value
201                .iter()
202                .map(|(_, item)| T::try_from_signal(item.signal.clone(), &config.item))
203                .collect()
204        })
205    }
206    fn recurse(signal: &Self::Signal) {
207        signal.with(|sig| sig.iter().for_each(|(_, sig)| T::recurse(&sig.signal)))
208    }
209    fn reset_initial_value(signal: &Self::Signal) {
210        signal.value.update(|value| {
211            value.iter().for_each(|(_, item)| T::reset_initial_value(&item.signal));
212        });
213    }
214    fn with_error<O>(_: &Self::Signal, f: impl FnOnce(Option<&FormError>) -> O) -> O {
215        f(None)
216    }
217}
218
219impl<T, El, S> FormComponent<Vec<El>> for Vec<T>
220where
221    T: Clone + FormComponent<El, Signal = FormFieldSignal<S>>,
222    S: Clone + Eq + 'static + std::fmt::Debug,
223    <T as FormField<El>>::Config: std::fmt::Debug,
224{
225    fn render(props: RenderProps<Self::Signal, Self::Config>) -> impl IntoView {
226        let (min_items, max_items) = props.config.size.split();
227
228        let next_id =
229            create_rw_signal(
230                props
231                    .signal
232                    .with(|items| if items.is_empty() { 1 } else { items[items.len() - 1].id + 1 }),
233            );
234
235        if min_items.is_some() || max_items.is_some() {
236            props.signal.update(|items| {
237                let num_items = items.len();
238                if let Some(min_items) = min_items {
239                    if items.len() < min_items {
240                        items.reserve(min_items - num_items);
241                        while items.len() < min_items {
242                            let id = next_id.get_untracked();
243                            items.insert(
244                                id,
245                                VecSignalItem {
246                                    id,
247                                    signal: T::default_signal(&props.config.item, None),
248                                },
249                            );
250                            next_id.update(|x| *x += 1);
251                        }
252                    }
253                }
254                if let Some(max_items) = max_items {
255                    while max_items < items.len() {
256                        items.pop();
257                    }
258                }
259            });
260        }
261
262        let VecConfig {
263            item: item_config,
264            item_container_class,
265            item_class,
266            item_label,
267            item_style,
268            size,
269            add,
270            remove,
271        } = props.config;
272
273        let item_config_clone = item_config.clone();
274        view! {
275            <div id={props.id} class={props.class} style={props.style}>
276                <For
277                    key=|(_, (key, _))| *key
278                    each=move || props.signal.value.get().into_iter().enumerate()
279                    children=move |(index, (key, item))| {
280                        let id = || index.to_string();
281
282                        let item_props = RenderProps::builder()
283                            .id(Oco::Owned(id()))
284                            .name(crate::format_form_name(props.name.as_ref(), id()))
285                            .class(item_class.clone())
286                            .style(item_style.clone())
287                            .field_changed_class(props.field_changed_class.clone())
288                            .signal(item.signal)
289                            .config(item_config.clone())
290                            .build();
291
292                        VecConfig::<<T as FormField<El>>::Config>::wrap(
293                            &size,
294                            item_container_class.clone(),
295                            item_label.as_ref(),
296                            &remove,
297                            props.signal,
298                            key,
299                            Oco::Owned(id()),
300                            <T as FormComponent<El>>::render(item_props),
301                        ).into_view()
302                    }
303                />
304                {
305                    let num_items_is_max = move || {
306                        let num_items = props.signal.with(|items| items.len());
307                        num_items >= max_items.unwrap_or(usize::MAX)
308                    };
309
310                    let cursor = move || if num_items_is_max() { None } else { Some("pointer") };
311                    let opacity = move || if num_items_is_max() { Some("0.5") } else { None };
312
313                    let on_add = move |_| {
314                        if !num_items_is_max() {
315                            props.signal.update(|items| {
316                                let id = next_id.get_untracked();
317                                items.insert(id, VecSignalItem { id, signal: T::default_signal(&item_config_clone, None) });
318                                next_id.update(|x| *x = id + 1);
319                            });
320                        }
321                    };
322
323                    match (&size, &add) {
324                        (VecConfigSize::Const(_), _)|(_, Adornment::None) => View::default(),
325                        (_, Adornment::Component(component)) => component(Rc::new(on_add), Rc::new(opacity)),
326                        (_, Adornment::Default) => view! {
327                            <input
328                                type="button"
329                                on:click=on_add
330                                style:cursor=cursor
331                                style:margin-top="0.5 rem"
332                                style:opacity=opacity
333                                value="Add"
334                            />
335                        }
336                        .into_view(),
337                        (_, Adornment::Spec(adornment_spec)) => {
338                            let style = (adornment_spec.class.is_none() && adornment_spec.style.is_none()).then_some("margin-top: 0.5rem;");
339                            view! {
340                                <input
341                                    type="button"
342                                    class={adornment_spec.class.clone()}
343                                    cursor=cursor
344                                    on:click=on_add
345                                    style:opacity=opacity
346                                    style=style
347                                    value={adornment_spec.text.clone().unwrap_or(Oco::Borrowed("Add"))}
348                                />
349                            }
350                            .into_view()
351                        }
352                    }
353                }
354            </div>
355        }
356    }
357}
358
359impl From<usize> for VecConfigSize {
360    fn from(value: usize) -> Self {
361        Self::Const(value)
362    }
363}
364
365impl From<(usize, usize)> for VecConfigSize {
366    fn from(value: (usize, usize)) -> Self {
367        Self::Bounded {
368            min: Some(value.0),
369            max: Some(value.1),
370        }
371    }
372}
373
374static ASCII_LOWER: [char; 26] = [
375    'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w',
376    'x', 'y', 'z',
377];
378
379static ASCII_UPPER: [char; 26] = [
380    'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W',
381    'X', 'Y', 'Z',
382];
383
384impl<Config: Default> VecConfig<Config> {
385    #[allow(clippy::too_many_arguments)]
386    fn wrap<Signal: std::fmt::Debug>(
387        size: &VecConfigSize,
388        item_container_class: Option<Oco<'static, str>>,
389        item_label: Option<&VecItemLabel>,
390        remove_adornment: &Adornment,
391        signal: FormFieldSignal<IndexMap<usize, VecSignalItem<Signal>>>,
392        key: usize,
393        id: Oco<'static, str>,
394        item: impl IntoView,
395    ) -> impl IntoView {
396        // use leptos::ev::MouseEvent;
397        // use wasm_bindgen::{JsCast, UnwrapThrowExt};
398
399        let (min_items, _) = size.split();
400        let num_items_is_min = move || {
401            let num_items = signal.with(|items| items.len());
402            num_items <= min_items.unwrap_or_default()
403        };
404
405        let cursor: StyleSignal = Rc::new(move || if num_items_is_min() { None } else { Some("pointer") });
406        let opacity: StyleSignal = Rc::new(move || if num_items_is_min() { Some("0.5") } else { None });
407
408        let on_remove = move |_| {
409            if !num_items_is_min() {
410                signal.update(|items| {
411                    items.remove(&key);
412                });
413            }
414        };
415
416        let remove_component = match (size, remove_adornment) {
417            (VecConfigSize::Const(_), _) | (_, Adornment::None) => View::default(),
418            (_, Adornment::Component(component)) => component(Rc::new(on_remove), opacity),
419            (_, Adornment::Default) => {
420                view! {
421                    <MaterialClose
422                        cursor=cursor
423                        on:click=on_remove
424                        opacity=opacity
425                        style=Oco::Borrowed("margin-left: 0.5rem !important;")
426                    />
427                }
428            }
429            (_, Adornment::Spec(adornment_spec)) => {
430                let adornment_style = (adornment_spec.class.is_none() && adornment_spec.style.is_none())
431                    .then_some("margin-left: 0.5rem;");
432                view! {
433                    <MaterialClose
434                        class=adornment_spec.class.clone()
435                        cursor=cursor
436                        height=adornment_spec.height
437                        on:click=on_remove
438                        opacity=opacity
439                        style=adornment_style.map(Oco::from)
440                        width=adornment_spec.width
441                    />
442                }
443            }
444        };
445
446        view! {
447            <div class={item_container_class} style="display: flex; flex-direction: row; align-items: center; margin-bottom: 0.5rem">
448                {match item_label {
449                    Some(item_label) => item_label.wrap_label(key, id, item, signal),
450                    None => item.into_view(),
451                }}
452                {remove_component}
453            </div>
454        }
455    }
456}
457
458impl VecItemLabel {
459    fn wrap_label<Signal>(
460        &self,
461        key: usize,
462        id: Oco<'static, str>,
463        item: impl IntoView,
464        signal: FormFieldSignal<IndexMap<usize, VecSignalItem<Signal>>>,
465    ) -> View {
466        let notation = self.notation;
467        let punctuation = self.punctuation;
468        let prefix = move || {
469            signal
470                .with(|items| items.get_index_of(&key))
471                .and_then(|index| match (notation, punctuation) {
472                    (Some(notation), punctuation) => {
473                        Some(notation.render(index) + punctuation.map(|x| x.render()).unwrap_or_default())
474                    }
475                    _ => None,
476                })
477                .map(|prefix| view! { <div>{prefix}</div> }.into_view())
478                .unwrap_or_default()
479        };
480        view! {
481            <label for={id} class={self.class.clone()} style={self.style.clone()}>
482                {prefix}
483                {item}
484            </label>
485        }
486        .into_view()
487    }
488}
489
490impl VecConfigSize {
491    fn split(&self) -> (Option<usize>, Option<usize>) {
492        match *self {
493            VecConfigSize::Bounded { min, max } => (min, max),
494            VecConfigSize::Const(num_items) => (Some(num_items), Some(num_items)),
495            VecConfigSize::Unbounded => (None, None),
496        }
497    }
498}
499
500impl VecItemLabelNotation {
501    fn render(&self, index: usize) -> String {
502        let display_index = index + 1;
503        let ascii_set = match self {
504            Self::CapitalLetter => &ASCII_UPPER,
505            Self::Letter => &ASCII_LOWER,
506            Self::Number => return display_index.to_string(),
507        };
508        let n = (display_index.ilog(ascii_set.len()) + 1) as usize;
509        let mut chars = vec![' '; n];
510        let mut num = index;
511        for j in 0..n {
512            chars[n - 1 - j] = ascii_set[num % ascii_set.len()];
513            num /= ascii_set.len();
514        }
515        chars.into_iter().collect()
516    }
517}
518
519impl VecItemLabelPunctuation {
520    fn render(&self) -> &'static str {
521        match self {
522            Self::Parenthesis => ")",
523            Self::Period => ".",
524        }
525    }
526}