12.54. DD 54: Dynamic Web Form

12.54.1. Summary

This document outlines the approach for implementing a dynamic web form feature.

12.54.2. 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.

12.54.3. Requirements

A form consist of a layout and a set of fields.

12.54.3.1. 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.

12.54.3.2. 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.

12.54.4. Proposed Solutions

Forms are initialized using a flexible structure defined by the TypeScript interface FormType<T>. This interface comprises properties such as value (current form data), initial (initial form data for resetting), readOnly (flag to disable input), onUpdate (callback on form data update), and computeFormState (function to derive the form state based on current data).

interface FormType<T extends object> {
  value: Partial<T>;
  initial?: Partial<T>;
  readOnly?: boolean;
  onUpdate?: (v: Partial<T>) => void;
  computeFormState?: (v: Partial<T>) => FormState<T>;
}

T: is the type of the result object value: is a reference to the current value of the result initial: data for resetting readOnly: when true, fields won’t allow input onUpdate: notification of the result update computeFormState: compute a new state of the form based on the current value

Form state have the same shape of T but every field type is FieldUIOptions.

Fields type can be:
  • strings

  • numbers

  • boolean

  • arrays

  • object

The field type AmountJson and AbsoluteTime are opaque since field is used as a whole.

The form can be instanciated using

import { FormProvider } from "@gnu-taler/web-util/browser";

Then the field component can access all the properties by the useField(name) hook, which will return

interface InputFieldHandler<Type> {
  value: Type;
  onChange: (s: Type) => void;
  state: FieldUIOptions;
  isDirty: boolean;
}

value: the current value of the field onChange: a function to call anytime the user want to change the value state: the state of the field (hidden, error, etc..) isDirty: if the user already tried to change the value

A set of common form field exist in @gnu-taler/web-util:

  • InputAbsoluteTime

  • InputAmount

  • InputArray

  • InputFile

  • InputText

  • InputToggle

and should be used inside a Form context.

function MyFormComponent():VNode {
  return <FormProvider>
    <InputAmount name="amount"  />
    <InputText   name="subject" />
    <button type="submit"> Confirm </button>
  </FormProvider>
}

12.54.4.1. Example

Consider a form shape represented by the TypeScript type:

type TheFormType = {
  name: string,
  age: number,
  savings: AmountJson,
  nextBirthday: AbsoluteTime,
  pets: string[],
  addres: {
    street: string,
    city: string,
  }
}

An example instance of this form could be:

const theFormValue: TheFormType = {
  name: "Sebastian",
  age: 15,
  pets: ["dog","cat"],
  address: {
    street: "long",
    city: "big",
  }
}

For such a form, a valid state can be computed using a function like computeFormStateBasedOnFormValues, returning an object indicating the state of each field, including properties such as hidden, disabled, and required.

function computeFormStateBasedOnFormValues(formValues): {
  //returning fixed state as an example
  //the return state will be commonly be computed from the values of the form
  return {
    age: {
      hidden: true,
    },
    pets: {
      disabled: true,
      elements: [{
        disabled: false,
      }],
    },
    address: {
      street: {
        required: true,
        error: "the street name was not found",
      },
      city: {
        required: true,
      },
    },
  }
}

12.54.5. Q / A