Skip to content

bps-statistics/form-gear

Repository files navigation

FormGear is a framework engine for dynamic form creation and complex form processing and validation for data collection. It is designed to work across multiple data collection platforms such as CAPI, CAWI, and PAPI.

About

FormGear is a framework engine for form creation, processing, and validation. FormGear was initially designed to support official statistics data collection in BPS - Statistics Indonesia, Indonesia's National Statistics Office, by a team under the direction of BPS. This requirement calls for dynamic form creation, complex processing and validation, and ease of use in the data collection process.

FormGear uses a defined JSON object template, thus is easy to build, use, and efficiently handle nested inquiries to capture everything down to the last detail. Unlike other similar framework, validation is handled in a FALSE condition in which each field is validated against a test equation. This leads to a more efficient and effective way of validating each component.

Features

  • Easily create complex form with various form controls.
  • Divide form into several sections for ease during data collection.
  • Create nested form inquiry to accommodate recurring fields.
  • Add conditions to enable form control.
  • Validate answer given during data collection with your own test function.
  • Add remark to record additional information.
  • Add preset to provide information acquired prior to data collection.

Usage

Table of Content

Online examples

Develop on JS Framework examples:

Installation

FormGear can be installed via package manager like npm or yarn, or it can be used directly via CDN.

npm install form-gear
import { FormGear } from 'form-gear'
import {  } from './node_modules/form-gear/dist/style.css'

const data = Promise.all([
                fetch("./data/template.json").then((res) => res.json()),
                fetch("./data/preset.json").then((res) => res.json()),
                fetch("./data/response.json").then((res) => res.json()),
                fetch("./data/validation.json").then((res) => res.json())
                fetch("./data/remark.json").then((res) => res.json())
            ]);

data.then(([template, preset, response, validation, remark]) => initForm(template, preset, response, validation, remark));

function initForm(template, preset, response, validation, remark){
        
   let config = {
      clientMode: 1, // CAWI = 1, CAPI = 2
      token: ``,
      baseUrl: ``,
      lookupKey: `key%5B%5D`,
      lookupValue: `value%5B%5D`,
      username: 'AdityaSetyadi',
      formMode: 1 // 1 => OPEN ; 2 => REJECTED ; 3 => SUBMITTED ; 4 => APPROVED ;
   }
   
   let uploadHandler = function (setter) {
      console.log('camera handler', setter);
      cameraFunction = setter;
      openCamera();
   }

   let GpsHandler = function (setter, isPhoto) {
      console.log('camera handler', setter);
      isPhoto = true,
         cameraGPSFunction = setter;
      openCameraGPS(isPhoto);
   }

   let onlineSearch = async (url) =>
      (await fetch(url, setBearer())
         .catch((error: any) => {
            return {
               success: false,
               data: {},
               message: '500'
            }
         }).then((res: any) => {
         if (res.status === 200) {
            let temp = res.json();
            return temp;
         } else {
            return {
               success: false,
               data: {},
               message: res.status
            }
         }
         }).then((res: any) => {
            return res;
         }
      ));

   let setResponseMobile = function (res, rem) {
      respons = res
      remarks = rem

      console.log('respons', respons)
      console.log('remarks', remarks)
   }

   let setSubmitMobile = function (res, rem) {
      respons = res
      remarks = rem

      console.log('respons submit', respons)
      console.log('remarks submit', remarks)
   }

   let openMap = function (koordinat) {
      koordinat = koordinat

      console.log('coordinat ', koordinat)
   }

   let form = FormGear(template, preset, response, validation, remark, config, uploadHandler, GpsHandler, onlineSearch, setResponseMobile, setSubmitMobile, openMap);
   
   return form;
   
}

Template

FormGear use defined template which is based on JSON Object. This allows for dynamic form creation, letting you to easily build your form with various form controls simply by expanding the JSON object. This also allows FormGear to create nest other form control in other form control, making it easy for you to create recurring questions based on the nested form control. Below is the structure and example of the JSON object that FormGear uses as its form template.

