{"id":19163482,"url":"https://github.com/ahzhezhe/form-studio","last_synced_at":"2025-05-07T11:22:13.368Z","repository":{"id":42491605,"uuid":"367233972","full_name":"ahzhezhe/form-studio","owner":"ahzhezhe","description":"A tool that helps design, create and manage form / survey / questionnaire through simple JSON configurations.","archived":false,"fork":false,"pushed_at":"2024-10-26T16:01:52.000Z","size":1629,"stargazers_count":5,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-19T21:25:54.855Z","etag":null,"topics":["form","form-builder","form-editor","form-engine","questionnaire","survey"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"isc","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ahzhezhe.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null},"funding":{"custom":["https://www.paypal.me/ahzhezhe"]}},"created_at":"2021-05-14T03:00:57.000Z","updated_at":"2025-04-03T12:48:56.000Z","dependencies_parsed_at":"2024-09-18T06:17:12.166Z","dependency_job_id":"7c015c59-7b21-4318-9289-bb703dc5e384","html_url":"https://github.com/ahzhezhe/form-studio","commit_stats":{"total_commits":169,"total_committers":3,"mean_commits":"56.333333333333336","dds":0.09467455621301779,"last_synced_commit":"216d561d2bf8f84f020019dacbd38c8287b5fe39"},"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ahzhezhe%2Fform-studio","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ahzhezhe%2Fform-studio/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ahzhezhe%2Fform-studio/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ahzhezhe%2Fform-studio/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ahzhezhe","download_url":"https://codeload.github.com/ahzhezhe/form-studio/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252866127,"owners_count":21816397,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["form","form-builder","form-editor","form-engine","questionnaire","survey"],"created_at":"2024-11-09T09:15:32.964Z","updated_at":"2025-05-07T11:22:13.347Z","avatar_url":"https://github.com/ahzhezhe.png","language":"TypeScript","funding_links":["https://www.paypal.me/ahzhezhe"],"categories":[],"sub_categories":[],"readme":"# **form-studio**\n[![npm package](https://img.shields.io/npm/v/form-studio)](https://www.npmjs.com/package/form-studio)\n[![npm downloads](https://img.shields.io/npm/dt/form-studio)](https://www.npmjs.com/package/form-studio)\n[![GitHub test](https://github.com/ahzhezhe/form-studio/workflows/test/badge.svg?branch=master)](https://github.com/ahzhezhe/form-studio)\n[![GitHub issues](https://img.shields.io/github/issues/ahzhezhe/form-studio)](https://github.com/ahzhezhe/form-studio/issues)\n[![GitHub license](https://img.shields.io/github/license/ahzhezhe/form-studio)](https://github.com/ahzhezhe/form-studio/blob/master/LICENSE)\n\n\u003cbr /\u003e\n\n## **What is form-studio?**\nIt is a tool that helps design, create and manage form / survey / questionnaire through simple JSON configurations.\n\nIt provides:\n- Data structure for form configurations and answers.\n- Conditionally disabling/enabling questions based on choices made in another question.\n- Answer validation mechanism.\n- Instructions for rendering the UI based on current status of the form.\n\nIt does not provide:\n- Any UI components, define your own UI configurations that suit your project needs and render the UI according to your own design system.\n- Validators, define your own validators that suit your project needs.\n\n[API Documentation](https://ahzhezhe.github.io/docs/form-studio-v0/index.html)\n\n[Demo](https://github.com/ahzhezhe/form-studio-demo)\n\n\u003cbr /\u003e\n\n## **Install via NPM**\n```\nnpm install form-studio\n```\n\n\u003cbr /\u003e\n\n## **Import**\n```typescript\nimport { Form } from 'form-studio';\n```\nor\n```typescript\nconst { Form } = require('form-studio');\n```\n\n\u003cbr /\u003e\n\n# **Form Configs**\nForm configs is the definition of the form. It should be persisted somewhere (e.g. database) so that it can be reused later.\n\nThere are 3 types of items in a configs: `Group`, `Question` and `Choice`.\n\nEach of them has the following properties:\n- `id`: An unique id to identify the item\n- `order`: Sort order of the item among it's parent\n- `defaultDisabled`: To indicate that the item is disabled by default\n- `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\n\n### **Group**\nA group is a logical grouping of a set of questions.\n\nA form needs at least 1 group.\n\nGroups can also have sub-groups.\n\n### **Question**\nThere are 3 types of questions: `any`, `choice` and `choices`.\n\nA 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).\n\n`any` questions accept `any` value as an answer.\n\n`choice` questions accept a choice value as an answer.\n\n`choices` questions accept a list of choice values as an answer.\n\n`choice` and `choices` questions need to have 1 or more choices.\n\nYou can also define the validators to be used by a question to validate its answer.\n\n### **Choice**\nChoices are for `choice` or `choices` questions.\n\nA choice comes with a value. Value of the choices will be the answer of the question.\n\nA choice has the ability to disable/enable other groups/questions/choices when it's selected/unselected.\n\n### **Example**\nThe following example consists of 1 group and 2 questions under it.\n\nThe second question is disabled by default. If 'yes' is selected for the first question, the second question will be enabled.\n\n```json\n{\n  \"groups\": [\n    {\n      \"questions\": [\n        {\n          \"id\": \"proceed\",\n          \"type\": \"choice\",\n          \"custom\": {\n            \"title\": \"Would you like to proceed?\"\n          },\n          \"choices\": [\n            {\n              \"id\": \"yes\",\n              \"custom\": {\n                \"title\": \"Yes\"\n              }\n            },\n            {\n              \"id\": \"no\",\n              \"custom\": {\n                \"title\": \"No\"\n              }\n            }\n          ],\n        },\n        {\n          \"id\": \"name\",\n          \"defaultDisabled\": true,\n          \"enabledOnSelected\": [\"yes\"],\n          \"type\": \"any\",\n          \"custom\": {\n            \"type\": \"string\",\n            \"title\": \"What is you name?\",\n          },\n        }\n      ]\n    }\n  ]\n}\n```\n\n### **Validate Configs**\nYou can call `validateConfigs` method to validate your configs.\n\n```\nForm.validateConfigs(configs);\n```\n\n\u003cbr /\u003e\n\n# **Validators**\n`form-studio` doesn't come with any predefined validator. You need to define your own validators according to your project needs.\n\nA validator is a function that will be called when the answer of a question is updated, it throws error when validation fails.\n\nEach question can be assigned with one or more validators to be used.\n\n### **Example**\n```typescript\nconst validators = {\n  atLeast1: answer =\u003e {\n    if (answer.length \u003c 1) {\n      throw new Error('Please select at least 1 option.');\n    }\n  },\n\n  notNull: answer =\u003e {\n    if (!answer) {\n      throw new Error('This question cannot be left unanswered.');\n    }\n  },\n\n  number: (answer, question) =\u003e {\n    const { min, max } = question.custom;\n    if (answer \u003c min){\n      throw new Error('Please enter no less than ' + min + '.');\n    }\n    if (answer \u003e max){\n      throw new Error('Please enter no greater than ' + max + '.');\n    }\n  }\n};\n```\n\n\u003cbr /\u003e\n\n# **Form Update Listener**\nA listener function that will be called when form is updated.\n\nForm will be updated when answer is set, validation is triggered, etc.\n\nForm updated listener is needed when the form is being used in frontend, so that you can trigger an UI rerender when form is updated.\n\n### **Example (React)**\n```typescript\nconst [renderInstructions, setRenderInstructions] = useState\u003cRenderInstructions\u003e();\n\nconst onFormUpdate = form =\u003e setRenderInstructions(form.getRenderInstructions());\n```\n\n\u003cbr /\u003e\n\n# **Construct a Form**\n```typescript\nconst form = new Form(configs, { validators, onFormUpdate });\n```\n\n\u003cbr /\u003e\n\n# **Render Instructions**\nRender instructions can be get by calling `getRenderInstructions` method.\n\nIt is a set of instructions that tell you how the form should look like.\n\nEach item in the instructions comes with the following properties:\n- `id`: An unique id to identify the item\n- `disabled`: Whether or not this item is disabled, you should handle it in the UI, e.g. hide or grey out disabled item\n- `custom`: The exact same values that you specified in the form configs\n\nQuestions also come with the following important properties that you will need to use to determine the UI:\n- `type`:\n  - `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\n  - `choice`: render UI that allows user to select 1 option from a list of options, e.g. select, radio button group\n  - `choices`: render UI that allows user to select multiple options from a list of options, e.g. check box group\n- `currentAnswer`: current answer of the question, it is unvalidated and might not be valid, but you will still need to show them on UI\n- `validatedAnswer`: validated answer\n- `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\n- `error`: error for question which failed validation\n\n### **Example (React)**\n```typescript\nlet form: Form;\n\nexport const SurveyPage = () =\u003e {\n  const [renderInstructions, setRenderInstructions] = useState\u003cRenderInstructions\u003e();\n\n  useEffect(() =\u003e {\n    form = new Form(configs, {\n      validators,\n      validate: false,\n      onFormUpdate: form =\u003e setRenderInstructions(form.getRenderInstructions())\n    });\n  }, []);\n\n  const renderQuestion = (question: QuestionRenderInstructions) =\u003e {\n    const { disabled, type, custom } = question;\n    if (disabled) {\n      return null;\n    }\n    if (type === 'any') {\n      return (\n        \u003c\u003e\n          {custom.inputType === 'string' \u0026\u0026 renderStringInput(question)}\n        \u003c/\u003e\n      );\n    }\n    if (type === 'choice') {\n      return renderRadioGroup(question);\n    }\n    if (type === 'choices') {\n      return renderCheckBoxGroup(question);\n    }\n  };\n\n  const renderRadioGroup = (question: QuestionRenderInstructions) =\u003e {\n    const { id, choices, error, currentAnswer } = question;\n    return (\n      \u003cRadioGroup\n        error={error}\n        value={currentAnswer}\n        onChange={e =\u003e form.setChoice(id, e.target.value)}\u003e\n        {choices!.map(choice =\u003e\n          \u003cRadio\n            value={choice.value}\n            disabled={choice.disabled}\u003e\n            {choice.custom.title}\n          \u003c/Radio\u003e\n        )}\n      \u003c/RadioGroup\u003e\n    );\n  };\n\n  const renderCheckBoxGroup = (question: QuestionRenderInstructions) =\u003e {\n    const { id, choices, error, currentAnswer } = question;\n    return (\n      \u003cCheckBoxGroup\n        error={error}\n        value={currentAnswer}\n        onChange={answer =\u003e form.setChoices(id, answer as any[])}\u003e\n        {choices!.map(choice =\u003e\n          \u003cCheckBox\n            value={choice.value}\n            disabled={choice.disabled}\u003e\n            {choice.custom.title}\n          \u003c/CheckBox\u003e\n        )}\n      \u003c/CheckBoxGroup\u003e\n    );\n  };\n\n  const renderStringInput = (question: QuestionRenderInstructions) =\u003e {\n    const { id, custom, currentAnswer, error } = question;\n    return (\n      \u003cTextInput\n        error={error}\n        maxLength={custom.maxLength as number}\n        value={currentAnswer}\n        onChange={e =\u003e form.setAny(id, e.target.value)} /\u003e\n    );\n  };\n\n  return renderInstructions?.questions.map(question =\u003e renderQuestion(question));\n};\n```\n\n\u003cbr /\u003e\n\n# **Setting Answers**\n`any` questions use `setAnswer` or `setAny` method to set answer.\n\n`choice` questions use `setAnswer`, `setChoice` or `selectChoice` method to set answer.\n\n`choices` questions use `setAnswer`, `setChoices` or `selectChoice` method to set answer.\n\n### **Example (General)**\n```typescript\nform.setChoice('proceed', 'yes');\nform.setAny('name', 'Jason');\n```\n\n### **Example (Frontend)**\n```typescript\nonChange={e =\u003e form.setChoice(id, e.target.value)}\nonChange={e =\u003e form.setAny(id, e.target.value)}\n```\n\n\u003cbr /\u003e\n\n# **Validate in Frontend**\nUse `validate` method to trigger validation for the entire form.\n\nUse `isValidating` \u0026 `isClean` methods to get the state of form validation.\nE.g. you can disable a button if `isValidating` is `true` or `isClean` is `false`.\n\n### **Example (React)**\n```typescript\nconst save = () =\u003e {\n  if (form.isValidating() || !form.isClean()) {\n    alert('Please try again.')\n  }\n}\n\n\u003cButton onClick={save}\u003eSave\u003c/Button\u003e\n```\n\n# **Persist the Answers**\nUse `asyncValidate` method to get the final validated answers.\n\nYou can then store the answers to database or send it to backend via API.\n\n### **Example (Frontend)**\n```typescript\nconst answers = await form.asyncValidate();\n\nif (!answers) {\n  alert('There are some invalid answers.');\n  return;\n}\n\nawait ... // Call API to send the answers to backend\n```\n\nIf 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.\n\n### **Example (Backend API)**\n```typescript\nconst answers = req.body;\nform.importAnswers(answers);\nconst valid = await form.asyncValidate();\n\nif (!valid) {\n  res.status(400);\n  res.json({ error: 'There are some invalid answers.' });\n  res.end();\n  return;\n}\n\nawait ... // Save the answers to database\n\nres.status(200);\nres.end();\n```\n\n\u003cbr /\u003e\n\n# **Importing Answers**\nUse `importAnswers` method to import answers to the entire form.\n\n### **Example (Frontend)**\n```typescript\nconst answers = await ... // retrieve from API\nform.importAnswers(answers);\n```\n\n### **Example (Backend)**\n```typescript\nconst answers = await ... // retrieve from database\nform.importAnswers(answers);\n```\n\n\u003cbr /\u003e\n\n# **Other Features**\nUse the following methods to clear current answers:\n- `clear`\n- `clearGroup`\n- `clearAnswer`\n\nUse the following methods to reset answers to their default answers:\n- `reset`\n- `resetGroup`\n- `resetAnswer`\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fahzhezhe%2Fform-studio","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fahzhezhe%2Fform-studio","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fahzhezhe%2Fform-studio/lists"}