1use std::collections::HashSet;
13
14use tablo_ui::{
15 ButtonSize, ButtonVariant, FieldLegendVariant, button, field_error as ui_field_error,
16 field_legend, field_set,
17};
18use topcoat::{Result, context::Cx, view::*};
19
20use super::{Schema, fields::Field, read::Pairs, tree::Source};
21use crate::form::{FieldError, FieldErrors, FormValue, FormValues};
22
23pub trait RepeaterItem: Sized + Send + Sync + 'static {
29 fn schema() -> Schema;
31
32 fn parse(cx: &Cx, values: &FormValues) -> std::result::Result<Self, Vec<FieldError>>;
38
39 fn write(&self, out: &mut FormValues);
41}
42
43pub(crate) const ROW: &str = "__row__";
45
46pub const MAX_ROWS: usize = 100;
49
50pub(crate) struct RepeaterControl {
52 item: fn() -> Schema,
53 pub(crate) add_label: Option<String>,
55}
56
57impl RepeaterControl {
58 pub(crate) fn item_schema(&self) -> Schema {
60 (self.item)()
61 }
62
63 #[cfg(test)]
65 pub(crate) fn of(item: fn() -> Schema) -> Self {
66 Self {
67 item,
68 add_label: None,
69 }
70 }
71
72 pub(crate) fn new<T: RepeaterItem>() -> Self {
73 Self {
74 item: T::schema,
75 add_label: None,
76 }
77 }
78
79 pub(super) fn read(
90 &self,
91 key: &str,
92 pairs: &mut Pairs<'_>,
93 ) -> std::result::Result<Option<FormValue>, String> {
94 let Some(order) = pairs.claim(key).pop() else {
95 return Ok(None);
96 };
97 let item = (self.item)();
98 let mut seen = HashSet::new();
99 let mut rows = Vec::new();
100 for token in order.split(',').map(str::trim).filter(|t| !t.is_empty()) {
101 if rows.len() == MAX_ROWS {
102 return Err(format!("`{key}` lists more than {MAX_ROWS} rows"));
103 }
104 let row: usize = token
105 .parse()
106 .map_err(|_| format!("`{key}` lists `{token}`, which is not a row"))?;
107 if !seen.insert(row) {
108 return Err(format!("`{key}` lists row {row} twice"));
109 }
110 let mut posted = Vec::new();
111 for leaf in item.fields().map(Field::name) {
112 for value in pairs.claim(&row_key(key, &row.to_string(), leaf)) {
113 posted.push((leaf.to_string(), value.to_string()));
114 }
115 }
116 if posted.is_empty() {
117 return Err(format!("`{key}` lists row {row}, which posts nothing"));
118 }
119 rows.push(item.read(&posted)?);
120 }
121 Ok(Some(FormValue::Rows(rows)))
122 }
123
124 pub(crate) async fn check(
128 &self,
129 cx: &Cx,
130 key: &str,
131 rows: &[FormValues],
132 errors: &mut FieldErrors,
133 ) {
134 for (row, item) in rows.iter().enumerate() {
135 let (schema, values) = self.row(key, &row.to_string(), item);
136 Box::pin(schema.check_controls(cx, &values, &FormValues::new(), errors)).await;
137 }
138 }
139
140 fn row(&self, key: &str, row: &str, item: &FormValues) -> (Schema, FormValues) {
143 let prefix = row_key(key, row, "");
144 let mut schema = (self.item)();
145 for field in &mut schema.fields {
146 field.prefix(&prefix);
147 }
148 let values = item
149 .iter()
150 .map(|(leaf, value)| (format!("{prefix}{leaf}"), value.clone()))
151 .collect();
152 (schema, values)
153 }
154
155 pub(crate) async fn render<'a>(
158 &self,
159 cx: &'a Cx,
160 field: &Field,
161 source: &Source<'_>,
162 ) -> Result<BoxView<'a>> {
163 let key = field.name().to_string();
164 let items = source.rows(&key);
165 let mut rows = Vec::with_capacity(items.len());
166 for (row, item) in items.iter().enumerate() {
167 rows.push(
168 self.render_row(cx, &key, &row.to_string(), item, source.errors(), source)
169 .await?,
170 );
171 }
172 let blank = self
173 .render_row(
174 cx,
175 &key,
176 ROW,
177 &FormValues::new(),
178 &FieldErrors::new(),
179 source,
180 )
181 .await?;
182 let order = (0..items.len())
183 .map(|row| row.to_string())
184 .collect::<Vec<_>>()
185 .join(",");
186 let next = items.len().to_string();
187 let label = field.label_str().to_string();
188 let add = self
189 .add_label
190 .clone()
191 .unwrap_or_else(|| "Add item".to_string());
192 let error = source
193 .error_for(field)
194 .map(|error| (error.message(&label), super::fields::error_kind(error)));
195 let legend_id = format!("{}-legend", source.id(&key));
196 Ok(view! {
197 cx =>
198 field_set(
199 attrs: attributes! {
200 class="ac-field gap-3"
201 data-repeater=(key.clone())
202 data-repeater-next=(next)
203 },
204 field_legend(
205 variant: FieldLegendVariant::Label,
206 attrs: attributes! { id=(legend_id.clone()) class="mb-0" },
207 (label)
208 )
209 <input type="hidden" name=(key) value=(order) data-repeater-order="">
210 if let Some((message, kind)) = error {
211 ui_field_error(
212 attrs: attributes! { class="ac-error" data-error=(kind) },
213 (message)
214 )
215 }
216 <ol
217 class="flex flex-col gap-3"
218 aria-labelledby=(legend_id)
219 data-repeater-rows=""
220 >
221 for row in rows {
222 (row)
223 }
224 </ol>
225 <template data-repeater-blank="">(blank)</template>
226 <div>
227 button(
228 variant: ButtonVariant::Outline,
229 size: ButtonSize::Sm,
230 attrs: attributes! { type="button" data-repeater-add="" },
231 (add)
232 )
233 </div>
234 )
235 }
236 .boxed())
237 }
238
239 async fn render_row<'a>(
242 &self,
243 cx: &'a Cx,
244 key: &str,
245 row: &str,
246 item: &FormValues,
247 errors: &FieldErrors,
248 source: &Source<'_>,
249 ) -> Result<BoxView<'a>> {
250 let (schema, values) = self.row(key, row, item);
251 let mut rendered = Source::form(&values, errors).scoped(source.scope());
252 if let Some(options) = source.options() {
253 rendered = rendered.options_at(options);
254 }
255 let controls = schema.render(cx, rendered).await?;
256 let row = row.to_string();
257 let action = |label: &'static str, data: Attributes| {
258 view! {
259 cx =>
260 button(
261 variant: ButtonVariant::Ghost,
262 size: ButtonSize::Sm,
263 attrs: attributes! { type="button" (data) },
264 (label)
265 )
266 }
267 };
268 Ok(view! {
269 cx =>
270 <li
271 class="flex flex-col gap-4 rounded-lg border border-border p-4"
272 data-repeater-row=(row)
273 >
274 (controls)
275 <div class="flex flex-wrap justify-end gap-2">
276 (action("Move up", attributes! { cx => data-repeater-up="" }))
277 (action("Move down", attributes! { cx => data-repeater-down="" }))
278 (action("Remove", attributes! { cx => data-repeater-remove="" }))
279 </div>
280 </li>
281 }
282 .boxed())
283 }
284}
285
286pub(crate) fn row_key(key: &str, row: &str, leaf: &str) -> String {
288 format!("{key}.{row}.{leaf}")
289}
290
291pub fn write_items<T: RepeaterItem>(items: &[T]) -> FormValue {
294 FormValue::Rows(
295 items
296 .iter()
297 .map(|item| {
298 let mut out = FormValues::new();
299 item.write(&mut out);
300 out
301 })
302 .collect(),
303 )
304}
305
306pub fn parse_items<T: RepeaterItem>(
315 cx: &Cx,
316 key: &str,
317 values: &FormValues,
318) -> std::result::Result<Vec<T>, Vec<FieldError>> {
319 let rows: &[FormValues] = match values.get(key) {
320 None => &[],
321 Some(FormValue::Rows(rows)) => rows,
322 Some(_) => return Err(vec![crate::form::unreadable(key)]),
323 };
324 let mut items = Vec::with_capacity(rows.len());
325 let mut errors = Vec::new();
326 for (row, values) in rows.iter().enumerate() {
327 match T::parse(cx, values) {
328 Ok(item) => items.push(item),
329 Err(failures) => errors.extend(failures.into_iter().map(|failure| FieldError {
330 key: row_key(key, &row.to_string(), &failure.key),
331 kind: failure.kind,
332 })),
333 }
334 }
335 if errors.is_empty() {
336 Ok(items)
337 } else {
338 Err(errors)
339 }
340}
341
342#[cfg(test)]
343mod tests;