template.json
│   description
│   dataKey
│   title
│   acronym
│   ...
└───components
│   │   section
│   └───components
│       │   textInput
│       │   checkboxInput
│       │   selectInput
│       │   ...
└───
│   │   section
│   └───components
│       │   textInput
│       │   checkboxInput
│       │   nestedInput
│       └───
│           │   sourceQuestion
│           └───components
│               │   textInput
│               │   checkboxInput
│               │   nestedInput
│               └───
│                   │   sourceQuestion
│                   └───components
│                       │   textInput
│                       │   dateInput
│                       │   ...
│               │   ...
│       │
│       │   dateInput
│       │   ...
└───
│   │   section
│   └───components
│       │   textInput
│       │   checkboxInput
│       │   selectInput
│       │   ...
│

[
    {
        "label":"Hobbies",
        "dataKey":"hobbies",
        "type":29,
        "cols":3,
        "options":[
            {
                "label":"Sleeping",
                "value":"1"
            },
            {
                "label":"Play Games",
                "value":"2"
            },
            {
                "label":"Watching Movie",
                "value":"3"
            },
            {
                "label":"Cooking",
                "value":"4"
            },
            {
                "label":"Working",
                "value":"5"
            },
            {
                "label":"Travelling",
                "value":"6"
            },
            {
                "label":"Other",
                "value":"13",
                "open":true
            }
        ]
    },
    {
        "label": "Province",
        "dataKey": "l2_r322_prov_1",
        "typeOption" : 2,
        "type": 27,
        "enableCondition":"Number(getValue('l2_land_location@$ROW$')) > 0",
        "componentEnable":["l2_land_location@$ROW$"],
        "sourceSelect":[{
            "id": "f796a32a-36df-4554-bb79-bf8065e28c52",
            "tableName": "kode_kab_ppkk",
            "value": "kode_prov",
            "desc": "nama_prov",
            "parentCondition": [
            
            ]  
        }]
    },
    {
        "label":"Healthy neighborhood rating",
        "dataKey":"rating",
        "type":26,
        "cols":5,
        "options":[
            {
                "label":"",
                "value":"1"
            },
            {
                "label":"",
                "value":"2"
            },
            {
                "label":"",
                "value":"3"
            },
            {
                "label":"",
                "value":"4"
            },
            {
                "label":"",
                "value":"5"
            }
        ]
    },
    {
        "label":"Happiness Index",
        "dataKey":"happy",
        "type":18,
        "range":[
        {
            "min":0,
            "max":100,
            "step":5
        }
        ]
    },
    {
        "label":"Chld nested",
        "dataKey":"childnested",
        "description":"ChildNested",
        "type":2,
        "sourceQuestion":"hobbies",
        "components":[
            [
                {
                    "label":"name--- $NAME$",
                    "dataKey":"name_",
                    "type":4,
                    "expression":"let nm = ''; let list = getValue('hobbies'); if(list !== undefined && list.length > 0) { let rowIndex = getRowIndex(0); let filter = list.filter(obj => obj.value == rowIndex); nm=filter[0].label }; nm;",
                    "componentVar":["hobbies"],
                    "render":true,
                    "renderType":1
                }
            ]
        ]
    }
]

Control Type


FormGear allows you to work with a lot of possible HTML input types. To add a control type, you can simply add the code as a value of the type component as a JSON object and add other corresponding components:

{
   "description":"Interviewing family characteristics individually",
   "dataKey":"family-characteristics-2022",
   "title":"Family Characteristics",
   "acronym":"FC-22.Individu",
   "version":"0.0.1",
   "components":[
		    {
                        "label":"Full Name",
                        "dataKey":"full_name",
                        "hint":"Full name including his degree, position, etc",
                        "answer":"Ignatius",
                        "enableRemark":true,
                        "type":25
                    }
		]
}

