diff options
Diffstat (limited to 'design-documents/054-dynamic-form.rst')
-rw-r--r-- | design-documents/054-dynamic-form.rst | 527 |
1 files changed, 527 insertions, 0 deletions
diff --git a/design-documents/054-dynamic-form.rst b/design-documents/054-dynamic-form.rst new file mode 100644 index 00000000..caae86b8 --- /dev/null +++ b/design-documents/054-dynamic-form.rst @@ -0,0 +1,527 @@ +DD 54: Dynamic Form +####################### + +Summary +======= + +This document outlines the design of forms defined in the +backend in a JSON file which will be rendered by a client +for asking information to a person. + + +Motivation +========== + +Currently, creating a new form for a web app involves coding a new +page with HTML, CSS, and JS. Exchange AML requires multiple forms, +and different instances may have distinct forms based on jurisdiction. +Being able to define forms in a JSON file that a client software +(not just web SPA) could use to ask the information helps to change +it without requiring a new upgrade of the client app. + + +Requirements +============ + +A form consist of a layout, a set of fields and metadata required to +recognice which form configuration was used to produce the saved value. + +Layout requirements +------------------- + +* **editable by system admin**: System admins should be able to create new forms + or edit current one shipped with the source. + +* **accesibility**: Forms should meet accessibility level AA. + +* **responsive**: Forms should be responsive and function on all devices. + +* **metadata**: Generated form information should contain enough data + to handle multiple form versions. + +Fields requirements +------------------- + +* **validations**: Each field may require custom validation + +* **custom data type**: A field may consist of a list, string, number, or a + complex composite structure. + + +Metadata requirements +--------------------- + +* **identification**: the form configuration instance should have an unique + non reusable id. + +* **label**: the form should have a name recognizable for the user + +* **version**: the same form, with the same id, could be updated. This will + increase the version number. A newer form should support older forms. + +Proposed Solutions +================== + +The propose solution defines the structure of a form, the fields and additional +type form-configuration which links a form with a set of fields. + +Form metadata +------------- + +This is the root object of the configuration. + +.. code-block:: typescript + + type FormMetadata = { + label: string; + id: string; + version: number; + config: FormConfiguration; + }; + + +Form configuration +------------- + +Defies a basic structure and the set of fields the form is going to have. + +The ``FormConfiguration`` is an enumerated type which list can be extended in the +future. + +.. code-block:: typescript + + type FormConfiguration = DoubleColumnForm; + + type DoubleColumnForm = { + type: "double-column"; + design: DoubleColumnFormSection[]; + } + + type DoubleColumnFormSection = { + title: string; + description?: string; + fields: UIFormElementConfig[]; + }; + + +Form fields +----------- + +A form can have two type of element: decorative/informative or input. + +An example of a decorative element is a grouping element which make all the fields +inside the group look into the same section. This element will not allow the user +to enter information and won't produce any value in the resulting JSON. + +An example of an input field is a text field which allows the user to enter text. +This element should have an ``id`` which will point into the location in which the +value will be stored in the resulting JSON. Note that two field in the same form +with the same ``id`` will result in undefined behavior. + +The ``UIFormElementConfig`` is an enumerated type with all type of fields supported + +.. code-block:: typescript + + type UIFormElementConfig = + | UIFormElementGroup + | UIFormElementCaption + | UIFormFieldAbsoluteTime + | UIFormFieldAmount + | UIFormFieldArray + | UIFormFieldChoiseHorizontal + | UIFormFieldChoiseStacked + | UIFormFieldFile + | UIFormFieldInteger + | UIFormFieldSelectMultiple + | UIFormFieldSelectOne + | UIFormFieldText + | UIFormFieldTextArea + | UIFormFieldToggle; + + +All form elements should extend from ``UIFieldElementDescription`` which defines a base +configuration to show a field. + +.. code-block:: typescript + + type UIFieldElementDescription = { + /* label if the field, visible for the user */ + label: string; + + /* long text to be shown on user demand */ + tooltip?: string; + + /* short text to be shown close to the field, usually below and dimmer*/ + help?: string; + + /* name of the field, useful for a11y */ + name: string; + + /* if the field should be initially hidden */ + hidden?: boolean; + + }; + +That will be enough for a decorative form element (like group element or +a text element) but if it defines an input field then it should extend +from ``UIFormFieldBaseConfig`` which add more information to the previously +defined ``UIFieldElementDescription``. + + +.. code-block:: typescript + + type UIFormFieldBaseConfig = UIFieldElementDescription & { + /* example to be shown inside the field */ + placeholder?: string; + + /* show a mark as required */ + required?: boolean; + + /* readonly and dim */ + disabled?: boolean; + + /* conversion id to convert the string into the value type + the id should be known to the ui impl + */ + converterId?: string; + + /* property id of the form */ + id: UIHandlerId; + }; + + /** + * string which defined a json path + * + */ + type UIHandlerId = string + + +The ``id`` property defines the location in which this information is going +to be saved in the JSON result. Formally formally, it should be a ``dot-selector`` + +.. code-block:: ini + + dot-selector = "." dot-member-name + dot-member-name = name-first *name-char + name-first = ALPHA / "_" + name-char = DIGIT / name-first + + DIGIT = %x30-39 ; 0-9 + ALPHA = %x41-5A / %x61-7A ; A-Z / a-z + + +All the input fields will create a string value located where the id +points, unless a ``convertedId`` is specified. The ``convertedId`` is a reference +to a converter that the client software implements. For example, an input field +with ``convertedId = "Taler.Amount"`` will transform the value the user +entered into a *AmountString* with the currency in the configuration. + + +Description of supported fields +------------------------------- + +All of this fields defines an UI handler which help the user to input +the value with as handy as possible. The type of the field doesn't define +the type of the value in the resulting JSON, that's defined by the ``converterId``. + +Decorative elements +`````````````````` + +To show some additional text + +.. code-block:: typescript + + type UIFormElementCaption = { type: "caption" } & UIFieldElementDescription; + +To group fields in the UI and maybe show a collapsable handler. + +.. code-block:: typescript + + type UIFormElementGroup = { + type: "group"; + fields: UIFormElementConfig[]; + } & UIFieldElementDescription; + +Example +''''''' + +.. code-block:: json + + { + "label": "Example form", + "id": "example", + "version": 1, + "config": { + "type": "double-column", + "design": [ + { + "title": "Decorative elements", + "fields": [ + { + "type": "caption", + "name": "cap", + "label": "This is a caption" + }, + { + "type": "group", + "name": "group", + "label": "The first name and last name are in a group", + "fields": [ + { + "type": "text", + "name": "firstName", + "id": ".person.name", + "label": "First name" + }, + { + "type": "text", + "name": "lastName", + "id": ".person.lastName", + "label": "Last name" + } + ] + } + ] + } + ] + } + } + +.. image:: ../screenshots/dynamic-forms.decorative-elements.png + :width: 400 + +Time input +`````````` + +This may be rendered as a calendar + +.. code-block:: typescript + + type UIFormFieldAbsoluteTime = { + type: "absoluteTimeText"; + max?: TalerProtocolTimestamp; + min?: TalerProtocolTimestamp; + pattern: string; + } & UIFormFieldBaseConfig; + +.. code-block:: json + + { + "label": "Example form", + "id": "example", + "version": 1, + "config": { + "type": "double-column", + "design": [ + { + "title": "Time inputs", + "fields": [ + { + "type": "absoluteTime", + "name": "thedate", + "id": ".birthdate", + "converterId": "Taler.AbsoluteTime", + "help": "the day you born", + "pattern":"dd/MM/yyyy", + "label": "Birthdate" + } + ] + } + ] + } + } + +.. image:: ../screenshots/dynamic-forms.time.png + :width: 400 + + +Amount input +```````````` + +Money input. + +.. code-block:: typescript + + type UIFormFieldAmount = { + type: "amount"; + max?: Integer; + min?: Integer; + currency: string; + } & UIFormFieldBaseConfig; + +.. code-block:: json + + { + "label": "Example form", + "id": "example", + "version": 1, + "config": { + "type": "double-column", + "design": [ + { + "title": "Amount inputs", + "fields": [ + { + "type": "amount", + "name": "thedate", + "id": ".amount", + "converterId": "Taler.Amount", + "help": "how much do you have?", + "currency":"EUR", + "label": "Amount" + } + ] + } + ] + } + } + +.. image:: ../screenshots/dynamic-forms.amount.png + :width: 400 + + +List input +`````````` + +This input allows to enter more than element in the same field, and the +resulting JSON will have a json list. The UI should show the elements +already present in the list, and for that it will use ``labelFieldId``. + +.. code-block:: typescript + + type UIFormFieldArray = { + type: "array"; + // id of the field shown when the array is collapsed + labelFieldId: UIHandlerId; + fields: UIFormElementConfig[]; + } & UIFormFieldBaseConfig; + + +.. image:: ../screenshots/dynamic-forms.list.png + :width: 400 + +Choice input +```````````` + +To be used when the user need to choose on predefined values + +.. code-block:: typescript + + interface SelectUiChoice { + label: string; + description?: string; + value: string; + } + +A set of buttons next to each other + +.. code-block:: typescript + + type UIFormFieldChoiseHorizontal = { + type: "choiceHorizontal"; + choices: Array<SelectUiChoice>; + } & UIFormFieldBaseConfig; + + +A set of buttons next on top of each other + +.. code-block:: typescript + + type UIFormFieldChoiseStacked = { + type: "choiceStacked"; + choices: Array<SelectUiChoice>; + } & UIFormFieldBaseConfig; + +A drop down list to select one of the elements + +.. code-block:: typescript + + type UIFormFieldSelectOne = { + type: "selectOne"; + choices: Array<SelectUiChoice>; + } & UIFormFieldBaseConfig; + +A drop down list to select multiple of the element, which +will produce a list of values in the resulting JSON. + +.. code-block:: typescript + + type UIFormFieldSelectMultiple = { + type: "selectMultiple"; + max?: Integer; + min?: Integer; + unique?: boolean; + choices: Array<SelectUiChoice>; + } & UIFormFieldBaseConfig; + +.. image:: ../screenshots/dynamic-forms.choice.png + :width: 400 + +File input +`````````` + +.. code-block:: typescript + + type UIFormFieldFile = { + type: "file"; + maxBytes?: Integer; + minBytes?: Integer; + // comma-separated list of one or more file types + // https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept#unique_file_type_specifiers + accept?: string; + } & UIFormFieldBaseConfig; + +.. image:: ../screenshots/dynamic-forms.file.png + :width: 400 + +Number input +```````````` + +.. code-block:: typescript + + type UIFormFieldInteger = { + type: "integer"; + max?: Integer; + min?: Integer; + } & UIFormFieldBaseConfig; + + +.. image:: ../screenshots/dynamic-forms.number.png + :width: 400 + +Text input +```````````` + +A simple line of text + +.. code-block:: typescript + + type UIFormFieldText = { type: "text" } & UIFormFieldBaseConfig; + +A bigger multi-line of text + +.. code-block:: typescript + + type UIFormFieldTextArea = { type: "textArea" } & UIFormFieldBaseConfig; + + +.. image:: ../screenshots/dynamic-forms.text.png + :width: 400 + +Boolean input +````````````` + +.. code-block:: typescript + + type UIFormFieldToggle = { type: "toggle" } & UIFormFieldBaseConfig; + +.. image:: ../screenshots/dynamic-forms.boolean.png + :width: 400 + +Examples +======== + + + +Q / A +===== |