nova_forms/
form_context.rs

1use crate::{Data, FormData, QueryString, QueryStringPart};
2use leptos::*;
3use serde::{de::DeserializeOwned, Serialize};
4use std::{collections::HashMap, ops::Deref, str::FromStr, sync::atomic::AtomicU64};
5
6static VERSION: VersionProvider = VersionProvider::new();
7pub type Version = u64;
8
9struct VersionProvider(AtomicU64);
10
11impl VersionProvider {
12    const fn new() -> Self {
13        Self(AtomicU64::new(0))
14    }
15
16    fn next(&self) -> Version {
17        self.0.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
18    }
19}
20
21#[derive(Debug, Clone, Copy)]
22pub struct BaseGroupContext(GroupContext);
23
24impl BaseGroupContext {
25    pub(crate) fn new() -> Self {
26        let group = GroupContext(RwSignal::new(GroupData {
27            inputs: HashMap::new(),
28            order: Vec::new(),
29            qs: QueryString::default(),
30            disabled: false,
31            label: None,
32        }));
33        
34        BaseGroupContext(group)
35    }
36
37    pub fn to_group_context(self) -> GroupContext {
38        self.0
39    }
40}
41
42impl Deref for BaseGroupContext {
43    type Target = GroupContext;
44
45    fn deref(&self) -> &Self::Target {
46        &self.0
47    }
48}
49
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub struct InputContext(RwSignal<InputData>);
53
54#[derive(Debug, Clone)]
55struct InputData {
56    error: bool,
57    disabled: bool,
58    validate: Version,
59    creation: Version,
60    qs: QueryString,
61    label: Option<TextProp>,
62}
63
64impl InputContext {
65    pub fn new(bind: QueryStringPart) -> Self {
66        let context = expect_context::<GroupContext>();
67        Self::new_with_context(bind, context)
68    }
69
70    pub fn add_label(&self, label: TextProp) {
71        self.0.update(|input| {
72            input.label = Some(label);
73        });
74    }
75
76    pub fn label(&self) -> Option<TextProp> {
77        self.0.get_untracked().label
78    }
79
80    fn new_with_context(bind: QueryStringPart, context: GroupContext) -> Self {
81        let version = VERSION.next();
82
83        let input = InputContext(RwSignal::new(InputData {
84            error: false,
85            disabled: context.disabled().get_untracked(),
86            qs: context.qs().add(bind),
87            creation: version,
88            validate: 0,
89            label: None,
90        }));
91
92        context.register_input(bind, input);
93        on_cleanup(move || {
94            context.deregister(&bind);
95        });    
96        
97        input
98    }
99
100    pub fn set_error(&self, has_error: bool) {
101        self.0.update(|input| {
102            input.error = has_error;
103        });
104    }
105
106    pub fn error(&self) -> Signal<bool> {
107        let self_signal = self.0;
108        Memo::new(move |_| self_signal.get().error).into()
109    }
110
111    pub fn set_disabled(&self, disabled: bool) {
112        self.0.update(|input| {
113            input.disabled = disabled;
114        });
115    }
116
117    pub fn disabled(&self) -> Signal<bool> {
118        let self_signal = self.0;
119        Memo::new(move |_| self_signal.get().disabled).into()
120    }
121
122    pub fn validate(&self) {
123        self.0.update(|input| {
124            input.validate = VERSION.next();
125        });
126    }
127
128    pub fn validate_signal(&self) -> Signal<bool> {
129        let self_signal = self.0;
130        Memo::new(move |_| self_signal.get().validate > self_signal.get().creation).into()
131    }
132
133    pub fn qs(&self) -> QueryString {
134        self.0.get_untracked().qs
135    }
136
137    pub fn raw_value(&self) -> Signal<String> {
138        let qs = self.qs();
139        Signal::derive(move || {
140            expect_context::<FormData>()
141                .get(qs)
142                .get()
143                .map(|data| data.into_input().unwrap().raw().to_owned())
144                .unwrap_or_default()
145     })
146    }
147
148    pub fn set_raw_value<T: ToString>(&self, value: T) {
149        let qs = self.qs();
150        expect_context::<FormData>().set(qs, Data::new_input(value.to_string()));
151    }
152
153    pub fn value<T: FromStr>(&self) -> Signal<Result<T, T::Err>> {
154        let self_signal = self.raw_value();
155        Signal::derive(move || {
156            T::from_str(&self_signal.get())
157        })
158    }
159
160    pub fn set_value<T: ToString>(&self, value: T) {
161        self.set_raw_value(value);
162    }
163
164    pub fn get(&self, qs: QueryString) -> Signal<Option<Node>> {
165        let self_copy = *self;
166        Memo::new(move |_| {    
167            if qs.is_empty() {
168                Some(Node::Input(self_copy))
169            } else {
170                panic!("invalid query string");
171            }
172        }).into()
173    }
174}
175
176#[derive(Debug, Clone, Copy, PartialEq, Eq)]
177pub struct GroupContext(RwSignal<GroupData>);
178
179#[derive(Debug, Clone)]
180struct GroupData {
181    inputs: HashMap<QueryStringPart, Node>,
182    order: Vec<QueryStringPart>,
183    qs: QueryString,
184    disabled: bool,
185    label: Option<TextProp>,
186}
187
188impl GroupContext {
189    pub fn new(bind: QueryStringPart) -> Self {
190        let context = expect_context::<GroupContext>();
191        Self::new_with_context(bind, context)
192    }
193
194    pub fn add_label(&self, label: TextProp) {
195        self.0.update(|data| {
196            data.label = Some(label);
197        });
198    }
199
200    pub fn label(&self) -> Option<TextProp> {
201        self.0.get_untracked().label
202    }
203
204    fn new_with_context(bind: QueryStringPart, context: GroupContext) -> Self {
205
206        let group = GroupContext(RwSignal::new(GroupData {
207            inputs: HashMap::new(),
208            order: Vec::new(),
209            qs: context.qs().add(bind),
210            disabled: false,
211            label: None,
212        }));
213
214        context.register_group(bind, group);
215        on_cleanup(move || {
216            context.deregister(&bind);
217        });
218
219        group
220    }
221
222    pub fn disabled(&self) -> Signal<bool> {
223        let self_signal = self.0;
224        Signal::derive(move || self_signal.get().disabled)
225    }
226
227    pub fn nodes(&self) -> Vec<Node> {
228        self.0.get().order.iter().map(|qs| {
229            self.0.get().inputs.get(qs).unwrap().clone()
230        }).collect()
231    }
232
233    pub fn qs(&self) -> QueryString {
234        self.0.get_untracked().qs
235    }
236
237    fn register_input(&self, qs: QueryStringPart, input: InputContext) {
238        self.0.update(|data| {
239            data.inputs.insert(qs, Node::Input(input));
240            data.order.push(qs);
241        });
242    }
243
244    fn register_group(&self, qs: QueryStringPart, group: GroupContext) {
245        self.0.update(|data| {
246            data.inputs.insert(qs, Node::Group(group));
247            data.order.push(qs);
248        });
249    }
250
251    pub fn deregister(&self, qs: &QueryStringPart) {
252        self.0.update(|data| {
253            data.inputs.remove(qs);
254            data.order.retain(|k| k != qs);
255        });
256    }
257
258    pub fn set_disabled(&self, disabled: bool) {
259        logging::log!("set disabled: {}", disabled);
260        self.0.update(|data| {
261            data.disabled = disabled;
262        });
263        self.0.get_untracked().inputs.values().for_each(|node| {
264            match node {
265                Node::Input(input) => input.set_disabled(disabled),
266                Node::Group(group) => group.set_disabled(disabled),
267            }
268        });
269    }
270
271    pub fn error(&self) -> Signal<bool> {
272        let self_signal = self.0;
273        Signal::derive(move || self_signal.get().inputs.values().any(|node| {
274            match node {
275                Node::Input(input) => input.error().get(),
276                Node::Group(group) => group.error().get(),
277            }
278        }))
279    }
280
281    pub fn validate(&self) {
282        self.0.get_untracked().inputs.values().for_each(|node| {
283            match node {
284                Node::Input(input) => input.validate(),
285                Node::Group(group) => group.validate(),
286            }
287        });
288    }
289
290    pub fn len(&self) -> Signal<Option<usize>> {
291        let qs = self.qs();
292        Signal::derive(move || {
293            expect_context::<FormData>().get(qs).get().map(|d| d.as_group().unwrap().len())
294        })
295    }
296
297    pub fn raw_value(&self) -> Signal<Data> {
298        let qs = self.qs();
299        Signal::derive(move || {
300            expect_context::<FormData>().get(qs).get().unwrap()
301        })
302    }
303
304    pub fn set_raw_value(&self, data: Data) {
305        let qs = self.qs();
306        expect_context::<FormData>().set(qs, data);
307    }
308
309
310    pub fn value<T: DeserializeOwned + PartialEq>(&self) -> Signal<Option<T>> {
311        let data = self.raw_value();
312
313        Memo::new(move |_| {
314            let deserialized = data.get().to::<T>();
315
316            if cfg!(debug_assertions) {
317                if let Err(err) = &deserialized {
318                    logging::warn!("deserialization error, this may indicated that your form data is not matching your bindings: {:?}", err);
319                }
320            }
321    
322            Some(deserialized.ok()?)
323        }).into()
324    }
325
326    pub fn set_value<T: Serialize>(&self, value: &T) {
327        self.set_raw_value(Data::from(value));
328    }
329
330    pub fn get(&self, qs: QueryString) -> Signal<Option<Node>> {
331        let self_copy = *self;
332        Memo::new(move |_| {
333            let mut qs = qs.iter();
334            let (head, tail) = (qs.next(), qs.collect());
335            if let Some(head) = head {
336                self_copy.0.get().inputs.get(&head)?.get(tail).get()
337            } else {
338                Some(Node::Group(self_copy))
339            }
340        }).into()
341    }
342}
343
344#[derive(Debug, Clone, Copy, PartialEq, Eq)]
345pub enum Node {
346    Input(InputContext),
347    Group(GroupContext),
348}
349
350impl Node {
351    pub fn qs(&self) -> QueryString {
352        match self {
353            Node::Input(input) => input.qs(),
354            Node::Group(group) => group.qs(),
355        }
356    }
357
358    pub fn as_group(&self) -> Option<&GroupContext> {
359        match self {
360            Node::Input(_) => None,
361            Node::Group(group) => Some(group),
362        }
363    }
364
365    pub fn as_input(&self) -> Option<&InputContext> {
366        match self {
367            Node::Input(input) => Some(input),
368            Node::Group(_) => None,
369        }
370    }
371
372    pub fn into_group(self) -> GroupContext {
373        match self {
374            Node::Input(_) => panic!("expected group, got input"),
375            Node::Group(group) => group,
376        }
377    }
378
379    pub fn into_input(self) -> InputContext {
380        match self {
381            Node::Input(input) => input,
382            Node::Group(_) => panic!("expected input, got group"),
383        }
384    }
385
386    pub fn set_disabled(&self, disabled: bool) {
387        match self {
388            Node::Input(input) => input.set_disabled(disabled),
389            Node::Group(group) => group.set_disabled(disabled),
390        }
391    }
392
393    pub fn validate(&self) {
394        match self {
395            Node::Input(input) => input.validate(),
396            Node::Group(group) => group.validate(),
397        }
398    }
399
400    pub fn get(&self, qs: QueryString) -> Signal<Option<Node>> {
401        match self {
402            Node::Input(input) => input.get(qs),
403            Node::Group(group) => group.get(qs),
404        }
405    }
406}
407
408pub fn node(qs: QueryString) -> Signal<Option<Node>> {
409    let group = expect_context::<BaseGroupContext>();
410    group.get(qs)
411}
412
413#[macro_export]
414macro_rules! node {
415    ( $key:ident $( [ $tail:tt ] )* ) => {
416        node!(@part($crate::QueryString::default().add_key(stringify!($key))) $( [ $tail ] )*)
417    };
418    ( .. $( [ $tail:tt ] )* ) => {
419        node!(@part(leptos::expect_context::<$crate::GroupContext>().qs()) $( [ $tail ] )*)
420    };
421    ( @part($part:expr) [ $index:literal ] $( [ $tail:tt ] )* ) => {
422        node!(@part($part.add_index($index))$( [ $tail ] )*)
423    };
424    ( @part($part:expr) [ $key:ident ] $( [ $tail:tt ] )* ) => {
425        node!(@part($part.add_key(stringify!($key))) $( [ $tail ] )*)
426    };
427    ( @part($part:expr) ) => {
428        node($part)
429    };
430}
431
432#[macro_export]
433macro_rules! group {
434    ( $($key:ident)? $( [ $tail:tt ] )* ) => {
435        Signal::derive(move || Some(node!( $($key)? $( [ $tail ] )* ).get()?.into_group()))
436    };
437    ( .. $( [ $tail:tt ] )* ) => {
438        Signal::derive(move || Some(node!( .. $( [ $tail ] )* ).get()?.into_group()))
439    };
440}
441
442#[macro_export]
443macro_rules! input {
444    ( $($key:ident)? $( [ $tail:tt ] )* ) => {
445        Signal::derive(move || Some(node!( $($key)? $( [ $tail ] )* ).get()?.into_input()))
446    };
447    ( .. $( [ $tail:tt ] )* ) => {
448        Signal::derive(move || Some(node!( .. $( [ $tail ] )* ).get()?.into_input()))
449    };
450}
451
452#[macro_export]
453macro_rules! value {
454    ( $($key:ident)? $( [ $tail:tt ] )* as $ty:ty ) => {
455        Signal::derive(move || Some(input!( $($key)? $( [ $tail ] )* ).get()?.value::<$ty>().get().ok()?))
456    };
457    ( .. $( [ $tail:tt ] )* as $ty:ty ) => {
458        Signal::derive(move || Some(input!( .. $( [ $tail ] )* ).get()?.value::<$ty>().get().ok()?))
459    };
460}
461
462#[macro_export]
463macro_rules! values {
464    ( $($key:ident)? $( [ $tail:tt ] )* ) => {
465        Signal::derive(move || Some(group!( $($key)? $( [ $tail ] )* ).get()?.raw_value().get()))
466    };
467    ( .. $( [ $tail:tt ] )* ) => {
468        Signal::derive(move || Some(group!( .. $( [ $tail ] )* ).get()?.raw_value().get()))
469    };
470}
471
472#[cfg(test)]
473mod tests {
474    use crate::qs;
475    use super::*;
476
477    #[test]
478    fn test_node_macro() {
479        let _ = leptos::create_runtime();
480
481        let form_data = FormData::new();
482        let group = BaseGroupContext::new();
483        provide_context(form_data);
484        provide_context(group);
485        provide_context(group.to_group_context());
486
487        let group = GroupContext::new(QueryStringPart::from("a"));
488        let input = InputContext::new_with_context(QueryStringPart::from("b"), group);
489        input.set_raw_value("test");
490
491        let node = node!(a[b]);
492        assert_eq!(node.get_untracked().unwrap().as_input().unwrap().qs(), qs!(a[b]));
493    }
494
495    #[test]
496    fn test_input_macro() {
497        let _ = leptos::create_runtime();
498
499        let form_data = FormData::new();
500        let group = BaseGroupContext::new();
501        provide_context(form_data);
502        provide_context(group);
503        provide_context(group.to_group_context());
504
505        let group = GroupContext::new(QueryStringPart::from("a"));
506        let input = InputContext::new_with_context(QueryStringPart::from("b"), group);
507        input.set_raw_value("test");
508
509        let node = input!(a[b]);
510        assert_eq!(node.get_untracked().unwrap().qs(), qs!(a[b]));
511    }
512
513    #[test]
514    fn test_group_macro() {
515        let _ = leptos::create_runtime();
516
517        let form_data = FormData::new();
518        let group = BaseGroupContext::new();
519        provide_context(form_data);
520        provide_context(group);
521        provide_context(group.to_group_context());
522
523        let group = GroupContext::new(QueryStringPart::from("a"));
524        let input = InputContext::new_with_context(QueryStringPart::from("b"), group);
525        input.set_raw_value("test");
526
527        let node = group!(a);
528        assert_eq!(node.get_untracked().unwrap().nodes().len(), 1);
529    }
530
531    #[test]
532    fn test_value_macro() {
533        let _ = leptos::create_runtime();
534
535        let form_data = FormData::new();
536        let group = BaseGroupContext::new();
537        provide_context(form_data);
538        provide_context(group);
539        provide_context(group.to_group_context());
540
541        let group = GroupContext::new(QueryStringPart::from("a"));
542        let input = InputContext::new_with_context(QueryStringPart::from("b"), group);
543        input.set_raw_value("test");
544
545        let value = value!(a[b] as String);
546        assert_eq!(value.get_untracked().unwrap(), "test");
547    }
548
549    #[test]
550    fn test_input() {
551        let _ = leptos::create_runtime();
552
553        let form_data = FormData::new();
554        let group = BaseGroupContext::new();
555        provide_context(form_data);
556        provide_context(group.to_group_context());
557
558        let input = InputContext::new(QueryStringPart::from("a"));
559        input.set_raw_value("test");
560
561        assert_eq!(form_data.get(qs!(a)).get_untracked().unwrap().as_input().unwrap().raw(), "test");
562    }
563
564    #[test]
565    fn test_input_group() {
566        let _ = leptos::create_runtime();
567
568        let form_data = FormData::new();
569        let group = BaseGroupContext::new();
570        provide_context(form_data);
571        provide_context(group.to_group_context());
572
573        let group = GroupContext::new(QueryStringPart::from("a"));
574        let input = InputContext::new_with_context(QueryStringPart::from("b"), group);
575        input.set_raw_value("test");
576
577        assert_eq!(form_data.get(qs!(a[b])).get_untracked().unwrap().as_input().unwrap().raw(), "test");
578    }
579
580    #[test]
581    fn test_input_group_modify() {
582        let _ = leptos::create_runtime();
583
584        let form_data = FormData::new();
585        let group = BaseGroupContext::new();
586        provide_context(form_data);
587        provide_context(group.to_group_context());
588
589        let group = GroupContext::new(QueryStringPart::from("a"));
590        let input = InputContext::new_with_context(QueryStringPart::from("b"), group);
591        input.set_raw_value("test");
592
593        assert_eq!(form_data.get(qs!(a[b])).get_untracked().unwrap().as_input().unwrap().raw(), "test");
594
595        input.set_raw_value("test2");
596        assert_eq!(form_data.get(qs!(a[b])).get_untracked().unwrap().as_input().unwrap().raw(), "test2");
597    }
598
599    #[test]
600    fn test_input_group_validate() {
601        let _ = leptos::create_runtime();
602
603        let form_data = FormData::new();
604        let group = BaseGroupContext::new();
605        provide_context(form_data);
606        provide_context(group.to_group_context());
607
608        let group = GroupContext::new(QueryStringPart::from("a"));
609        let input1 = InputContext::new_with_context(QueryStringPart::from("b"), group);
610        let input2 = InputContext::new_with_context(QueryStringPart::from("c"), group);
611        group.validate();
612        let input3 = InputContext::new_with_context(QueryStringPart::from("d"), group);
613
614        assert_eq!(input1.validate_signal().get_untracked(), true);
615        assert_eq!(input2.validate_signal().get_untracked(), true);
616        assert_eq!(input3.validate_signal().get_untracked(), false);
617    }
618
619    #[test]
620    fn test_input_group_disable() {
621        let _ = leptos::create_runtime();
622
623        let form_data = FormData::new();
624        let group = BaseGroupContext::new();
625        provide_context(form_data);
626        provide_context(group.to_group_context());
627
628        let group = GroupContext::new(QueryStringPart::from("a"));
629        let input1 = InputContext::new_with_context(QueryStringPart::from("b"), group);
630        let input2 = InputContext::new_with_context(QueryStringPart::from("c"), group);
631        group.set_disabled(true);
632        let input3 = InputContext::new_with_context(QueryStringPart::from("d"), group);
633
634        assert_eq!(input1.disabled().get_untracked(), true);
635        assert_eq!(input2.disabled().get_untracked(), true);
636        assert_eq!(input3.disabled().get_untracked(), true);
637    }
638
639    #[test]
640    fn test_input_group_error() {
641        let _ = leptos::create_runtime();
642
643        let form_data = FormData::new();
644        let group = BaseGroupContext::new();
645        provide_context(form_data);
646        provide_context(group.to_group_context());
647
648        let group = GroupContext::new(QueryStringPart::from("a"));
649        let _input1 = InputContext::new_with_context(QueryStringPart::from("b"), group);
650        assert_eq!(group.error().get_untracked(), false);
651        
652        let input2 = InputContext::new_with_context(QueryStringPart::from("c"), group);
653        input2.set_error(true);
654        let _input3 = InputContext::new_with_context(QueryStringPart::from("d"), group);
655
656        assert_eq!(group.error().get_untracked(), true);
657    }
658}