FormGear uses numbers as code to define each control type. Below is a table of control types and their corresponding code and description.

Control Type Code Description
Section 1 Adds section to form.
NestedInput 2 Adds nested input field to existing field.
InnerHTML 3 Adds text written in HTML format.
VariableInput 4 Variable input.
DateInput 11 Date input.
DateTimeLocalInput 12 Date and time input field with no time zone.
TimeInput 13 Time input field.
MonthInput 14 Month input field.
WeekInput 15 Week input field.
SingleCheckInput 16 Checkbox input field, lets user choose only one option out of limited choices.
ToggleInput 17 Toggle input.
RangeSliderInput 18 Range slider input, lets user to select a value or range of value from a specified min and max.
UrlInput 19 URL input field.
CurrencyInput 20 Currency input field, limited to IDR and USD.
ListTextInputRepeat 21 Text input field, creating a list by letting user add more written input as needed.
ListSelectInputRepeat 22 Dropdown input field, creating a list by letting user add more choices as needed.
MultipleSelectInput 23 Drop-down input, lets user to choose one or more options of limited choices.
MaskingInput 24 Number input with certain formatting, such as phone number and taxpayer identification number.
TextInput 25 Single-line input field.
RadioInput 26 Radio button, lets user choose one options of limited choices.
SelectInput 27 Drop-down input.
NumberInput 28 Numeric input field.
CheckboxInput 29 Checkbox input field, lets user choose one or more options of limited choices.
TextAreaInput 30 Adjustable text area input field.
EmailInput 31 Email address input field.
PhotoInput 32 Photo input, lets user add picture with .jpg, .jpeg, .png, and .gif format.
GpsInput 33 GPS input.
CsvInput 34 CSV input, lets user upload .csv file to be stored as .json format in the Response. The .csv file can later be downloaded again in the same format.

Component Type


To customize each form control further, you can use components . Below are the components, the corresponding ControlType in which the component can be used, the input or value type of each component, and the description.

Component Type Corresponding ControlType Input Type Description Notes
dataKey All string Component identifier.
label All string Component label that will show up in the form.
hint All string Provide hint on how to fill the corresponding field.
type All ControlType any Define the ControlType of a field.
components 1, 2 ComponentType Store components inside a Section or NestedInput.
rows 30 number Define the number of rows needed in TextAreaInput.
cols 26, 29 number Define the number of columns the options in RadioInput and CheckboxInput will be divided to.
options 22, 23, 26, 29 Option[] Define options for ListSelectInputRepeat, MultipleSelectInput, RadioInput, and CheckboxInput.
range 18 Range[] Define the min, max, and value of each step for a RangeSliderInput.
description 1, 2 string Adds description for a Section or NestedInput.
answer All any Storing answer collected on data collection. ListSelectInputRepeat and MultipleSelectInput must add: [{"label": "lastId#0","value": "0"}]
sourceQuestion 2 string Define the source question for a NestedInput field.
sourceOption 22, 23, 26, 27, 29 string Source of the options used in the field.
typeOption 22, 23, 26, 27, 29 number Type of the options used in the field. 1: Its component, 2: A lookup table, 3: Other component.
currency 20 string Define the currency that will be used in a CurrencyInput field, limited to IDR and USD.
separatorFormat 20 string Define the separator that will be used in a CurrencyInput field.
isDecimal 20 boolean Define whether or not a CurrencyInput field allows decimal.
maskingFormat 24 string Define the format of maskingInput used. 9 for numbers, a for letters, and * for alphanumeric format. e.g. : 'maskingFormat' : '9999-aa99'
expression 4 string A variable expression. getValue function can be used to get the value of other field.
componentVar 4 string[] Define the component(s) used in VariableInput.
render 4 boolean Define whether or not the variable will be rendered.
renderType 4 number Variable output type. 1: single output, 2: array output
enable All boolean Define whether or not a component have a condition(s) that need to be fulfilled before it can be filled.
enableCondition All string An expression to define a condition enabled for the field to be filled. getProp function can be used to get the client mode, getValue function can be used to get the value of other field.
componentEnable All string[] A list of component(s) used in a condition expression.
enableRemark All boolean Define whether or not a component allows a remark.
titleModalDelete 21, 22 string Title of the warning that will show up when user tries to delete an item in ListTextInputRepeat or ListSelectInputRepeat.
contentModalDelete 21, 22 string Content of the warning that will show up when user tries to delete an item in ListTextInputRepeat or ListSelectInputRepeat.

