{"id":38496754,"url":"https://github.com/fatherbrennan/fs-form-group","last_synced_at":"2026-01-17T05:54:19.747Z","repository":{"id":57686395,"uuid":"474859619","full_name":"fatherbrennan/fs-form-group","owner":"fatherbrennan","description":"The FSFormGroup library makes it easy to create DOM Elements with linked methods to update and save states to a JSON file instance.","archived":false,"fork":false,"pushed_at":"2022-03-28T05:50:45.000Z","size":15,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-08-09T06:37:10.358Z","etag":null,"topics":["electron","fs","nodejs","vanilla-javascript"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fatherbrennan.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-03-28T05:35:51.000Z","updated_at":"2022-04-03T09:06:45.000Z","dependencies_parsed_at":"2022-09-19T08:40:18.145Z","dependency_job_id":null,"html_url":"https://github.com/fatherbrennan/fs-form-group","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/fatherbrennan/fs-form-group","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fatherbrennan%2Ffs-form-group","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fatherbrennan%2Ffs-form-group/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fatherbrennan%2Ffs-form-group/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fatherbrennan%2Ffs-form-group/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fatherbrennan","download_url":"https://codeload.github.com/fatherbrennan/fs-form-group/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fatherbrennan%2Ffs-form-group/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28501334,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-17T04:31:57.058Z","status":"ssl_error","status_checked_at":"2026-01-17T04:31:45.816Z","response_time":85,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":["electron","fs","nodejs","vanilla-javascript"],"created_at":"2026-01-17T05:54:17.893Z","updated_at":"2026-01-17T05:54:19.735Z","avatar_url":"https://github.com/fatherbrennan.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# FSFormGroup\n\nThe FSFormGroup library makes it easy to create DOM Elements with linked methods to update and save states to a JSON file instance.\n\n**Intended use**\n\nUsing [Electron](https://www.electronjs.org/) to create desktop applications is simple, however saving app preferences must be done outside the sandboxed app session - which is why FSFormGroup uses the Node.js File System module to read/write/parse JavaScript objects and save them to a JSON file, which can be read back on next app launch.\n\n## **Installation**\n\n```sh\nnpm install fs-form-group --save-dev\n```\n\n**Import and define module**\n\n```js\nconst FSFormGroup = require('fs-form-group');\n```\n\n## **Constructor**\n\n### Syntax\n\n```js\nnew FSFormGroup(path);\nnew FSFormGroup(path, options);\nnew FSFormGroup(path, options, css);\n```\n\n### Parameters\n\n`path`\n\n-   | Type     | Required | Description                            |\n    | -------- | -------- | -------------------------------------- |\n    | `string` | \u0026#10004; | Valid file path to store instance data |\n\n`options`\n\n-   | Type     | Required | Description                                  |\n    | -------- | -------- | -------------------------------------------- |\n    | `object` |          | Options for JSON stringification and parsing |\n\n-   | Property   | Required | Type     | Default  | Description                                       |\n    | ---------- | -------- | -------- | -------- | ------------------------------------------------- |\n    | `encoding` |          | `string` | `'utf8'` | Use as data encoding type                         |\n    | `space`    |          | `number` | `0`      | Passed to `JSON.stringify()` as `space` parameter |\n\n`css`\n\n-   | Type     | Required | Description                                                 |\n    | -------- | -------- | ----------------------------------------------------------- |\n    | `object` |          | Space-separated list of classes to attach to created groups |\n\n-   | Property               | Required | Type     | Default | Description                                                   |\n    | ---------------------- | -------- | -------- | ------- | ------------------------------------------------------------- |\n    | `formGroup`            |          | `string` | `''`    | Add to group wrapper element class `\u003cdiv\u003e`                    |\n    | `formGroupHeading`     |          | `string` | `''`    | Add to heading element class `\u003ch3\u003e`                           |\n    | `formGroupDescription` |          | `string` | `''`    | Add to description element class `\u003cp\u003e`                        |\n    | `formGroupComponent`   |          | `string` | `''`    | Add to input component wrapper element class `\u003cdiv\u003e`          |\n    | `formGroupInput`       |          | `string` | `''`    | Add to all input elements class (including buttons) `\u003cinput\u003e` |\n\n### Examples\n\nCreate a new file instance using default options\n\n```js\nconst form = new FSFormGroup('./path/to/file.json');\n```\n\nCreate a new file instance which will attach defined classes to each created input group.\n\n```js\nconst form = new FSFormGroup('./path/to/file.json', null, {\n    formGroup: 'fg-dark-theme',\n    formGroupInput: 'fg-dark-theme-input',\n});\n\n// Result from FSFormGroup.inputGroup(...):\n//\n// \u003cdiv class=\"fg-dark-theme\"\u003e\n//     \u003ch3\u003eHeading\u003c/h3\u003e\n//     \u003cp\u003eDescription\u003c/p\u003e\n//     \u003cdiv\u003e\n//         \u003cinput type=\"text\" class=\"fg-dark-theme-input\" value=\"inputGroup\" /\u003e\n//     \u003c/div\u003e\n// \u003c/div\u003e\n```\n\n## **Private Properties**\n\n`_components`\n\n-   | Type     | Default | Description                                                                                                             |\n    | -------- | ------- | ----------------------------------------------------------------------------------------------------------------------- |\n    | `object` | `{}`    | Holds all created component instances. **Do not read**. States should be read using [`FSFormGroup.getData()`](#getdata) |\n\n`_groupKey`\n\n-   | Type     | Default | Description                                                                                                                                                                          |\n    | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n    | `number` | `0`     | Counter variable staring from `0` of created component instances with an undefined `groupKey` value. Used to create default instance `groupKey` by: group`_groupKey`; e.g. `group0`. |\n\n## **Methods**\n\n---\n\n### **`getData()`**\n\n### Syntax\n\n```js\nFSFormGroup.getData();\nFSFormGroup.getData(groupKey);\n```\n\n### Parameters\n\n`groupKey`\n\n-   | Type     | Description                                     |\n    | -------- | ----------------------------------------------- |\n    | `string` | The existing identifier of the object to return |\n\n### Returns\n\n-   If `groupKey` is omitted, an object is returned with all groups. `{ groupKeys[] }`.\n-   If `groupKey` is defined and exists, the `groupKey[]` array is returned; otherwise returns `undefined`.\n\n### Examples\n\nCollect latest file state and loop through all groups and states.\n\n```js\n// Collect latest file state as JavaScript object\nconst data = form.getData();\n\nconsole.log(data);\n// -\u003e {\n//        group0: [\n//            { value: 'inputs group0 state 1' },\n//            { value: 'inputs group0 state 2' },\n//        ],\n//        group1: [\n//            { value: 'inputs group1 state 3' },\n//            { value: 'inputs group1 state 4' },\n//        ],\n//    }\n\n// Loop through form groups\nfor (const group in data) {\n    console.log(group);\n\n    // Loop through group states\n    for (const state of data[group]) {\n        console.log(state);\n    }\n}\n\n// Console:\n// -\u003e group0\n//   -\u003e { value: 'inputs group0 state 1' }\n//   -\u003e { value: 'inputs group0 state 2' }\n// -\u003e group1\n//   -\u003e { value: 'inputs group1 state 3' }\n//   -\u003e { value: 'inputs group1 state 4' }\n```\n\nCollect latest file group state and loop through all states.\n\n```js\n// Collect latest file state as JavaScript object\nconst data = form.getData('group1');\n\nconsole.log(data);\n// -\u003e {\n//        group1: [\n//            { value: 'inputs group1 state 3' },\n//            { value: 'inputs group1 state 4' },\n//        ],\n//    }\n\n// Loop through form groups\nfor (const group in data) {\n    console.log(group);\n\n    // Loop through group states\n    for (const state of data[group]) {\n        console.log(state);\n    }\n}\n\n// Console:\n// -\u003e group1\n//   -\u003e { value: 'inputs group1 state 3' }\n//   -\u003e { value: 'inputs group1 state 4' }\n```\n\n---\n\n### **`inputGroup()`**\n\n### Syntax\n\n```js\nFSFormGroup.inputGroup(options);\n```\n\n### Parameters\n\n`options`\n\n-   | Type     | Required | Description                          |\n    | -------- | -------- | ------------------------------------ |\n    | `object` | \u0026#10004; | Object containing component options. |\n\n-   | Property           | Required | Type                                                 | Default          | Description                                                                                                                                                                                                                   |\n    | ------------------ | -------- | ---------------------------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n    | `componentClass`   |          | `string`                                             |                  | Space-separated class list to attach to component wrapper element `\u003cdiv\u003e`                                                                                                                                                     |\n    | `description`      |          | `string`                                             |                  | Text content to display in component description element `\u003cp\u003e`                                                                                                                                                                |\n    | `descriptionClass` |          | `string`                                             |                  | Space-separated class list to attach to description element `\u003cp\u003e`                                                                                                                                                             |\n    | `events`           |          | `array` or\u003cbr\u003e`object`                               |                  | Key, value pairs where: `key` defines a valid `HTMLElement` or `HTMLInputElement` [`Event`](https://developer.mozilla.org/en-US/docs/Web/Events); and `value` defines a `callbackFn`. See [events](#events).                  |\n    | `groupClass`       |          | `string`                                             |                  | Space-separated class list to attach to group wrapper element `\u003cdiv\u003e`                                                                                                                                                         |\n    | `groupKey`         |          | `string`                                             | group`_groupKey` | Use as group key (identifier).                                                                                                                                                                                                |\n    | `heading`          |          | `string`                                             |                  | Text content to display in component description element `\u003ch3\u003e`                                                                                                                                                               |\n    | `headingClass`     |          | `string`                                             |                  | Space-separated class list to attach to heading element `\u003ch3\u003e`                                                                                                                                                                |\n    | `props`            |          | `array` or\u003cbr\u003e`object`                               |                  | Immutable attributes to set on `HTMLInputElement`. Defined by key, value pairs. Underscore keys will be accessible through instance but not set to element. See [props](#props).                                              |\n    | `state`            | \u0026#10004; | `array` or\u003cbr\u003e`object` or\u003cbr\u003e`string` or\u003cbr\u003e`number` |                  | Mutable key, value pairs for `HTMLInputElement` which are parsed and written to JSON file. A passed `object` must have a minimum `value` key with a set value pair.\u003cbr\u003ee.g. `{ value: 'hello world!' }`. See [state](#state). |\n\n### Returns\n\n-   Input group component wrapped in a `HTMLDivElement`.\n\n---\n\n### **`inputsGroup()`**\n\n### Syntax\n\n```js\nFSFormGroup.inputsGroup(options);\n```\n\n### Parameters\n\n`options`\n\n-   | Type    | Required | Description                                        |\n    | ------- | -------- | -------------------------------------------------- |\n    | `array` | \u0026#10004; | Array of objects containing _n_ component options. |\n\n-   | Property           | Required | Type                                                 | Default          | Description                                                                                                                                                                                                                   |\n    | ------------------ | -------- | ---------------------------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n    | `componentClass`   |          | `string`                                             |                  | Space-separated class list to attach to component wrapper element `\u003cdiv\u003e`                                                                                                                                                     |\n    | `description`      |          | `string`                                             |                  | Text content to display in component description element `\u003cp\u003e`                                                                                                                                                                |\n    | `descriptionClass` |          | `string`                                             |                  | Space-separated class list to attach to description element `\u003cp\u003e`                                                                                                                                                             |\n    | `events`           |          | `array` or\u003cbr\u003e`object`                               |                  | Key, value pairs where: `key` defines a valid `HTMLElement` or `HTMLInputElement` [`Event`](https://developer.mozilla.org/en-US/docs/Web/Events); and `value` defines a `callbackFn`. See [events](#events).                  |\n    | `groupClass`       |          | `string`                                             |                  | Space-separated class list to attach to group wrapper element `\u003cdiv\u003e`                                                                                                                                                         |\n    | `groupKey`         |          | `string`                                             | group`_groupKey` | Use as group key (identifier).                                                                                                                                                                                                |\n    | `heading`          |          | `string`                                             |                  | Text content to display in component description element `\u003ch3\u003e`                                                                                                                                                               |\n    | `headingClass`     |          | `string`                                             |                  | Space-separated class list to attach to heading element `\u003ch3\u003e`                                                                                                                                                                |\n    | `props`            |          | `array` or\u003cbr\u003e`object`                               |                  | Immutable attributes to set on `HTMLInputElement`. Defined by key, value pairs. Underscore keys will be accessible through instance but not set to element. See [props](#props).                                              |\n    | `state`            | \u0026#10004; | `array` or\u003cbr\u003e`object` or\u003cbr\u003e`string` or\u003cbr\u003e`number` |                  | Mutable key, value pairs for `HTMLInputElement` which are parsed and written to JSON file. A passed `object` must have a minimum `value` key with a set value pair.\u003cbr\u003ee.g. `{ value: 'hello world!' }`. See [state](#state). |\n\n### Returns\n\n-   Inputs group component wrapped in a `HTMLDivElement`.\n\n---\n\n### **`removableInputsGroup()`**\n\n### Syntax\n\n```js\nFSFormGroup.removableInputsGroup(options);\n```\n\n### Parameters\n\n`options`\n\n-   | Type    | Required | Description                                        |\n    | ------- | -------- | -------------------------------------------------- |\n    | `array` | \u0026#10004; | Array of objects containing _n_ component options. |\n\n-   | Property           | Required | Type                                                 | Default                                                          | Description                                                                                                                                                                                                                   |\n    | ------------------ | -------- | ---------------------------------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n    | `addNewButton`     |          | `object`                                             | `addNewButton.value: 'Add New'`\u003cbr\u003e`addNewButton.type: 'button'` | Props for the button that adds a new input component button                                                                                                                                                                   |\n    | `componentClass`   |          | `string`                                             |                                                                  | Space-separated class list to attach to component wrapper element `\u003cdiv\u003e`                                                                                                                                                     |\n    | `description`      |          | `string`                                             |                                                                  | Text content to display in component description element `\u003cp\u003e`                                                                                                                                                                |\n    | `descriptionClass` |          | `string`                                             |                                                                  | Space-separated class list to attach to description element `\u003cp\u003e`                                                                                                                                                             |\n    | `events`           |          | `array` or\u003cbr\u003e`object`                               |                                                                  | Key, value pairs where: `key` defines a valid `HTMLElement` or `HTMLInputElement` [`Event`](https://developer.mozilla.org/en-US/docs/Web/Events); and `value` defines a `callbackFn`. See [events](#events).                  |\n    | `groupClass`       |          | `string`                                             |                                                                  | Space-separated class list to attach to group wrapper element `\u003cdiv\u003e`                                                                                                                                                         |\n    | `groupKey`         |          | `string`                                             | group`_groupKey`                                                 | Use as group key (identifier).                                                                                                                                                                                                |\n    | `heading`          |          | `string`                                             |                                                                  | Text content to display in component description element `\u003ch3\u003e`                                                                                                                                                               |\n    | `headingClass`     |          | `string`                                             |                                                                  | Space-separated class list to attach to heading element `\u003ch3\u003e`                                                                                                                                                                |\n    | `max`              |          | `number`                                             | `10`                                                             | A number defining the maximum number of input components allowed                                                                                                                                                              |\n    | `props`            |          | `array` or\u003cbr\u003e`object`                               |                                                                  | Immutable attributes to set on `HTMLInputElement`. Defined by key, value pairs. Underscore keys will be accessible through instance but not set to element. See [props](#props).                                              |\n    | `removeButton`     |          | `object`                                             | `removeButton.value: 'x'`\u003cbr\u003e`removeButton.type: 'button'`       | Props for the button that removes an input component button                                                                                                                                                                   |\n    | `state`            | \u0026#10004; | `array` or\u003cbr\u003e`object` or\u003cbr\u003e`string` or\u003cbr\u003e`number` |                                                                  | Mutable key, value pairs for `HTMLInputElement` which are parsed and written to JSON file. A passed `object` must have a minimum `value` key with a set value pair.\u003cbr\u003ee.g. `{ value: 'hello world!' }`. See [state](#state). |\n\n### Returns\n\n-   Removable inputs group component wrapped in a `HTMLDivElement`.\n\n## Reference\n\n-   ### **events**\n\n    -   **keys**\n        -   [`...EventTypes`](https://developer.mozilla.org/en-US/docs/Web/Events)\n            -   Popular: `input`, `change`, `keydown`, `keyup`, `focus`, `blur`\n    -   **values**\n\n        -   `callbackFn`\n\n        ```js\n        // Callback function\n        function eventHandler(event, instance) {\n            // instance: {\n            //     element: HTMLInputElement,\n            //     props: { //... },\n            //     state: { value: //... }\n            //     setState: ƒ setState(newState) {\n            //         // Method to update file instance\n            //         // Returns newState\n            //     }\n            // }\n        }\n        ```\n\n        -   File state only updates when `instance.setState()` is called, thus if not called on element modification, UI and file state could be out of sync.\n\n    -   **examples**\n\n        -   Filter element value on input event and update/sync the UI with the file instance.\n\n        ```js\n        events: {\n            input: (event, instance) =\u003e {\n                const state = { ...instance.state };\n                // Filter value on input event\n                state.value = event.target.value.slice().replace(/[^a-zA-z]*/g, '');\n                // Set and sync UI and I/O states\n                event.target.value = instance.setState({ ...state }).value;\n            },\n        }\n        ```\n\n        -   Filter element using hidden prop key for multiple FSFormGroup components and event types and update/sync the UI with the file instance.\n\n        ```js\n        function filterAndHandleEvent(event, instance) {\n            // Create regex pattern using hidden props\n            const regex = new RegExp(instance.props._regex, 'g');\n            // Shallow copy state\n            const state = { ...instance.state };\n            // Filter value on input event using passed regex pattern\n            state.value = event.target.value.slice().replace(regex, '');\n            // Set and sync UI and I/O states\n            event.target.value = instance.setState({ ...state }).value;\n        }\n\n        // First FSFormGroup.inputGroup() instance\n        props: { _regex: '[^a-zA-z]*', }, // Only allow letters\n        events: {\n            input: { filterAndHandleEvent },\n            change: { filterAndHandleEvent },\n        }\n\n        // Second FSFormGroup.inputGroup() instance\n        props: { _regex: '[^0-9]*', }, // Only allow numbers\n        events: {\n            input: { filterAndHandleEvent },\n            change: { filterAndHandleEvent },\n        }\n        ```\n\n-   ### **props**\n\n    -   **keys**\n        -   [`...DOMString`](https://developer.mozilla.org/en-US/docs/Web/API/DOMString)\n            -   Popular: `type`, `class`, `name`\n            -   Default: `type=\"text\"`\n        -   `...string` (hidden)\n            -   A key name beginning with an underscore character is regarded as a hidden prop, and is not attached to `element`, but can be accessed via `callbackFn.instance.props`\n            -   Example:\n                -   Define: `props: { _regex: '[^0-9*]' }`\n                -   Use: `instance.props._regex`\n    -   **values**\n\n        -   [`...DOMString`](https://developer.mozilla.org/en-US/docs/Web/API/DOMString)\n        -   `...any` (hidden)\n\n    -   **examples**\n\n        -   Filter element using hidden prop key for multiple FSFormGroup components and event types and update/sync the UI with the file instance.\n\n        ```js\n        props: {\n            type: 'text',\n            tabindex: '-1',\n            class: 'bg-transparent text-white border-0',\n            _regex: '[^0-9]*', // Hidden\n        },\n        state: {\n            value: 'Initial text value'\n        }\n\n        // Returns HTMLElement\n        // \u003cdiv\u003e\n        //     \u003cdiv\u003e\n        //         \u003cinput\n        //             type=\"text\"\n        //             tabindex=\"-1\"\n        //             class=\"bg-transparent text-white border-0\"\n        //             value=\"Initial text value\"\n        //         /\u003e\n        //     \u003c/div\u003e\n        // \u003c/div\u003e\n        ```\n\n-   ### **state**\n\n    -   **keys**\n        -   `value` (required)\n            -   Initial value to set for `element`\n        -   `...string`\n    -   **values**\n\n        -   `...any`\n\n    -   **type**\n\n        -   Input:\n\n            -   `[ { value: (string,number) } ]`\n            -   `{ value: (string,number) }`\n            -   `string`\n            -   `number`\n\n        -   Output:\n            -   `[ { value: (string,number) } ]`\n\n    -   **examples**\n\n        -\n\n        ```js\n        props: {\n            type: 'text',\n            tabindex: '-1',\n            class: 'bg-transparent text-white border-0',\n            _regex: '[^0-9]*', // Hidden\n        },\n        state: {\n            value: 'Initial text value'\n        }\n\n        // Returns HTMLElement\n        // \u003cdiv\u003e\n        //     \u003cdiv\u003e\n        //         \u003cinput\n        //             type=\"text\"\n        //             tabindex=\"-1\"\n        //             class=\"bg-transparent text-white border-0\"\n        //             value=\"Initial text value\"\n        //         /\u003e\n        //     \u003c/div\u003e\n        // \u003c/div\u003e\n\n        // JSON file instance\n        // { \"group0\": [{ \"value\": \"Initial text value\" }] }\n        ```\n\n## **Examples**\n\n### Use saved JSON states; otherwise use default states.\n\n```js\n/**\n * Check if a file exists and use existing saved states.\n */\n(function useSavedFileStates() {\n    // A global object of all FSFormGroup options, with default states\n    const groups = {\n        group0: {\n            groupKey: 'group0',\n            heading: 'Group 0',\n            description: 'Input groups.',\n            props: { type: 'text' },\n            // Default states\n            state: [{ value: 'hello' }, { value: 'world!' }],\n            events: { input: inputEventHandler },\n        },\n        group1: {\n            groupKey: 'group1',\n            heading: 'Group 1',\n            description: 'Removable input groups.',\n            max: 10,\n            props: { type: 'number' },\n            // Default states\n            state: [{ value: 5 }, { value: 10 }, { value: 15 }],\n            events: { input: inputEventHandler },\n        },\n    };\n    try {\n        /**\n         * Test if passed property/key exists in the parent object\n         * and has at least one child object.\n         * @param {Object} parent Parent object path.\n         * @param {string} test Property name.\n         * @returns {boolean} True if the property exists.\n         */\n        const propertyExists = (parent, test) =\u003e {\n            if (parent.hasOwnProperty(test) \u0026\u0026 parent[test] \u0026\u0026 Object.keys(parent[test]).length \u003e 0) {\n                return true;\n            }\n            return false;\n        };\n\n        // Try to read and parse the file as a json object\n        const objFile = JSON.parse(fs.readFileSync(jsonFilePath, 'utf8'));\n\n        // Update to use existing file states\n        for (const groupKey in groups) {\n            if (propertyExists(objFile, groupKey) === true) {\n                groups[groupKey].state = objFile[groupKey].slice();\n            }\n        }\n    } catch (err) {}\n})();\n```\n\n### Electron Example.\n\n#### **main.js**\n\n```js\n// Imports\nconst { app, BrowserWindow } = require('electron');\n\n/**\n * Initialise main render\n */\nfunction createWindow() {\n    const win = new BrowserWindow({\n        show: false,\n        autoHideMenuBar: true,\n        title: 'FSFormGroup',\n        width: 320,\n        height: 544,\n        minWidth: 320,\n        minHeight: 544,\n        webPreferences: {\n            contextIsolation: false,\n            nodeIntegration: true,\n            enableRemoteModule: true,\n        },\n    });\n\n    // Pretty start\n    win.once('ready-to-show', () =\u003e {\n        win.show();\n    });\n\n    // Load file\n    win.loadFile('Public/index.html');\n\n    // win.webContents.openDevTools(); // Show WebDev Tools\n}\n\napp.whenReady().then(() =\u003e {\n    createWindow();\n\n    app.on('activate', () =\u003e {\n        if (BrowserWindow.getAllWindows().length === 0) {\n            createWindow();\n        }\n    });\n});\n\napp.on('window-all-closed', () =\u003e {\n    if (process.platform !== 'darwin') {\n        app.quit();\n    }\n});\n```\n\n#### **Public/index.html**\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"en\"\u003e\n    \u003chead\u003e\n        \u003cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\" /\u003e\n        \u003clink rel=\"stylesheet\" href=\"css/app.css\" /\u003e\n        \u003cmeta charset=\"UTF-8\" /\u003e\n        \u003cmeta name=\"author\" content=\"fatherbrennan\" /\u003e\n        \u003ctitle\u003eFS Module Form Group\u003c/title\u003e\n        \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" /\u003e\n        \u003cmeta http-equiv=\"Content-Security-Policy\" content=\"default-src 'self';script-src 'self'\" /\u003e\n    \u003c/head\u003e\n\n    \u003cbody\u003e\n        \u003cdiv id=\"form\"\u003e\u003c/div\u003e\n        \u003cscript src=\"js/app.js\"\u003e\u003c/script\u003e\n    \u003c/body\u003e\n\u003c/html\u003e\n```\n\n#### **Public/js/app.js**\n\n```js\n// Dependencies\nconst fs = require('fs');\nconst path = require('path');\nconst FSFormGroup = require('fs-form-group');\n\n/**\n * Initalise application preferences variable.\n * -\u003e Saves states to JSON file.\n * -\u003e Reads JSON file on application startup; uses saved states or fallback to default states.\n * -\u003e Creates an interactive HTML form for user input; and save valid updates to JSON file.\n * -\u003e HTML form attaches feedback block on invalid user input.\n */\nconst preferences = (function initPreferences() {\n    const jsonPath = path.join(__dirname, 'src', 'Preferences', 'preferences.json');\n\n    /**\n     * Feedback block handler for active component.\n     * @property {node|element} element New component state defined by user input.\n     * @property {node} parentNode Initial state of component.\n     * @property {function} removeFeedback Remove feedback class to feedback block.\n     * @property {function} setFeedback Add feedback class to feedback block.\n     * @property {function} init Cache elements and insert feedback block into the DOM.\n     * @property {function} reset Reset all properties to default and remove feedback block from the DOM.\n     */\n    const feedback = {\n        element: null,\n        parentNode: null,\n        removeFeedback: function (feedback) {\n            this.element.classList.remove(feedback);\n            return true;\n        },\n        setFeedback: function (feedback) {\n            this.element.classList.add(feedback);\n            return false;\n        },\n        init: function (element) {\n            this.element = (function () {\n                const div = document.createElement('div');\n                div.setAttribute('class', 'fs-fg-feedback');\n                return div;\n            })();\n            this.parentNode = element.parentNode;\n            this.parentNode.insertBefore(this.element, element);\n        },\n        reset: function () {\n            this.parentNode.removeChild(this.element);\n            this.parentNode = null;\n            this.element = null;\n        },\n    };\n\n    /**\n     * Track focused component state (for validity)\n     * @property {object} new New component state defined by user input.\n     * @property {object} old Initial state of component.\n     * @property {boolean} valid True if `new` is valid.\n     * @property {function} reset Reset all properties to default.\n     */\n    const activeState = {\n        new: null,\n        old: null,\n        valid: true,\n        reset: function () {\n            this.new = null;\n            this.old = null;\n            this.valid = true;\n        },\n    };\n\n    /**\n     * Filter user input to allow integers only.\n     * @param {event} e HTML Event.\n     * @returns {number} Input value as an integer.\n     */\n    function intFilter(e) {\n        const a = e.target.value.slice().replace(/\\D*/g, '');\n        // Set UI\n        e.target.value = a;\n        // Feedback\n        if (a === '') {\n            feedback.setFeedback('nan');\n            activeState.valid = false;\n            return a;\n        } else feedback.removeFeedback('nan');\n        return +a;\n    }\n\n    function inputEventHandler(event, instance) {\n        const state = { ...instance.state };\n        // Assume valid state before filtering\n        activeState.valid = true;\n        // Filter and sync UI/file states\n        state.value = instance.props.hasOwnProperty('_filter') ? instance.props._filter(event) : event.target.value;\n        // Set new states\n        activeState.new = instance.setState({ ...state });\n    }\n\n    function focusEventHandler(event, instance) {\n        // Cache initial component state\n        activeState.old = { ...instance.state };\n        // Initialise feedback node for active component\n        feedback.init(instance.element);\n        // Filter\n        if (instance.props.hasOwnProperty('_filter')) instance.props._filter(event);\n    }\n\n    function blurEventHandler(event, instance) {\n        // Revert to old valid state (and sync component state with UI state)\n        if (activeState.valid === false) event.target.value = instance.setState({ ...activeState.old }).value;\n        // Reset defaults\n        feedback.reset();\n        activeState.reset();\n    }\n\n    const groups = {\n        group0: {\n            groupKey: 'group0',\n            heading: 'Group 0',\n            description: 'Only allow integer input',\n            max: 4,\n            props: {\n                type: 'text',\n                placeholder: 'Integer.',\n                _filter: intFilter,\n            },\n            // Default\n            state: 1,\n            events: {\n                blur: blurEventHandler,\n                focus: focusEventHandler,\n                input: inputEventHandler,\n            },\n        },\n        group1: {\n            groupKey: 'group1',\n            heading: 'Group 1',\n            max: 8,\n            props: {\n                type: 'text',\n                placeholder: 'Text',\n            },\n            // Default\n            state: [{ value: 'hello' }, { value: 'world!' }],\n            events: {\n                blur: blurEventHandler,\n                focus: focusEventHandler,\n                input: inputEventHandler,\n            },\n        },\n    };\n\n    /**\n     *\n     * Check if a file exists and use saved states if they exist.\n     */\n    (function useSavedFileStates() {\n        try {\n            /**\n             * Test if passed property/key exists in the parent object\n             * and has at least one child object.\n             * @param {object} parent Parent object path.\n             * @param {string} test Property name.\n             * @returns {boolean} True if the property exists.\n             */\n            const propertyExists = (parent, test) =\u003e {\n                if (parent.hasOwnProperty(test) \u0026\u0026 parent[test] \u0026\u0026 Object.keys(parent[test]).length \u003e 0) {\n                    return true;\n                }\n                return false;\n            };\n\n            // Try to read and parse the file as a json object\n            const json = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));\n\n            // Use existing file states\n            for (const groupKey in groups) {\n                if (propertyExists(json, groupKey) === true) {\n                    groups[groupKey].state = json[groupKey].slice();\n                }\n            }\n        } catch (e) {}\n    })();\n\n    const form = new FSFormGroup(jsonPath, null, {\n        formGroup: 'fs-fg',\n        formGroupHeading: 'fs-fg-heading',\n        formGroupDescription: 'fs-fg-description',\n        formGroupComponent: 'fs-fg-component',\n        formGroupInput: 'fs-fg-input',\n    });\n\n    const f = new DocumentFragment();\n\n    // Build document fragment with removable input form groups\n    for (const groupKey in groups) {\n        f.appendChild(form.removableInputsGroup(groups[groupKey]));\n    }\n\n    // Insert form fragment in DOM\n    document.getElementById('form').appendChild(f);\n\n    return form;\n})();\n\n// View latest preferences states\nconsole.log(preferences.getData());\n```\n\n#### **Public/css/app.css**\n\n```css\n:root {\n    --primary-color: #fff;\n    --secondary-color: #dfdfdf;\n    --primary-background: #1e1e1e;\n    --secondary-background: #2c2c2c;\n    --tertiary-background: #3c3c3c;\n    --active-primary: #008080;\n    --error: #f08080;\n    --highlight: #00ffff77;\n}\n::-webkit-scrollbar {\n    display: none;\n}\n::selection {\n    background-color: var(--highlight);\n}\n*,\n:before,\n:after {\n    box-sizing: border-box;\n}\nbody {\n    font-family: sans-serif;\n    color: var(--primary-color);\n    background-color: var(--primary-background);\n    color: #00ffff;\n}\ninput::-webkit-outer-spin-button,\ninput::-webkit-inner-spin-button {\n    -webkit-appearance: none;\n    margin: 0;\n}\n/* Form Group CSS */\n.fs-fg {\n    font-size: 0.75rem;\n    color: var(--secondary-color);\n    margin-bottom: 0.75em;\n    padding: 0.25em;\n    min-width: 100%;\n    max-width: fit-content;\n    line-height: 1em;\n}\n.fs-fg:hover {\n    background-color: var(--secondary-background);\n}\n.fs-fg-heading {\n    font-size: inherit;\n    font-weight: 600;\n    margin-top: 0;\n}\n.fs-fg-description {\n    font-weight: 300;\n}\n.fs-fg-component {\n    position: relative;\n    display: flex;\n    flex-direction: row;\n}\n.fs-fg-input[type='button'] {\n    background-color: var(--active-primary);\n    max-width: max-content;\n}\n.fs-fg-input:focus {\n    border-color: var(--active-primary);\n}\n.fs-fg-feedback:after {\n    background-color: var(--error);\n    border-bottom-left-radius: 0.25em;\n    border-bottom-right-radius: 0.25em;\n    bottom: -1.25em;\n    left: 0;\n    padding-left: 0.25em;\n    padding-right: 0.25em;\n    padding-bottom: 0.25em;\n    position: absolute;\n    z-index: 2;\n}\n.fs-fg-feedback.nan:after {\n    content: 'NaN';\n}\n.fs-fg-feedback.nan + .fs-fg-input {\n    border-color: var(--error);\n}\n.fs-fg-input {\n    background-color: transparent;\n    border-width: 0.1em;\n    border-style: solid;\n    border-color: var(--tertiary-background);\n    color: var(--secondary-color);\n    outline: 0;\n    width: 100%;\n}\n.fs-fg-component,\n.fs-fg-description,\n.fs-fg-heading {\n    margin-bottom: 0.25em;\n}\n```\n\n#### **Public/src/Preferences/preferences.json**\n\n```json\n{ \"group0\": [{ \"value\": 1 }], \"group1\": [{ \"value\": \"hello\" }, { \"value\": \"world!\" }] }\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffatherbrennan%2Ffs-form-group","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffatherbrennan%2Ffs-form-group","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffatherbrennan%2Ffs-form-group/lists"}