nova_forms/components/
pages.rs1use 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#[component]
156pub fn Page(
157 #[prop(into, optional)] bind: Option<QueryStringPart>,
159 id: &'static str,
161 #[prop(into)] label: TextProp,
163 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#[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}