taler-docs

Documentation for GNU Taler components, APIs and protocols
Log | Files | Refs | README | LICENSE

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 =====