Input for ListSelectInputRepeat, MultipleSelectInput, RadioInput, and CheckboxInput


Option[] defines option components for ListSelectInputRepeat, MultipleSelectInput, RadioInput, and CheckboxInput input.

  • label: label of an option that will show up in the form.
  • value: value of an option that will be recorded.
  • open: define whether or not an option is open-ended. The value recorded will be both the value of an option and the label entered on data collection.
[
    {
        "label":"Hobbies",
        "dataKey":"hobbies",
        "type":29,
        "cols":3,
        "options":[
            {
                "label":"Sleeping",
                "value":"1"
            },
            {
                "label":"Play Games",
                "value":"2"
            },
            {
                "label":"Watching Movie",
                "value":"3"
            },
            {
                "label":"Cooking",
                "value":"4"
            },
            {
                "label":"Working",
                "value":"5"
            },
            {
                "label":"Travelling",
                "value":"6"
            },
            {
                "label":"Other",
                "value":"13",
                "open":true
            }
        ]
    }
]

Input for RangeSliderInput


Range[] defines components of RangeSliderInput input.

  • min: minimum of a range.
  • max: maximum of a range.
  • step: added value for each step.
[
   {
       "label":"Happiness Index",
       "dataKey":"happy",
       "type":18,
       "range":[
            {
                "min":0,
                "max":100,
                "step":5
            }
        ]
    }
]

Input for sourceSelect


sourceOption defines components of sourceSelect input.

  • id: unique identifier of the lookup table that will be used.
  • tableName: the name of the lookup table that will be used.
  • value: the column name in the lookup table that will be recorded as the option’s value.
  • desc: the column name in the lookup table that will be shown as the option’s label.
  • parentCondition: an array consisting of key and value. key refers to the parent lookup table’s value, while value refers to the parent lookup table’s desc.
[
    {
        "label":"Child nested",
        "dataKey":"childnested",
        "description":"ChildNested",
        "type":2,
        "sourceQuestion":"hobbies",
        "components":[
            [
                {
                    "label":"name--- $NAME$",
                    "dataKey":"name_",
                    "type":4,
                    "expression":"let nm = ''; let list = getValue('hobbies'); if(list !== undefined && list.length > 0) { let rowIndex = getRowIndex(0); let filter = list.filter(obj => obj.value == rowIndex); nm=filter[0].label }; nm;",
                    "componentVar":["most_fav"],
                    "render":true,
                    "renderType":1
                }
            ]
        ]
    }
]

Preset

Preset is used to provide prefilled data given prior to data collection. This prefilled data usually obtained from previous data collection or a listing conducted before the actual data collection. Preset consists of dataKey of the corresponding field and the prefilled answer to that field.

preset.json
│
└───predata
│   │
│   └───
│       │   dataKey
│       │   answer
│   │
│   └───
│       │   dataKey
│       │   answer
│
{
   "description":"sample template",
   "dataKey":"sample_tpl1",
   "predata":[
      {
         "dataKey":"nama_lengkap",
         "answer":"Setyadi"
      }
   ]
}

Response

Response is initially empty, and is used to store any response given later during data collection. Response consists of dataKey of the corresponding field and the answer collected from that field. answer from a field that has both label and value will record both. answer will only be collected if the value entered to the corresponding field satisfied both the condition enabled and validation test function. Below is the structure and example of the response JSON object:

