Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/ahzhezhe/form-studio

A tool that helps design, create and manage form / survey / questionnaire through simple JSON configurations.
https://github.com/ahzhezhe/form-studio

form form-builder form-editor form-engine questionnaire survey

Last synced: about 1 month ago
JSON representation

A tool that helps design, create and manage form / survey / questionnaire through simple JSON configurations.

Awesome Lists containing this project

README

        

# **form-studio**
[![npm package](https://img.shields.io/npm/v/form-studio)](https://www.npmjs.com/package/form-studio)
[![npm downloads](https://img.shields.io/npm/dt/form-studio)](https://www.npmjs.com/package/form-studio)
[![GitHub test](https://github.com/ahzhezhe/form-studio/workflows/test/badge.svg?branch=master)](https://github.com/ahzhezhe/form-studio)
[![GitHub issues](https://img.shields.io/github/issues/ahzhezhe/form-studio)](https://github.com/ahzhezhe/form-studio/issues)
[![GitHub license](https://img.shields.io/github/license/ahzhezhe/form-studio)](https://github.com/ahzhezhe/form-studio/blob/master/LICENSE)


## **What is form-studio?**
It is a tool that helps design, create and manage form / survey / questionnaire through simple JSON configurations.

It provides:
- Data structure for form configurations and answers.
- Conditionally disabling/enabling questions based on choices made in another question.
- Answer validation mechanism.
- Instructions for rendering the UI based on current status of the form.

It does not provide:
- Any UI components, define your own UI configurations that suit your project needs and render the UI according to your own design system.
- Validators, define your own validators that suit your project needs.

[API Documentation](https://ahzhezhe.github.io/docs/form-studio-v0/index.html)

[Demo](https://github.com/ahzhezhe/form-studio-demo)


## **Install via NPM**
```
npm install form-studio
```


## **Import**
```typescript
import { Form } from 'form-studio';
```
or
```typescript
const { Form } = require('form-studio');
```


# **Form Configs**
Form configs is the definition of the form. It should be persisted somewhere (e.g. database) so that it can be reused later.

There are 3 types of items in a configs: `Group`, `Question` and `Choice`.

Each of them has the following properties:
- `id`: An unique id to identify the item
- `order`: Sort order of the item among it's parent
- `defaultDisabled`: To indicate that the item is disabled by default
- `custom`: Any values that help you determine on how to render the frontend UI or how to perform validation, it should contain minimal but useful information, e.g. title, placeholder

### **Group**
A group is a logical grouping of a set of questions.

A form needs at least 1 group.

Groups can also have sub-groups.

### **Question**
There are 3 types of questions: `any`, `choice` and `choices`.

A question comes with an answer (could be undefined if it is unanswered) and an error (could be undefined if it is unanswered, unvalidated or passed validation).

`any` questions accept `any` value as an answer.

`choice` questions accept a choice value as an answer.

`choices` questions accept a list of choice values as an answer.

`choice` and `choices` questions need to have 1 or more choices.

You can also define the validators to be used by a question to validate its answer.

### **Choice**
Choices are for `choice` or `choices` questions.

A choice comes with a value. Value of the choices will be the answer of the question.

A choice has the ability to disable/enable other groups/questions/choices when it's selected/unselected.

### **Example**
The following example consists of 1 group and 2 questions under it.

The second question is disabled by default. If 'yes' is selected for the first question, the second question will be enabled.

```json
{
"groups": [
{
"questions": [
{
"id": "proceed",
"type": "choice",
"custom": {
"title": "Would you like to proceed?"
},
"choices": [
{
"id": "yes",
"custom": {
"title": "Yes"
}
},
{
"id": "no",
"custom": {
"title": "No"
}
}
],
},
{
"id": "name",
"defaultDisabled": true,
"enabledOnSelected": ["yes"],
"type": "any",
"custom": {
"type": "string",
"title": "What is you name?",
},
}
]
}
]
}
```

### **Validate Configs**
You can call `validateConfigs` method to validate your configs.

```
Form.validateConfigs(configs);
```


# **Validators**
`form-studio` doesn't come with any predefined validator. You need to define your own validators according to your project needs.

A validator is a function that will be called when the answer of a question is updated, it throws error when validation fails.

Each question can be assigned with one or more validators to be used.

### **Example**
```typescript
const validators = {
atLeast1: answer => {
if (answer.length < 1) {
throw new Error('Please select at least 1 option.');
}
},

notNull: answer => {
if (!answer) {
throw new Error('This question cannot be left unanswered.');
}
},

number: (answer, question) => {
const { min, max } = question.custom;
if (answer < min){
throw new Error('Please enter no less than ' + min + '.');
}
if (answer > max){
throw new Error('Please enter no greater than ' + max + '.');
}
}
};
```


# **Form Update Listener**
A listener function that will be called when form is updated.

Form will be updated when answer is set, validation is triggered, etc.

Form updated listener is needed when the form is being used in frontend, so that you can trigger an UI rerender when form is updated.

### **Example (React)**
```typescript
const [renderInstructions, setRenderInstructions] = useState();

const onFormUpdate = form => setRenderInstructions(form.getRenderInstructions());
```


# **Construct a Form**
```typescript
const form = new Form(configs, { validators, onFormUpdate });
```


# **Render Instructions**
Render instructions can be get by calling `getRenderInstructions` method.

It is a set of instructions that tell you how the form should look like.

Each item in the instructions comes with the following properties:
- `id`: An unique id to identify the item
- `disabled`: Whether or not this item is disabled, you should handle it in the UI, e.g. hide or grey out disabled item
- `custom`: The exact same values that you specified in the form configs

Questions also come with the following important properties that you will need to use to determine the UI:
- `type`:
- `any`: render whatever UI that is required based on your `custom` configs, e.g. if `custom.inputType` is `string`, then a text input is rendered
- `choice`: render UI that allows user to select 1 option from a list of options, e.g. select, radio button group
- `choices`: render UI that allows user to select multiple options from a list of options, e.g. check box group
- `currentAnswer`: current answer of the question, it is unvalidated and might not be valid, but you will still need to show them on UI
- `validatedAnswer`: validated answer
- `validating`: whether or not the question is currently being validated, it could happen if the validator used is an aysnc function, you might want to show a spinner or some other indicator on UI
- `error`: error for question which failed validation

### **Example (React)**
```typescript
let form: Form;

export const SurveyPage = () => {
const [renderInstructions, setRenderInstructions] = useState();

useEffect(() => {
form = new Form(configs, {
validators,
validate: false,
onFormUpdate: form => setRenderInstructions(form.getRenderInstructions())
});
}, []);

const renderQuestion = (question: QuestionRenderInstructions) => {
const { disabled, type, custom } = question;
if (disabled) {
return null;
}
if (type === 'any') {
return (
<>
{custom.inputType === 'string' && renderStringInput(question)}
>
);
}
if (type === 'choice') {
return renderRadioGroup(question);
}
if (type === 'choices') {
return renderCheckBoxGroup(question);
}
};

const renderRadioGroup = (question: QuestionRenderInstructions) => {
const { id, choices, error, currentAnswer } = question;
return (
form.setChoice(id, e.target.value)}>
{choices!.map(choice =>

{choice.custom.title}

)}

);
};

const renderCheckBoxGroup = (question: QuestionRenderInstructions) => {
const { id, choices, error, currentAnswer } = question;
return (
form.setChoices(id, answer as any[])}>
{choices!.map(choice =>

{choice.custom.title}

)}

);
};

const renderStringInput = (question: QuestionRenderInstructions) => {
const { id, custom, currentAnswer, error } = question;
return (
form.setAny(id, e.target.value)} />
);
};

return renderInstructions?.questions.map(question => renderQuestion(question));
};
```


# **Setting Answers**
`any` questions use `setAnswer` or `setAny` method to set answer.

`choice` questions use `setAnswer`, `setChoice` or `selectChoice` method to set answer.

`choices` questions use `setAnswer`, `setChoices` or `selectChoice` method to set answer.

### **Example (General)**
```typescript
form.setChoice('proceed', 'yes');
form.setAny('name', 'Jason');
```

### **Example (Frontend)**
```typescript
onChange={e => form.setChoice(id, e.target.value)}
onChange={e => form.setAny(id, e.target.value)}
```


# **Validate in Frontend**
Use `validate` method to trigger validation for the entire form.

Use `isValidating` & `isClean` methods to get the state of form validation.
E.g. you can disable a button if `isValidating` is `true` or `isClean` is `false`.

### **Example (React)**
```typescript
const save = () => {
if (form.isValidating() || !form.isClean()) {
alert('Please try again.')
}
}

Save
```

# **Persist the Answers**
Use `asyncValidate` method to get the final validated answers.

You can then store the answers to database or send it to backend via API.

### **Example (Frontend)**
```typescript
const answers = await form.asyncValidate();

if (!answers) {
alert('There are some invalid answers.');
return;
}

await ... // Call API to send the answers to backend
```

If you are sending the answers from frontend to backend, backend can construct the form using the same configs, import the answers, and call `asyncValidate` method again to revalidate the answers from frontend before you save them into database.

### **Example (Backend API)**
```typescript
const answers = req.body;
form.importAnswers(answers);
const valid = await form.asyncValidate();

if (!valid) {
res.status(400);
res.json({ error: 'There are some invalid answers.' });
res.end();
return;
}

await ... // Save the answers to database

res.status(200);
res.end();
```


# **Importing Answers**
Use `importAnswers` method to import answers to the entire form.

### **Example (Frontend)**
```typescript
const answers = await ... // retrieve from API
form.importAnswers(answers);
```

### **Example (Backend)**
```typescript
const answers = await ... // retrieve from database
form.importAnswers(answers);
```


# **Other Features**
Use the following methods to clear current answers:
- `clear`
- `clearGroup`
- `clearAnswer`

Use the following methods to reset answers to their default answers:
- `reset`
- `resetGroup`
- `resetAnswer`