Skip to main content

tablo_core/schema/
condition.rs

1//! Conditions: a field or a layout block that shows only while another field posts one of the
2//! values its condition names.
3//!
4//! The browser follows the watched field through one signal per condition, so the form needs no
5//! rerun. A hidden field or block sits in a disabled fieldset, which the browser neither validates
6//! nor submits. The server reads the same submission: [`Schema::condition_hidden`] names the
7//! fields it hides, and the panel drops their keys before parsing, so an edit keeps their stored
8//! values and a create takes their blank answers.
9
10use std::collections::HashMap;
11
12use topcoat::{
13    context::Cx,
14    runtime::{Event, Signal, signal},
15    view::*,
16};
17
18use super::{
19    Schema,
20    fields::{ChoiceField, CustomField, Field, TextField},
21    tree::Node,
22};
23use crate::{DeclarationErrorKind, topcoat_compat::async_page, values::FormValues};
24
25/// A field a condition can watch: a text, choice or custom field of the form `F`, passed to
26/// `visible_when` by reference.
27///
28/// A condition follows a toggle (`Field::toggle`, `Field::toggle_input`) through its checkbox, and
29/// any other control through its `value`: an app's own checkbox control posts the same `value`
30/// checked or not, so a condition cannot follow it.
31pub trait Watched<F> {
32    #[doc(hidden)]
33    fn key(&self) -> &str;
34
35    #[doc(hidden)]
36    fn checkbox(&self) -> bool {
37        false
38    }
39}
40
41impl<F> Watched<F> for TextField<F> {
42    fn key(&self) -> &str {
43        self.name()
44    }
45}
46
47impl<F> Watched<F> for ChoiceField<F> {
48    fn key(&self) -> &str {
49        self.name()
50    }
51}
52
53impl<F> Watched<F> for CustomField<F> {
54    fn key(&self) -> &str {
55        self.name()
56    }
57
58    fn checkbox(&self) -> bool {
59        self.is_checkbox()
60    }
61}
62
63/// Shows a field or a block while the field `watched` posts one of `values`.
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub(crate) struct Condition {
66    watched: String,
67    /// Whether the watched field is a checkbox, which renders unchecked with no value.
68    checkbox: bool,
69    values: Vec<String>,
70    /// Whether a submission that hides what this guards still parses, as an embedded shared
71    /// column's does: the codec reads that column only for the variants that declare it.
72    parses_when_hidden: bool,
73}
74
75impl Condition {
76    pub(crate) fn new<F>(
77        watched: &impl Watched<F>,
78        values: impl IntoIterator<Item = impl Into<String>>,
79    ) -> Self {
80        Self {
81            watched: watched.key().to_string(),
82            checkbox: watched.checkbox(),
83            values: values.into_iter().map(Into::into).collect(),
84            parses_when_hidden: false,
85        }
86    }
87
88    /// A condition watching the field named `key`, which posts a value rather than a checkbox's
89    /// checked state.
90    pub(crate) fn watching(
91        key: impl Into<String>,
92        values: impl IntoIterator<Item = impl Into<String>>,
93    ) -> Self {
94        Self {
95            watched: key.into(),
96            checkbox: false,
97            values: values.into_iter().map(Into::into).collect(),
98            parses_when_hidden: true,
99        }
100    }
101
102    /// Whether the submission `values` shows what the condition guards. A checkbox with no value
103    /// reads as `false`, as it renders and posts.
104    pub(crate) fn holds(&self, values: &FormValues) -> bool {
105        let posted = match values.text(&self.watched) {
106            Some(value) => value.trim(),
107            None if self.checkbox => "false",
108            None => "",
109        };
110        self.values.iter().any(|value| value == posted)
111    }
112
113    /// Renders `inner` in a fieldset the browser hides and disables while the condition fails, so
114    /// a hidden required field never blocks the submit.
115    pub(crate) fn guard<'a>(
116        &self,
117        cx: &'a Cx,
118        scope: &str,
119        values: &FormValues,
120        inner: BoxView<'a>,
121    ) -> BoxView<'a> {
122        let condition = self.clone();
123        let initial = self.holds(values);
124        let scope = scope.to_string();
125        async_page(async move {
126            let shown = shown(cx, &scope, &condition, initial);
127            let enabled = shown.clone();
128            Ok(view! {
129                cx =>
130                <fieldset
131                    class="contents"
132                    :disabled=$(!enabled.get())
133                    :hidden=$(!shown.get())
134                >
135                    (inner)
136                </fieldset>
137            })
138        })
139    }
140}
141
142/// The signal holding whether `condition` holds in the form `scope`, shared by its guard and its
143/// watched field.
144///
145/// Keyed by the page's path: navigation carries the values of signals two pages share, and
146/// another record's form starts from its own stored values. Keyed by the form's scope too: an
147/// action's input in a dialog may watch a key the page's own form watches.
148fn shown(cx: &Cx, scope: &str, condition: &Condition, initial: bool) -> Signal<bool> {
149    let page = topcoat::context::try_request_context::<http::request::Parts>(cx)
150        .map(|parts| parts.uri.path().to_string())
151        .unwrap_or_default();
152    signal(&cx.keyed(signal_key(&page, scope, condition)), move || {
153        initial
154    })
155}
156
157/// What names the signal of `condition` in the form `scope` on the page at `page`.
158fn signal_key(
159    page: &str,
160    scope: &str,
161    condition: &Condition,
162) -> (&'static str, String, String, String, String) {
163    (
164        "tablo-condition",
165        page.to_string(),
166        scope.to_string(),
167        condition.watched.clone(),
168        condition.values.join("\u{1f}"),
169    )
170}
171
172/// Renders the watched field `field` inside one change handler per condition reading it.
173///
174/// A handler reads only the event of the control posting the watched key: a searchable choice's
175/// filter box or an app control's own inputs change inside the same wrapper. A runtime expression
176/// has no `||`, so a condition naming several values stacks one handler per value: the innermost
177/// sets the signal from the first value, and each outer one, run after it as the event bubbles,
178/// sets it when its own value matches.
179pub(crate) fn watch<'a>(
180    cx: &'a Cx,
181    field: &Field,
182    conditions: &[Condition],
183    scope: &str,
184    values: &FormValues,
185    control: BoxView<'a>,
186) -> BoxView<'a> {
187    let scope = scope.to_string();
188    let conditions: Vec<(Condition, bool)> = conditions
189        .iter()
190        .map(|condition| (condition.clone(), condition.holds(values)))
191        .collect();
192    let checkbox = field.is_checkbox();
193    let key = field.name().to_string();
194    async_page(async move {
195        let mut view = control;
196        for (condition, initial) in conditions {
197            let shown = shown(cx, &scope, &condition, initial);
198            if checkbox {
199                // A checkbox posts `true` when checked and its hidden `false` otherwise.
200                let on = condition.values.iter().any(|value| value == "true");
201                let off = condition.values.iter().any(|value| value == "false");
202                let name = key.clone();
203                view = view! {
204                    cx =>
205                    <div
206                        class="contents"
207                        @change=$(|e: Event| if e.target.name == name {
208                            shown.set(if e.target.checked { on } else { off })
209                        })
210                    >
211                        (view)
212                    </div>
213                }
214                .boxed();
215                continue;
216            }
217            let mut values = condition.values.into_iter();
218            let Some(first) = values.next() else {
219                continue;
220            };
221            let (set, name) = (shown.clone(), key.clone());
222            view = view! {
223                cx =>
224                <div
225                    class="contents"
226                    @change=$(|e: Event| if e.target.name == name {
227                        set.set(e.target.value.trim() == first)
228                    })
229                >
230                    (view)
231                </div>
232            }
233            .boxed();
234            for value in values {
235                let (set, name) = (shown.clone(), key.clone());
236                view = view! {
237                    cx =>
238                    <div
239                        class="contents"
240                        @change=$(|e: Event| if e.target.name == name {
241                            if e.target.value.trim() == value {
242                                set.set(true)
243                            }
244                        })
245                    >
246                        (view)
247                    </div>
248                }
249                .boxed();
250            }
251        }
252        Ok(view)
253    })
254}
255
256impl Node {
257    /// The condition on this node: a field's own, or a layout block's.
258    fn condition<'s>(&'s self, fields: &'s [Field]) -> Option<&'s Condition> {
259        match self {
260            Node::Field(index) => fields[*index].condition(),
261            Node::Section(s) => s.condition.as_ref(),
262            Node::Group(g) => g.condition.as_ref(),
263            Node::Grid(g) => g.condition.as_ref(),
264            Node::Tabs(t) => t.condition.as_ref(),
265            Node::Tab(t) => t.condition.as_ref(),
266            Node::Embedded(_) => None,
267        }
268    }
269}
270
271/// Where a schema's conditions sit.
272#[derive(Default)]
273struct Scan<'s> {
274    /// Each field slot with the conditions guarding it, outermost first and its own last.
275    fields: Vec<(usize, Vec<&'s Condition>)>,
276    /// Each condition with the conditions guarding the node it sits on.
277    conditions: Vec<(&'s Condition, Vec<&'s Condition>)>,
278}
279
280impl<'s> Scan<'s> {
281    fn of(nodes: &'s [Node], fields: &'s [Field]) -> Self {
282        let mut scan = Self::default();
283        scan.walk(nodes, fields, &mut Vec::new());
284        scan
285    }
286
287    fn walk(&mut self, nodes: &'s [Node], fields: &'s [Field], chain: &mut Vec<&'s Condition>) {
288        for node in nodes {
289            let own = node.condition(fields);
290            if let Some(condition) = own {
291                self.conditions.push((condition, chain.clone()));
292                chain.push(condition);
293            }
294            match node {
295                Node::Field(index) => self.fields.push((*index, chain.clone())),
296                Node::Embedded(embedded) => {
297                    // An embedded value's leaves are placed by its node, and each carries its own
298                    // condition as a top-level field does.
299                    embedded.visit_fields(&mut |index| {
300                        let own = fields[index].condition();
301                        if let Some(condition) = own {
302                            self.conditions.push((condition, chain.clone()));
303                            chain.push(condition);
304                        }
305                        self.fields.push((index, chain.clone()));
306                        if own.is_some() {
307                            chain.pop();
308                        }
309                    });
310                }
311                node => self.walk(node.children().unwrap_or_default(), fields, chain),
312            }
313            if own.is_some() {
314                chain.pop();
315            }
316        }
317    }
318
319    /// The conditions guarding the field named `key`, or `None` when the schema places none.
320    fn guarding(&self, fields: &[Field], key: &str) -> Option<&[&'s Condition]> {
321        self.fields
322            .iter()
323            .find(|(index, _)| fields[*index].name() == key)
324            .map(|(_, chain)| chain.as_slice())
325    }
326}
327
328impl<F> Schema<F> {
329    /// The keys of the fields a condition hides in the submission `values`.
330    pub(crate) fn condition_hidden(&self, values: &FormValues) -> Vec<String> {
331        Scan::of(&self.nodes, &self.fields)
332            .fields
333            .into_iter()
334            .filter(|(_, chain)| chain.iter().any(|condition| !condition.holds(values)))
335            .map(|(index, _)| self.fields[index].name().to_string())
336            .collect()
337    }
338
339    /// Each key a condition watches, with the key of a field it guards, or `None` when it guards
340    /// none.
341    pub(crate) fn condition_watchers(&self) -> Vec<(String, Option<String>)> {
342        let scan = Scan::of(&self.nodes, &self.fields);
343        let mut watchers = Vec::new();
344        for (condition, _) in &scan.conditions {
345            let mut guards = scan
346                .fields
347                .iter()
348                .filter(|(_, chain)| chain.iter().any(|c| std::ptr::eq(*c, *condition)))
349                .map(|(index, _)| self.fields[*index].name().to_string())
350                .peekable();
351            if guards.peek().is_none() {
352                watchers.push((condition.watched.clone(), None));
353            }
354            watchers.extend(guards.map(|key| (condition.watched.clone(), Some(key))));
355        }
356        watchers
357    }
358
359    /// The conditions each watched field's key drives, each once.
360    pub(crate) fn watched(&self) -> HashMap<String, Vec<Condition>> {
361        let mut out: HashMap<String, Vec<Condition>> = HashMap::new();
362        for (condition, _) in Scan::of(&self.nodes, &self.fields).conditions {
363            let driven = out.entry(condition.watched.clone()).or_default();
364            if !driven.contains(condition) {
365                driven.push(condition.clone());
366            }
367        }
368        out
369    }
370
371    /// What is wrong with the schema's conditions: a watched field the schema does not place, one
372    /// a condition hides while what it guards still shows, a value the watched field never posts,
373    /// and a hidden field with no blank answer, which a submission that hides it could not parse.
374    pub(crate) fn condition_errors(&self) -> Vec<DeclarationErrorKind> {
375        let scan = Scan::of(&self.nodes, &self.fields);
376        let mut errors = Vec::new();
377        for (condition, guarding) in &scan.conditions {
378            let watched = condition.watched.clone();
379            match scan.guarding(&self.fields, &condition.watched) {
380                None => errors.push(DeclarationErrorKind::UnplacedWatchedField {
381                    field: watched.clone(),
382                }),
383                // The browser follows the watched field's last change, the server its posted
384                // value: they agree only while every condition hiding the watched field also
385                // hides what it guards.
386                Some(chain)
387                    if !chain
388                        .iter()
389                        .all(|hiding| guarding.iter().any(|c| std::ptr::eq(*c, *hiding))) =>
390                {
391                    errors.push(DeclarationErrorKind::HiddenWatchedField {
392                        field: watched.clone(),
393                    });
394                }
395                Some(_) => {}
396            }
397            if let Some(field) = self.fields.iter().find(|f| f.name() == condition.watched) {
398                for value in &condition.values {
399                    if field.can_post(value) == Some(false) {
400                        errors.push(DeclarationErrorKind::UnpostedConditionValue {
401                            field: watched.clone(),
402                            value: value.clone(),
403                        });
404                    }
405                }
406            }
407        }
408        for (index, chain) in &scan.fields {
409            let field = &self.fields[*index];
410            // A condition hiding a field the codec reads nothing of there needs no blank answer.
411            let hides_unparsed = chain.iter().any(|condition| !condition.parses_when_hidden);
412            if hides_unparsed && field.is_required() {
413                errors.push(DeclarationErrorKind::RequiredConditionalField {
414                    field: field.name().to_string(),
415                });
416            }
417        }
418        errors
419    }
420}
421
422#[cfg(test)]
423mod tests;