├── .babelrc ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── aToB.PNG ├── composition.PNG ├── generateForm.PNG ├── index.html ├── package.json ├── processInputs.PNG ├── processLists.PNG ├── src ├── Example.jsx ├── data │ └── cities.json ├── generator │ ├── Lookup_Component.jsx │ └── helpers.js ├── index.js ├── routes.jsx ├── schema.json ├── styles.css └── transformers.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ], 6 | "plugins": [] 7 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Dipen Bagia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # declarative-form-generator 2 | A simple react form generator using functional programming concepts. 3 | An explanation of how it works is being drafted! 4 | 5 | ## How to run 6 | 7 | 1. `git clone https://github.com/dbagia/declarative-form-generator.git` 8 | 2. `yarn` 9 | 3. `yarn start` 10 | 4. `Go to http://localhost:8080` 11 | 12 | You should see a form rendered in the browser with some fields. Keying in any text or changing the drop-down value should log the entered data in the console. 13 | 14 | ## How does it work? 15 | The core idea behind the form generator is to use a schema to render form in the UI. 16 | 17 | The form schema is a JSON structure, an array of objects, each representing an input field on the form. 18 | 19 | The generator takes this JSON structure as an input and returns a list of React Components, each corresponding to the object in the JSON structure at that position. 20 | 21 | ## The Concept 22 | The generator has been built using a simple concept from Category Theory in order to render the form from an array of input fields. 23 | 24 | Below is an example of form schema: 25 | 26 | ``` 27 | [ 28 | { 29 | "type": "text", 30 | "label": "First Name", 31 | "required": true, 32 | "placeholder": "first name", 33 | "readOnly": false, 34 | "name": "fname" 35 | }, 36 | { 37 | "type": "list", 38 | "lookupId": "id", 39 | "lookupDisplay": "name", 40 | "lookupUrl": "http://localhost:8080/cities", 41 | "defaultValue": 2, 42 | "readOnly": false, 43 | "required": true, 44 | "label": "City", 45 | "name": "city", 46 | "placeholder": "city" 47 | }, 48 | { 49 | "type": "text", 50 | "label": "Last Name", 51 | "required": true, 52 | "placeholder": "last name", 53 | "defaultValue": "x", 54 | "readOnly": false, 55 | "name": "lname" 56 | } 57 | ] 58 | ``` 59 | 60 | The responsibility of the generator is to transform the above form schema into an array of ```React/JSX``` elements like so: 61 | 62 | ``` 63 | [ 64 |
65 | 66 | 73 | /> 74 |
, 75 | ... 76 | ] 77 | ``` 78 | The process of this transformation has been developed using ```composition```, ```currying``` and ```Monads``` by using [Crocks Library](https://github.com/evilsoft/crocks). 79 | 80 | ## A bit of Category Theory 81 | 82 | This section explains the above transformation process using diagrams. 83 | 84 | Let's first draw the final diagram and then we will zoom into the details. 85 | 86 | ![A map from JSON to React Elements](/generateForm.PNG) 87 | 88 | This diagram is depicting two sets A and B and a map *```f```* which performs the transformation from A to B. Set A is the list of JSON objects, the schema of the form and Set B is the list of React Elements. 89 | 90 | The map *```f```* is however a composition and we can expand this diagram to describe the actual design of this transformation. 91 | 92 | Starting from the form schema, as stated, it is a list of input fields. We can define transformations for individual field types (text, select, radio, checkbox etc) and compose all of them together to get to our final *```f```*. 93 | 94 | Let's take the input text example. 95 | 96 | ### Transforming text types 97 | 98 | We want to transform all the fields in the form schema which are of type ```text```. So after the transformation, we should have a set consisting of all the fields in form schema with fields with type ```text``` transformed into it's JSX equivalents. Let's call this set M. 99 | 100 | ![map processInputs](/processInputs.PNG) 101 | 102 | In the above diagram, we have two sets ```A``` and ```M``` and a map *```textInputs```* which performs the trainsition from set ```A``` to set ```M```. 103 | 104 | Notice that the map *```textInputs```* is only transforming those elements in set ```A``` whose type is ```text```. It is not transorming any other types. This has been indicated by black arrows (performing transformation) and red arrows (bypassing without transforming) in the diagram. 105 | 106 | ### Transforming list types 107 | 108 | Next, we want to transform all the fields in the form schema which are of type ```list``` to JSX ```select``` elements. We have a map called *```lists```* to do this. 109 | 110 | ![map processLists](/processLists.PNG) 111 | 112 | As you might have noticed, our map *```lists```* does not perform any transformations on ```input``` JSX elements. 113 | 114 | ### Transforming other types 115 | 116 | We can continue creating additional maps for other types like radio buttons, checkbox, file upload etc and continue to perform transformations on our set. 117 | 118 | ### Putting it all together 119 | 120 | Below is how a final transformation will look like starting from Set A to Set B. 121 | 122 | ![map processInputs and processLists](/aToB.PNG) 123 | 124 | The laws of Category Theory allow us to combine more than one maps through ```composition```. Hence, if we use composition on the above diagram, we can combine the maps *```textInputs```* and *```lists```* into a single map like so: 125 | 126 | ![map composition](/composition.PNG) 127 | 128 | The above diagram is the same as we started with: (map *```f```* ). 129 | 130 | ## Thinking Declaratively 131 | 132 | I have spent a lot of time trying to figure this out. The most useful resource I have found so far is Kevlin Henney's [talk](https://www.youtube.com/watch?v=NSzsYWckGd4) on declarative thinking. 133 | 134 | In a declarative/functional paradigm, you tell the computer what you need rather than how you want the computer to get it. The "how" part is left for the computer to decide. 135 | 136 | But how can we actually describe the "what" part in programming? 137 | 138 | **Describe the properties of the domain/problem** 139 | 140 | For instance, consider the problem of [balanced brackets](https://www.hackerrank.com/challenges/balanced-brackets/problem). One of the imperative ways of solving this problem is using Stack Data Structure. 141 | 142 | Using the declarative approach, the properties of this problem are as below: 143 | 144 | 1. The input is a string of variable length and only allows {, [, (, }, ] and ) 145 | 146 | 2. If the input length is odd, it is unbalanced (it is just not possible to have balanced brackets with odd number of characters) 147 | 148 | 3. If the input length is even, then for every opening brace of type (, { or [ there is an equivalent closing brace at a distance double the length of other opening braces after the current opening brace 149 | 150 | You can have a look at the declarative solution [here](https://github.com/dbagia/declarative-demos/tree/master/demos/balanced-brackets) 151 | 152 | ## Container Style Programming 153 | ### ... 154 | 155 | ## References 156 | 157 | * [Prof Frisby's Mostly Adequate Guide to Functional Programming](https://drboolean.gitbooks.io/mostly-adequate-guide/) 158 | * [Declarative Thinking](https://www.youtube.com/watch?v=NSzsYWckGd4) 159 | * [Crocks Library](https://evilsoft.github.io/crocks/docs/crocks/) 160 | -------------------------------------------------------------------------------- /aToB.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbagia/declarative-form-generator/7eaf35d246596f9e10e206dbd4dec848b30424f0/aToB.PNG -------------------------------------------------------------------------------- /composition.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbagia/declarative-form-generator/7eaf35d246596f9e10e206dbd4dec848b30424f0/composition.PNG -------------------------------------------------------------------------------- /generateForm.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbagia/declarative-form-generator/7eaf35d246596f9e10e206dbd4dec848b30424f0/generateForm.PNG -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | Declarative Form Generator 3 | 4 | 5 |
6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "declarative-form-generator", 3 | "version": "1.0.0", 4 | "description": "A simple react form generator using functional programming concepts", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --hot --colors", 8 | "build": "webpack", 9 | "test": "standard && jest" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/dbagia/declarative-form-generator.git" 14 | }, 15 | "keywords": [ 16 | "declarative", 17 | "react" 18 | ], 19 | "author": "Dipen Bagia (dipen.bagia@outlook.com)", 20 | "license": "Apache-2.0", 21 | "bugs": { 22 | "url": "https://github.com/dbagia/declarative-form-generator/issues" 23 | }, 24 | "homepage": "https://github.com/dbagia/declarative-form-generator#readme", 25 | "dependencies": { 26 | "axios": "0.21.1", 27 | "basscss": "8.1.0", 28 | "crocks": "0.12.4", 29 | "ramda": "0.27.0", 30 | "react": "16.13.0", 31 | "react-dom": "16.13.0", 32 | "react-router-dom": "5.1.2" 33 | }, 34 | "devDependencies": { 35 | "@babel/preset-env": "7.8.7", 36 | "@babel/preset-react": "7.8.3", 37 | "babel-jest": "25.1.0", 38 | "babel-loader": "8.0.6", 39 | "css-loader": "3.4.2", 40 | "jest": "25.1.0", 41 | "react-test-renderer": "16.13.0", 42 | "standard": "14.3.3", 43 | "style-loader": "1.1.3", 44 | "webpack": "4.42.0", 45 | "webpack-cli": "3.3.11", 46 | "webpack-dev-server": "3.10.3" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /processInputs.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbagia/declarative-form-generator/7eaf35d246596f9e10e206dbd4dec848b30424f0/processInputs.PNG -------------------------------------------------------------------------------- /processLists.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbagia/declarative-form-generator/7eaf35d246596f9e10e206dbd4dec848b30424f0/processLists.PNG -------------------------------------------------------------------------------- /src/Example.jsx: -------------------------------------------------------------------------------- 1 | import schema from './schema.json' 2 | import List from 'crocks/List' 3 | import when from 'crocks/logic/when' 4 | import compose from 'crocks/helpers/compose' 5 | import {isSchemaItemOfType} from './generator/helpers' 6 | import { listToReact, textToReact, addChangeListener } from './transformers' 7 | 8 | // textInputs:: (Pred -> ()) 9 | const textInputs = 10 | when(isSchemaItemOfType('text'), textToReact) 11 | 12 | // lists:: (Pred -> ()) 13 | const lists = 14 | when(isSchemaItemOfType('list'), listToReact) 15 | 16 | // transform:: a -> JSX 17 | const transform = 18 | compose(lists, textInputs, addChangeListener({})) 19 | 20 | const Example = 21 | List 22 | .fromArray(schema) 23 | .map(transform) 24 | 25 | export default Example 26 | -------------------------------------------------------------------------------- /src/data/cities.json: -------------------------------------------------------------------------------- 1 | [{"id":"CTY:1","name":"Mumbai"},{"id":"CTY:2","name":"Hyderabad"},{"id":"CTY:3","name":"Chennai"},{"id":"CTY:4","name":"Delhi"},{"id":"CTY:5","name":"Bangaluru"},{"id":"CTY:7","name":"Kolkatta"}] -------------------------------------------------------------------------------- /src/generator/Lookup_Component.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import { getData, toOptions } from './helpers' 3 | 4 | export default function LookupComponent(props) { 5 | const [options, setOptions] = useState([]) 6 | 7 | useEffect(() => { 8 | getData(props.field.lookupUrl) 9 | .map(r => r.data) 10 | .map(toOptions) 11 | .fork( 12 | err => console.log('err', err), setOptions 13 | ) 14 | }) 15 | 16 | return ( 17 |
18 | 21 | 29 |
30 | ) 31 | } 32 | 33 | -------------------------------------------------------------------------------- /src/generator/helpers.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { fromPromise } from 'crocks/Async' 3 | import axios from 'axios' 4 | import curry from 'crocks/helpers/curry' 5 | import IO from 'crocks/IO' 6 | import propOr from 'crocks/helpers/propOr' 7 | 8 | // defaultProp:: String -> (String -> String) 9 | const defaultProp = propOr('text') 10 | 11 | export const asyncAxiosGet = 12 | fromPromise(axios.get) 13 | 14 | // getData:: String -> Async 15 | export const getData = 16 | url => asyncAxiosGet(url) 17 | 18 | // toOptions:: [a] -> jsx 19 | export const toOptions = options => 20 | options 21 | .map((option, i) => 22 | 23 | ) 24 | 25 | export const $ = selector => 26 | IO.of(document.querySelectorAll(selector)) 27 | 28 | // isSchemaItemOfType:: String -> a -> Boolean 29 | export const isSchemaItemOfType = 30 | curry((type, schemaItem) => 31 | defaultProp('type', schemaItem) === type) 32 | 33 | // unsafe log:: a -> a 34 | export const log = o => { 35 | console.log(o) 36 | return o 37 | } 38 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import 'basscss/css/basscss.min.css' 2 | import React from 'react' 3 | import ReactDOM from 'react-dom' 4 | import { HashRouter as Router } from 'react-router-dom' 5 | import Routes from './routes.jsx' 6 | import './styles.css' 7 | import {$} from './generator/helpers' 8 | 9 | $('#app') 10 | .map(x => x[0]) 11 | .map(container => { 12 | ReactDOM.render( 13 | 14 | {Routes.run()} 15 | , 16 | container 17 | ) 18 | }) 19 | .run() 20 | -------------------------------------------------------------------------------- /src/routes.jsx: -------------------------------------------------------------------------------- 1 | import { Route } from 'react-router-dom' 2 | import Example from './Example.jsx' 3 | import React from 'react' 4 | import IO from 'crocks/IO' 5 | 6 | const Routes = IO.of( 7 |
8 | Example.toArray()} /> 9 |
10 | ) 11 | 12 | export default Routes 13 | -------------------------------------------------------------------------------- /src/schema.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "text", 4 | "label": "First Name", 5 | "required": true, 6 | "placeholder": "first name", 7 | "defaultValue": "x", 8 | "readOnly": false, 9 | "name": "fname" 10 | }, 11 | { 12 | "type": "list", 13 | "lookupId": "id", 14 | "lookupDisplay": "name", 15 | "lookupUrl": "http://localhost:8080/cities", 16 | "defaultValue": 2, 17 | "readOnly": false, 18 | "required": true, 19 | "label": "City", 20 | "name": "city", 21 | "placeholder": "city" 22 | }, 23 | { 24 | "type": "text", 25 | "label": "Last Name", 26 | "required": true, 27 | "placeholder": "last name", 28 | "defaultValue": "x", 29 | "readOnly": false, 30 | "name": "lname" 31 | }, 32 | { 33 | "label": "Country", 34 | "required": true, 35 | "placeholder": "Country", 36 | "defaultValue": "x", 37 | "readOnly": false, 38 | "name": "country" 39 | } 40 | ] -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Roboto', sans-serif; 3 | } 4 | 5 | label { 6 | display: block; 7 | } -------------------------------------------------------------------------------- /src/transformers.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import curry from 'crocks/helpers/curry' 3 | import assign from 'crocks/helpers/assign' 4 | import LookupComponent from './generator/Lookup_Component.jsx' 5 | 6 | export const textToReact = item => 7 |
8 | 9 | 17 |
18 | 19 | export const listToReact = item => 20 | 21 | 22 | export const addChangeListener = curry((userInput, item) => 23 | assign(item, { 24 | onChange: event => { 25 | userInput[item.name] = event.target.value 26 | console.log(userInput) 27 | } 28 | })) 29 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpack = require('webpack') 3 | var cities = require('./src/data/cities.json') 4 | module.exports = { 5 | entry: './src', 6 | output: { 7 | filename: 'bundle.js', 8 | path: path.join(__dirname, '/') 9 | }, 10 | devtool: 'inline-source-map', 11 | devServer: { 12 | inline: true, 13 | port: 8080, 14 | historyApiFallback: true, 15 | before: function (app) { 16 | app.get('/cities', function (req, res) { 17 | res.send(cities) 18 | }) 19 | } 20 | }, 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.css$/, 25 | use: ['style-loader', 'css-loader'] 26 | }, 27 | { 28 | test: /\.js[x]?$/, 29 | exclude: '/node_modules/', 30 | use: { 31 | loader: 'babel-loader', 32 | options: { 33 | presets: [] 34 | } 35 | } 36 | } 37 | 38 | ] 39 | } 40 | } 41 | --------------------------------------------------------------------------------