response.json
│
│   description
│   dataKey
│   templateVersion
│   validationVersion
│   createdBy
│   updatedBy
│   createdAt
│   updatedAt
└───answers
│   	   │
│   	   └───
│       	│   dataKey
│       	│   answer	
│   	   │
│   	   └───
│       	│   dataKey
│       	│   answer
│

{
   "description":"sample template",
   "dataKey":"sample_tpl1",
   "answers":[
      {
         "dataKey":"agree",
         "answer":true
      },
      {
         "dataKey":"full_name",
         "answer":"Agung"
      },
      {
         "dataKey":"members",
         "answer": {
            "value" : "3",
            "label" : "Clementine Bauch"
         }
      },
      {
         "dataKey":"address",
         "answer":"Jalan Otista"
      },
      {
         "dataKey":"field_usage",
         "answer":[
            {
               "label": "lastId#1",
               "value": "0"
            },
            {
               "label": "Farm",
               "value": "1"
            },
            {
               "label": "Meadows",
               "value": "3"
            }
         ]
      },
   ]
}

Validation

Validation is used to validate the answer given during data collection against a test function. Unlike other similar framework, validation is handled in a FALSE condition, meaning the test function is a condition where the value entered to a form control would be false. Validation has several components you can add:

  • dataKey: component identifier.
  • validations: store validation for each dataKey.
  • componentValidation: a list of the dataKey of components used in the test function.
  • test: the test function. A getValue function can be used to get the value of other field.
  • message: the warning message that will show up when the value entered did not fulfill the test function.
  • type: define whether the validation will be just a warning or an error.

Below are the structure and example of the validation JSON object:

validation.json
│
└───description
│   dataKey
│   version
│   testFunctions
│   │
│   └───
│       │   dataKey
│       │   componentValidation
│       │   validations
│			│
│			└───
│			    │   test
│			    │   message
│			    │   type	
│			│
│			└───
│			    │   test
│			    │   message
│			    │   type
│
{
    "description":"sample template",
    "dataKey":"sample_tpl1",
    "version":"0.0.1",
    "testFunctions":[
        {
            "dataKey":"age",
            "componentValidation":["age"],
            "validations": [
                {
                    "test":"getValue('age') >= 8 && getValue('age') < 10",
                    "message":"Min.10 y.o.",
                    "type":1
                },
                {
                    "test":"getValue('age') < 8",
                    "message":"Min.8 y.o.",
                    "type":2
                }
            ]
        }
    ]
}

Remark

Remark is initially empty and used to store notes on each field collected later during data collection. This note can be used to provide additional information of a field and bypass validation if the data found during data collection doesn’t satisfy the test function. Below is the structure and example of the remark JSON object:

remark.json
│
└───dataKey
|   notes
│   │
│   └───
│       │   dataKey
│       │   comments
│		    │
│		    └───
│		        │   sender
│		        │   dateTime
│		        │   comment	
│		    │
│		    └───
│		        │   sender
│		        │   dateTime
│		        │   comment
│
{
    "dataKey": "",
    "notes": [
        {
            "dataKey": "full_name",
            "comments": [
                {
                    "sender": "AdityaSetyadi",
                    "datetime": "2022-03-17 15:09:54",
                    "comment": "Based on the ID Card"
                },
                {
                    "sender": "AdityaSetyadi",
                    "datetime": "2022-03-18 10:10:40",
                    "comment": "Not the same with his driving license"
                }
            ]
        },
        {
            "dataKey": "land_area#1",
            "comments": [
                {
                    "sender": "AdityaSetyadi",
                    "datetime": "2022-03-18 01:09:15",
                    "comment": "Unknown"
                }
            ]
        }
    ]
}

Contributing

Your assistance is greatly appreciated if you want to contribute and make it better.

Further development ideas:

  • FormGear templates design platform
  • FormGear validation creator platform

License

FormGear is licensed under MIT License.

Our Team