max / goingson
| 1 | //! Form markup rendering. |
| 2 | //! |
| 3 | //! One command. Escaping is `form::escape`, which encodes all five characters |
| 4 | //! and is sound in element text and in a double-quoted attribute alike, so no |
| 5 | //! call site picks an escaper. |
| 6 | //! |
| 7 | //! # Why a batch |
| 8 | //! |
| 9 | //! A form's fields cross once, together, and the caller reads the results out |
| 10 | //! of a map by name. A command per field would make every call site that |
| 11 | //! assembles a form async. |
| 12 | |
| 13 | use HashMap; |
| 14 | |
| 15 | use ; |
| 16 | use Emit; |
| 17 | use ; |
| 18 | use Deserialize; |
| 19 | use ; |
| 20 | |
| 21 | /// One option of a select, as the frontend sends it. |
| 22 | |
| 23 | |
| 24 | /// What is submitted. |
| 25 | value: String, |
| 26 | /// What is read. Defaults to the value, matching `Choice::plain`. |
| 27 | #[serde(default)] |
| 28 | label: , |
| 29 | /// Whether this option is the current one. |
| 30 | /// |
| 31 | /// Redundant with the field's `value` and kept because callers say it both |
| 32 | /// ways: many compute `selected` from a comparison without also passing |
| 33 | /// `value`, and dropping it would silently unselect those. See |
| 34 | /// [`FieldSpec::current_value`]. |
| 35 | #[serde(default)] |
| 36 | selected: bool, |
| 37 | |
| 38 | |
| 39 | /// One field, as the frontend sends it. |
| 40 | /// |
| 41 | /// A mirror of [`Field`] plus the one thing the description deliberately does |
| 42 | /// not carry, the current value, which arrives in [`Filling`]. |
| 43 | /// |
| 44 | /// This type is the wire boundary: a description reorganising itself is not a |
| 45 | /// reason to make every caller send something new, so the JSON shape stays put |
| 46 | /// even when the fields land in a different struct. |
| 47 | |
| 48 | |
| 49 | |
| 50 | /// `text`, `password`, `number`, `email`, `url`, `tel`, `textarea`, |
| 51 | /// `select`, `checkbox` or `hidden`. Unknown kinds render as text. |
| 52 | kind: String, |
| 53 | /// The name the value submits under. |
| 54 | name: String, |
| 55 | /// What the user is asked for. |
| 56 | #[serde(default)] |
| 57 | label: String, |
| 58 | /// Standing help. |
| 59 | #[serde(default)] |
| 60 | hint: , |
| 61 | /// What is currently wrong. |
| 62 | #[serde(default)] |
| 63 | error: , |
| 64 | /// Whether the form refuses to submit without it. |
| 65 | #[serde(default)] |
| 66 | required: bool, |
| 67 | /// Whether it lives behind a "more options" disclosure. |
| 68 | #[serde(default)] |
| 69 | extended: bool, |
| 70 | /// The current value, for everything that takes typed text. |
| 71 | #[serde(default)] |
| 72 | value: , |
| 73 | /// The current state, for a checkbox. |
| 74 | #[serde(default)] |
| 75 | checked: , |
| 76 | /// The options, for a select. |
| 77 | #[serde(default)] |
| 78 | options: , |
| 79 | /// Ghost text shown while empty. |
| 80 | #[serde(default)] |
| 81 | placeholder: , |
| 82 | /// Markup appended after the hint. NOT escaped. |
| 83 | /// |
| 84 | /// The caller is stating the contents are trusted, which is what |
| 85 | /// [`Markup`] exists to make them say out loud. Two live callers need it, |
| 86 | /// both passing a recurrence block built elsewhere. |
| 87 | #[serde(default)] |
| 88 | trailing_html: , |
| 89 | |
| 90 | |
| 91 | |
| 92 | /// The value a select should show as current. |
| 93 | /// |
| 94 | /// `value` when the caller sent one, otherwise the value of whichever |
| 95 | /// option marked itself selected. The description matches an option by |
| 96 | /// value alone, which is the better contract, so the two ways the frontend |
| 97 | /// says this are reconciled here rather than in the crate. |
| 98 | |
| 99 | if let Some = self.value.as_deref |
| 100 | && !value.is_empty |
| 101 | |
| 102 | return value; |
| 103 | |
| 104 | self.options |
| 105 | .as_deref |
| 106 | .unwrap_or_default |
| 107 | .iter |
| 108 | .find |
| 109 | .map_or |
| 110 | |
| 111 | |
| 112 | |
| 113 | /// Map the frontend's kind string onto the description. |
| 114 | /// |
| 115 | /// Unknown kinds fall back to text rather than failing the render. A form that |
| 116 | /// draws with one field slightly wrong is recoverable; a form that does not |
| 117 | /// draw is not, and the warning says which happened. |
| 118 | |
| 119 | match spec_kind |
| 120 | "password" => Secret, |
| 121 | "number" => Number, |
| 122 | "email" => Email, |
| 123 | "url" => Url, |
| 124 | "tel" => Tel, |
| 125 | "textarea" => Textarea, |
| 126 | "select" => Select, |
| 127 | "checkbox" => Checkbox, |
| 128 | "hidden" => Hidden, |
| 129 | "text" => Text, |
| 130 | other => |
| 131 | warn!; |
| 132 | Text |
| 133 | |
| 134 | |
| 135 | |
| 136 | |
| 137 | /// Render a form's fields to markup, keyed by field name. |
| 138 | /// |
| 139 | /// `id_prefix` scopes the `id` attributes to one instance of the form, which |
| 140 | /// matters because the new-entity and edit-entity modals are the same field set |
| 141 | /// and their ids would otherwise collide. It never reaches `name`. |
| 142 | /// |
| 143 | /// Infallible by construction: every field renders to something, and an |
| 144 | /// unrecognised kind renders as text with a warning rather than taking the form |
| 145 | /// down with it. |
| 146 | |
| 147 | |
| 148 | pub async |
| 149 | fields: , |
| 150 | id_prefix: , |
| 151 | |
| 152 | let opts = default; |
| 153 | let mut out = with_capacity; |
| 154 | |
| 155 | for spec in &fields |
| 156 | let kind = kind_of; |
| 157 | |
| 158 | // Built here rather than inline so the borrow outlives the Field that |
| 159 | // points into it. |
| 160 | let choices: = spec |
| 161 | .options |
| 162 | .as_deref |
| 163 | .unwrap_or_default |
| 164 | .iter |
| 165 | .map |
| 166 | .collect; |
| 167 | |
| 168 | let field = Field |
| 169 | kind, |
| 170 | name: &spec.name, |
| 171 | label: &spec.label, |
| 172 | hint: spec.hint.as_deref, |
| 173 | error: spec.error.as_deref, |
| 174 | // makeover-layout 0.36.0's note, on the same footing as the |
| 175 | // constraints below: the JS form spec carries no consequence |
| 176 | // channel, so this says nothing rather than promoting a hint into |
| 177 | // one and guessing at its tone. |
| 178 | note: None, |
| 179 | // Both moved onto the description in makeover-layout 0.8.0. They |
| 180 | // arrived in `Filling` until then, which is why this function used |
| 181 | // to assemble the field and its filling from the same spec in two |
| 182 | // places. |
| 183 | placeholder: spec.placeholder.as_deref, |
| 184 | options: &choices, |
| 185 | // makeover-layout 0.38.0's theme picker, and the same position as |
| 186 | // the file members below: this JS form spec cannot name a theme |
| 187 | // kind, so it never carries themes. The described settings screen |
| 188 | // is where the picker lives (`quasi::settings`), and it builds one |
| 189 | // out of what the host resolved rather than out of a form spec. |
| 190 | themes: &, |
| 191 | follows: None, |
| 192 | // makeover-layout 0.31.0's file members, and the same position: |
| 193 | // the JS form spec carries no accept list and no multiplicity, so |
| 194 | // an empty list is what this app actually says. It means any file, |
| 195 | // which is what its one file field already was. |
| 196 | accept: &, |
| 197 | multiple: false, |
| 198 | required: spec.required, |
| 199 | // makeover-layout 0.11.0's constraints. This form spec carries none |
| 200 | // of them yet: the runtime form model is the JS one, and adding |
| 201 | // them here without the spec growing fields would be inventing |
| 202 | // limits the app never stated. |
| 203 | max_length: None, |
| 204 | min: None, |
| 205 | max: None, |
| 206 | step: None, |
| 207 | // makeover-layout 0.32.0's curve, on the same footing as the |
| 208 | // constraints above: this spec describes no ranges, so the default |
| 209 | // linear track with the host's own granularity is what the app |
| 210 | // actually says rather than a mapping invented here. |
| 211 | curve: default, |
| 212 | // makeover-layout 0.33.0's unit, on the same footing again: the JS |
| 213 | // form spec has no field for what a number is measured in, so this |
| 214 | // says nothing rather than parsing one out of the label. |
| 215 | unit: None, |
| 216 | // makeover-layout 0.34.0's second end, on the same footing again: |
| 217 | // this spec describes one value per field, so an interval is not |
| 218 | // something the JS form model can say and naming an upper end here |
| 219 | // would be inventing a question the app never asked. |
| 220 | upper_name: None, |
| 221 | extended: spec.extended, |
| 222 | // makeover-layout 0.37.0's instant flag, on the same footing again: |
| 223 | // the JS form spec has no way to say a datetime is submitted as the |
| 224 | // moment it names, and this runtime form posts to Tauri commands |
| 225 | // rather than to a route storing an instant. Saying it here would |
| 226 | // convert a value nothing asked to have converted. |
| 227 | as_instant: false, |
| 228 | ; |
| 229 | |
| 230 | let value = match kind |
| 231 | Checkbox => On, |
| 232 | // A select's value is the value of one of its options and the |
| 233 | // options are on the field now, so it takes the same variant |
| 234 | // everything else typed does. |
| 235 | Select => Text, |
| 236 | _ => match spec.value.as_deref |
| 237 | Some => Text, |
| 238 | None => Absent, |
| 239 | , |
| 240 | ; |
| 241 | |
| 242 | let filling = Filling |
| 243 | value, |
| 244 | trailing: spec.trailing_html.as_deref.map, |
| 245 | // Nothing of this app's own goes on the control. The seam exists |
| 246 | // for a host that knows a fact the description does not carry, and |
| 247 | // quasi's suggestion source is the one caller there is. |
| 248 | control_attrs: None, |
| 249 | id_prefix: id_prefix.as_deref, |
| 250 | ; |
| 251 | |
| 252 | out.insert; |
| 253 | |
| 254 | |
| 255 | out |
| 256 | |
| 257 |