nova_forms/components/
pages.rs

1use leptos::*;
2use crate::{Button, ButtonGroup, QueryStringPart};
3
4mod context {
5    use leptos::TextProp;
6    use ustr::Ustr;
7    use std::{convert::Infallible, str::FromStr};
8
9    #[derive(Debug, Clone)]
10    pub struct PageData {
11        id: PageId,
12        label: TextProp,
13        idx: usize,
14    }
15
16    impl PageData {
17        fn new(id: PageId, label: TextProp, idx: usize) -> Self {
18            Self { id, label, idx }
19        }
20
21        pub fn id(&self) -> PageId {
22            self.id
23        }
24
25        pub fn label(&self) -> TextProp {
26            self.label.clone()
27        }
28
29        pub fn idx(&self) -> usize {
30            self.idx
31        }
32    }
33
34    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35    pub struct PageId(Ustr);
36
37    impl ToString for PageId {
38        fn to_string(&self) -> String {
39            self.0.to_string()
40        }
41    }
42
43    impl FromStr for PageId {
44        type Err = Infallible;
45
46        fn from_str(s: &str) -> Result<Self, Self::Err> {
47            Ok(PageId(Ustr::from(s)))
48        }
49    }
50
51    impl PageId {
52        pub fn new(s: &str) -> Self {
53            PageId::from_str(s).unwrap()
54        }
55    }
56
57    #[derive(Debug, Clone)]
58    pub struct PageContext {
59        id: PageId,
60    }
61
62    impl PageContext {
63        pub fn new(id: PageId) -> Self {
64            Self { id }
65        }
66
67        #[allow(unused)]
68        pub fn id(&self) -> PageId {
69            self.id
70        }
71    }
72
73    #[derive(Debug, Clone, Default)]
74    pub struct PagesContext {
75        pages: Vec<PageData>,
76        selected: usize,
77    }
78
79    impl PagesContext {
80        pub fn register(&mut self, label: TextProp, id: PageId) {
81            self.pages.push(PageData::new(id, label, self.pages.len()));
82        }
83
84        pub fn is_selected(&self, id: PageId) -> bool {
85            self.pages
86                .iter()
87                .position(|t| t.id == id)
88                .map(|idx| idx == self.selected)
89                .unwrap_or(false)
90        }
91
92        pub fn is_first_selected(&self) -> bool {
93            self.pages.is_empty() || self.selected == 0
94        }
95
96        pub fn is_last_selected(&self) -> bool {
97            self.pages.is_empty() || self.selected == self.pages.len() - 1
98        }
99
100        pub fn next(&mut self) {
101            if self.selected + 1 < self.pages.len() {
102                self.selected += 1;
103            }
104        }
105
106        pub fn prev(&mut self) {
107            if self.selected > 0 {
108                self.selected -= 1;
109            }
110        }
111
112        pub fn select(&mut self, id: PageId) {
113            if let Some(idx) = self.pages.iter().position(|t| t.id == id) {
114                self.selected = idx;
115            }
116        }
117
118        pub fn len(&self) -> usize {
119            self.pages.len()
120        }
121
122        pub fn selected(&self) -> Option<PageId> {
123            self.pages
124                .get(self.selected)
125                .map(|tab_data| tab_data.id.clone())
126        }
127
128        pub fn pages(&self) -> &[PageData] {
129            self.pages.as_slice()
130        }
131    }
132}
133
134pub(crate) use context::*;
135
136use super::Group;
137
138#[component]
139pub fn Pages(
140    children: Children,
141) -> impl IntoView
142where
143{
144
145    let pages = create_rw_signal(PagesContext::default());
146    provide_context(pages);
147
148    let children = children();
149
150    view! { <div class="pages">{children}</div> }
151}
152
153
154/// Creates a new page in the form.
155#[component]
156pub fn Page(
157    /// An optional binding that creates a new group.
158    #[prop(into, optional)] bind: Option<QueryStringPart>,
159    /// The id of the page.
160    id: &'static str,
161    /// The label of the page.
162    #[prop(into)] label: TextProp,
163    /// The contents of the page.
164    children: Children
165) -> impl IntoView {
166    let id = PageId::new(id);
167
168    let pages_context = expect_context::<RwSignal<PagesContext>>();
169    pages_context.update(|pages_context| pages_context.register(label.clone(), id));
170
171    let label_clone = label.clone();
172
173    let page = move || view! {
174        <Provider value=PageContext::new(id)>
175            <div class=move || {
176                if pages_context.get().is_selected(id) { "page selected" } else { "page hidden" }
177            }>
178                <h2>{label}</h2>
179                {children()}
180            </div>
181        </Provider>
182    };
183
184    if let Some(bind) = bind {
185        view! {
186            <Group bind=bind label=label_clone>
187                {page()}
188            </Group>
189        }.into_view()
190    } else {
191        page().into_view()
192    }
193}
194
195
196/// Creates a new page in the form.
197#[component]
198pub fn PageStepper(
199) -> impl IntoView {
200    let pages_context = expect_context::<RwSignal<PagesContext>>();
201
202    view! {
203        <div class="stepper">
204            <ButtonGroup>
205                <Button
206                    label="Previous Page"
207                    icon="arrow_back"
208                    on:click=move |_| pages_context.update(|pages| pages.prev())
209                    disabled=Signal::derive(move || pages_context.get().is_first_selected())
210                />
211                <div class="stepper-spacer" />
212                <For
213                    each=move || {
214                        let pages = pages_context.get().pages().iter().cloned().collect::<Vec<_>>();
215                        pages
216                    }
217                    key=|page| page.id()
218                    children=move |page| {
219                        let page_id = page.id();
220                        view! {
221                            <button
222                                class="icon-button stepper-page-number"
223                                on:click=move |_| {
224                                    pages_context.update(|pages_context| pages_context.select(page_id))
225                                }
226                                disabled=move || pages_context.get().is_selected(page_id)
227                            >
228                                <span>{move || page.idx() + 1}</span>
229                            </button>
230                        }
231                    }
232                />
233                <div class="stepper-spacer" />
234                <Button
235                    label="Next Page"
236                    icon="arrow_forward"
237                    on:click=move |_| {
238                        pages_context.update(|pages_context| pages_context.next());
239                    }
240                    disabled=Signal::derive(move || pages_context.get().is_last_selected())
241                />
242            </ButtonGroup>
243        </div>
244    }
245}