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