054-dynamic-form.rst (15873B)
1 .. _dd54dynamicforms: 2 3 DD 54: Dynamic Forms 4 #################### 5 6 :Design status: Accepted 7 :Implementation status: Implemented 8 :DD shepherd: TBD 9 :Historical contributors: Sebastian, Florian Dold, Christian Grothoff 10 :First published: 2024-01-02 11 :Last substantive change: 2024-08-08 12 :Implementation evidence: taler-typescript-core (2023-12-29) 13 :Normative references: :doc:`../developer/taler-developer-manual` 14 15 .. note:: 16 17 The shared ``web-util`` types and codecs are the source of truth where they 18 differ from the illustrative types in this document. 19 20 Summary 21 ======= 22 23 This document outlines the design of forms defined in the 24 backend in a JSON file which will be rendered by a client 25 for asking information to a person. 26 27 28 Motivation 29 ========== 30 31 Currently, creating a new form for a web app involves coding a new 32 page with HTML, CSS, and JS. Exchange AML requires multiple forms, 33 and different instances may have distinct forms based on jurisdiction. 34 Being able to define forms in a JSON file that a client software 35 (not just web SPA) could use to ask the information helps to change 36 it without requiring a new upgrade of the client app. 37 38 39 Requirements 40 ============ 41 42 A form consist of a layout, a set of fields and metadata required to 43 recognice which form configuration was used to produce the saved value. 44 45 Layout requirements 46 ------------------- 47 48 * **editable by system admin**: System admins should be able to create new forms 49 or edit current one shipped with the source. 50 51 * **accesibility**: Forms should meet accessibility level AA. 52 53 * **responsive**: Forms should be responsive and function on all devices. 54 55 * **metadata**: Generated form information should contain enough data 56 to handle multiple form versions. 57 58 Fields requirements 59 ------------------- 60 61 * **validations**: Each field may require custom validation 62 63 * **custom data type**: A field may consist of a list, string, number, or a 64 complex composite structure. 65 66 67 Metadata requirements 68 --------------------- 69 70 * **identification**: the form configuration instance should have a unique 71 non reusable id. 72 73 * **label**: the form should have a name recognizable for the user 74 75 * **version**: the same form, with the same id, could be updated. This will 76 increase the version number. A newer form should support older forms. 77 78 Proposed Solutions 79 ================== 80 81 The propose solution defines the structure of a form, the fields and additional 82 type form-configuration which links a form with a set of fields. 83 84 Form metadata 85 ------------- 86 87 This is the root object of the configuration. 88 89 .. code-block:: typescript 90 91 type FormMetadata = { 92 label: string; 93 description?: string; 94 id: string; 95 version: number; 96 config: FormConfiguration; 97 }; 98 99 100 Form configuration 101 ------------------ 102 103 Defies a basic structure and the set of fields the form is going to have. 104 105 The ``FormConfiguration`` is an enumerated type which list can be extended in the 106 future. 107 108 .. code-block:: typescript 109 110 type FormConfiguration = DoubleColumnForm; 111 112 type DoubleColumnForm = { 113 type: "double-column"; 114 design: DoubleColumnFormSection[]; 115 } 116 117 type DoubleColumnFormSection = { 118 title: string; 119 description?: string; 120 fields: UIFormElementConfig[]; 121 }; 122 123 124 Form fields 125 ----------- 126 127 A form can have two type of element: decorative/informative or input. 128 129 An example of a decorative element is a grouping element which make all the fields 130 inside the group look into the same section. This element will not allow the user 131 to enter information and won't produce any value in the resulting JSON. 132 133 An example of an input field is a text field which allows the user to enter text. 134 This element should have an ``id`` which will point into the location in which the 135 value will be stored in the resulting JSON. Note that two field in the same form 136 with the same ``id`` will result in undefined behavior. 137 138 The ``UIFormElementConfig`` is an enumerated type with all type of fields supported 139 140 .. code-block:: typescript 141 142 type UIFormElementConfig = 143 | UIFormElementGroup 144 | UIFormElementCaption 145 | UIFormFieldAbsoluteTime 146 | UIFormFieldAmount 147 | UIFormFieldArray 148 | UIFormFieldChoiseHorizontal 149 | UIFormFieldChoiseStacked 150 | UIFormFieldFile 151 | UIFormFieldInteger 152 | UIFormFieldSelectMultiple 153 | UIFormFieldSelectOne 154 | UIFormFieldText 155 | UIFormFieldTextArea 156 | UIFormFieldToggle; 157 158 159 All form elements should extend from ``UIFieldElementDescription`` which defines a base 160 configuration to show a field. 161 162 .. code-block:: typescript 163 164 type UIFieldElementDescription = { 165 /* label if the field, visible for the user */ 166 label: string; 167 168 /* long text to be shown on user demand */ 169 tooltip?: string; 170 171 /* short text to be shown close to the field, usually below and dimmer*/ 172 help?: string; 173 174 /* name of the field, useful for a11y */ 175 name: string; 176 177 /* if the field should be initially hidden */ 178 hidden?: boolean; 179 180 }; 181 182 That will be enough for a decorative form element (like group element or 183 a text element) but if it defines an input field then it should extend 184 from ``UIFormFieldBaseConfig`` which add more information to the previously 185 defined ``UIFieldElementDescription``. 186 187 188 .. code-block:: typescript 189 190 type UIFormFieldBaseConfig = UIFieldElementDescription & { 191 /* example to be shown inside the field */ 192 placeholder?: string; 193 194 /* show a mark as required */ 195 required?: boolean; 196 197 /* readonly and dim */ 198 disabled?: boolean; 199 200 /* conversion id to convert the string into the value type 201 the id should be known to the ui impl 202 */ 203 converterId?: string; 204 205 /* property id of the form */ 206 id: UIHandlerId; 207 }; 208 209 /** 210 * string which defined a json path 211 * 212 */ 213 type UIHandlerId = string 214 215 216 The ``id`` property defines the location in which this information is going 217 to be saved in the JSON result. Formally formally, it should be a ``dot-selector`` 218 219 .. code-block:: ini 220 221 dot-selector = "." dot-member-name 222 dot-member-name = name-first *name-char 223 name-first = ALPHA / "_" 224 name-char = DIGIT / name-first 225 226 DIGIT = %x30-39 ; 0-9 227 ALPHA = %x41-5A / %x61-7A ; A-Z / a-z 228 229 230 All the input fields will create a string value located where the id 231 points, unless a ``convertedId`` is specified. The ``convertedId`` is a reference 232 to a converter that the client software implements. For example, an input field 233 with ``convertedId = "Taler.Amount"`` will transform the value the user 234 entered into a *AmountString* with the currency in the configuration. 235 236 237 Description of supported fields 238 ------------------------------- 239 240 All of this fields defines an UI handler which help the user to input 241 the value with as handy as possible. The type of the field doesn't define 242 the type of the value in the resulting JSON, that's defined by the ``converterId``. 243 244 Decorative elements 245 ``````````````````` 246 247 To show some additional text 248 249 .. code-block:: typescript 250 251 type UIFormElementCaption = { type: "caption" } & UIFieldElementDescription; 252 253 To group fields in the UI and maybe show a collapsable handler. 254 255 .. code-block:: typescript 256 257 type UIFormElementGroup = { 258 type: "group"; 259 fields: UIFormElementConfig[]; 260 } & UIFieldElementDescription; 261 262 Example 263 ''''''' 264 265 .. code-block:: json 266 267 { 268 "label": "Example form", 269 "id": "example", 270 "version": 1, 271 "config": { 272 "type": "double-column", 273 "design": [ 274 { 275 "title": "Decorative elements", 276 "fields": [ 277 { 278 "type": "caption", 279 "name": "cap", 280 "label": "This is a caption" 281 }, 282 { 283 "type": "group", 284 "name": "group", 285 "label": "The first name and last name are in a group", 286 "fields": [ 287 { 288 "type": "text", 289 "name": "firstName", 290 "id": ".person.name", 291 "label": "First name" 292 }, 293 { 294 "type": "text", 295 "name": "lastName", 296 "id": ".person.lastName", 297 "label": "Last name" 298 } 299 ] 300 } 301 ] 302 } 303 ] 304 } 305 } 306 307 .. image:: ../screenshots/dynamic-forms.decorative-elements.png 308 :width: 400 309 310 Time input 311 `````````` 312 313 This may be rendered as a calendar 314 315 .. code-block:: typescript 316 317 type UIFormFieldAbsoluteTime = { 318 type: "absoluteTimeText"; 319 max?: TalerProtocolTimestamp; 320 min?: TalerProtocolTimestamp; 321 pattern: string; 322 } & UIFormFieldBaseConfig; 323 324 .. code-block:: json 325 326 { 327 "label": "Example form", 328 "id": "example", 329 "version": 1, 330 "config": { 331 "type": "double-column", 332 "design": [ 333 { 334 "title": "Time inputs", 335 "fields": [ 336 { 337 "type": "absoluteTime", 338 "name": "thedate", 339 "id": ".birthdate", 340 "converterId": "Taler.AbsoluteTime", 341 "help": "the day you born", 342 "pattern":"dd/MM/yyyy", 343 "label": "Birthdate" 344 } 345 ] 346 } 347 ] 348 } 349 } 350 351 .. image:: ../screenshots/dynamic-forms.time.png 352 :width: 400 353 354 355 Amount input 356 ```````````` 357 358 Money input. 359 360 .. code-block:: typescript 361 362 type UIFormFieldAmount = { 363 type: "amount"; 364 max?: Integer; 365 min?: Integer; 366 currency: string; 367 } & UIFormFieldBaseConfig; 368 369 .. code-block:: json 370 371 { 372 "label": "Example form", 373 "id": "example", 374 "version": 1, 375 "config": { 376 "type": "double-column", 377 "design": [ 378 { 379 "title": "Amount inputs", 380 "fields": [ 381 { 382 "type": "amount", 383 "name": "thedate", 384 "id": ".amount", 385 "converterId": "Taler.Amount", 386 "help": "how much do you have?", 387 "currency":"EUR", 388 "label": "Amount" 389 } 390 ] 391 } 392 ] 393 } 394 } 395 396 .. image:: ../screenshots/dynamic-forms.amount.png 397 :width: 400 398 399 400 List input 401 `````````` 402 403 This input allows entering more than one element in the same field, and the 404 resulting JSON will have a json list. The UI should show the elements 405 already present in the list, and for that it will use ``labelFieldId``. 406 407 .. code-block:: typescript 408 409 type UIFormFieldArray = { 410 type: "array"; 411 // id of the field shown when the array is collapsed 412 labelFieldId: UIHandlerId; 413 fields: UIFormElementConfig[]; 414 } & UIFormFieldBaseConfig; 415 416 .. code-block:: json 417 418 { 419 "label": "Example form", 420 "id": "example", 421 "version": 1, 422 "config": { 423 "type": "double-column", 424 "design": [ 425 { 426 "title": "Amount inputs", 427 "fields": [ 428 { 429 "type": "array", 430 "name": "people", 431 "id": ".people", 432 "help": "who is coming to the party?", 433 "labelFieldId": ".name", 434 "fields": [{ 435 "type": "text", 436 "name": "firstName", 437 "id": ".name", 438 "label": "First name" 439 }, 440 { 441 "type": "text", 442 "name": "lastName", 443 "id": ".lastName", 444 "label": "Last name" 445 }], 446 } 447 ] 448 } 449 ] 450 } 451 } 452 453 454 .. image:: ../screenshots/dynamic-forms.list.png 455 :width: 400 456 457 Choice input 458 ```````````` 459 460 To be used when the user need to choose on predefined values 461 462 .. code-block:: typescript 463 464 interface SelectUiChoice { 465 label: string; 466 description?: string; 467 value: string; 468 } 469 470 A set of buttons next to each other 471 472 .. code-block:: typescript 473 474 type UIFormFieldChoiseHorizontal = { 475 type: "choiceHorizontal"; 476 choices: Array<SelectUiChoice>; 477 } & UIFormFieldBaseConfig; 478 479 480 A set of buttons next on top of each other 481 482 .. code-block:: typescript 483 484 type UIFormFieldChoiseStacked = { 485 type: "choiceStacked"; 486 choices: Array<SelectUiChoice>; 487 } & UIFormFieldBaseConfig; 488 489 A drop down list to select one of the elements 490 491 .. code-block:: typescript 492 493 type UIFormFieldSelectOne = { 494 type: "selectOne"; 495 choices: Array<SelectUiChoice>; 496 } & UIFormFieldBaseConfig; 497 498 A drop down list to select multiple of the element, which 499 will produce a list of values in the resulting JSON. 500 501 .. code-block:: typescript 502 503 type UIFormFieldSelectMultiple = { 504 type: "selectMultiple"; 505 max?: Integer; 506 min?: Integer; 507 unique?: boolean; 508 choices: Array<SelectUiChoice>; 509 } & UIFormFieldBaseConfig; 510 511 512 .. code-block:: json 513 514 { 515 "label": "Example form", 516 "id": "example", 517 "version": 1, 518 "config": { 519 "type": "double-column", 520 "design": [ 521 { 522 "title": "Choice inputs", 523 "fields": [ 524 { 525 "type": "choiceHorizontal", 526 "name": "food", 527 "label": "Food", 528 "id": ".food", 529 "choices": [ 530 { 531 "value": "meat", 532 "label": "Meat" 533 }, 534 { 535 "value": "sushi", 536 "label": "Sushi" 537 }, 538 { 539 "value": "taco", 540 "label": "Taco" 541 }, 542 { 543 "value": "salad", 544 "label": "Salad" 545 } 546 ] 547 } 548 ] 549 } 550 ] 551 } 552 } 553 554 .. image:: ../screenshots/dynamic-forms.choice.png 555 :width: 400 556 557 File input 558 `````````` 559 560 .. code-block:: typescript 561 562 type UIFormFieldFile = { 563 type: "file"; 564 maxBytes?: Integer; 565 minBytes?: Integer; 566 // comma-separated list of one or more file types 567 // https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept#unique_file_type_specifiers 568 accept?: string; 569 } & UIFormFieldBaseConfig; 570 571 572 .. code-block:: json 573 574 { 575 "label": "Example form", 576 "id": "example", 577 "version": 1, 578 "config": { 579 "type": "double-column", 580 "design": [ 581 { 582 "title": "File inputs", 583 "fields": [ 584 { 585 "type": "file", 586 "name": "photo", 587 "id": ".photo", 588 "label": "Photo", 589 "accept": "*.png" 590 } 591 ] 592 } 593 ] 594 } 595 } 596 597 .. image:: ../screenshots/dynamic-forms.file.png 598 :width: 400 599 600 Number input 601 ```````````` 602 603 .. code-block:: typescript 604 605 type UIFormFieldInteger = { 606 type: "integer"; 607 max?: Integer; 608 min?: Integer; 609 } & UIFormFieldBaseConfig; 610 611 612 Text input 613 `````````` 614 615 A simple line of text 616 617 .. code-block:: typescript 618 619 type UIFormFieldText = { type: "text" } & UIFormFieldBaseConfig; 620 621 A bigger multi-line of text 622 623 .. code-block:: typescript 624 625 type UIFormFieldTextArea = { type: "textArea" } & UIFormFieldBaseConfig; 626 627 628 Boolean input 629 ````````````` 630 631 .. code-block:: typescript 632 633 type UIFormFieldToggle = { type: "toggle" } & UIFormFieldBaseConfig; 634 635 636 .. code-block:: json 637 638 { 639 "label": "Example form", 640 "id": "example", 641 "version": 1, 642 "config": { 643 "type": "double-column", 644 "design": [ 645 { 646 "title": "Boolean inputs", 647 "fields": [ 648 { 649 "type": "toggle", 650 "name": "the_question", 651 "id": ".the_question", 652 "label": "Yes or no?" 653 } 654 ] 655 } 656 ] 657 } 658 } 659 660 .. image:: ../screenshots/dynamic-forms.boolean.png 661 :width: 400 662 663 Examples 664 ======== 665 666 667 668 Q / A 669 =====