Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@jslabs/react-forms

Package Overview
Dependencies
Maintainers
1
Versions
15
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@jslabs/react-forms

Abstract React forms with JSON Schema support (Ajv) including form generation and server-side validation.

  • 0.1.5-alpha.5
  • latest
  • Source
  • npm
  • Socket score

Version published
Maintainers
1
Created
Source

@jslabs/react-forms

Abstract React forms with JSON Schema support (Ajv) including form generation and server-side validation.

Premilinary docs (examples)

Ajv JSON Schema

// server/schemas/example.js
module.exports = {
    "additionalProperties": false,
    "type": "object",
    "properties": {
        "checkbox_field": {
            "type": "array",
            "items": { 
                "type": "integer",
            },
        },
        // "checkbox_field_1": {
        //     "type": "boolean"
        // },
        // "checkbox_field_2": {
        //     "type": "boolean"
        // },
        "radio_field": {
            "type": "integer"
        },
        "example_text": {
            "type": "string",
            "maxLength": 300,
        },
        "example_input": {
            "type": "string",
            "pattern": "^123", // Starts with 123...
        },
        "example_select": { 
            "type": "string",
            "enum": ["default", "option1", "option2"] 
        }
    }
}

Build a form directly from a Json Schema

// @todo Default JSON Schema form generator implementation.
function formFromJsonSchema(schema, state, context) {

    const STRING_TYPES = {
        "string": "text",
        "email": "email",
        // ...
    };

    const TEXT_AREA_LENGTH = 100;

    const fields = {};

    for (let key in schema) {

        const schemaField = schema[key];

        const field = {
            element: 'input',
            props: {},
        };

        if (schemaField.type in STRING_TYPES) {

            if (schemaField.maxLength >= TEXT_AREA_LENGTH) {
                field.element = 'textarea';
            } else {
                field.element = 'input';
                field.props.type = STRING_TYPES[schemaField.type];
            }
        }

        if (key in state) {
            field.props.value = state[key];
        }

        field.label = key.replace('_', '');
        field.label = field.label.charAt(0).toUpperCase() + field.label.slice(1);

        fields[key] = field;

    }

    return fields;

}

Full power of an abstract form spec with hooks.

// ./schemas/ExampleForm.js
import React from 'react';

const inputHook = ({ props }, context) => {

    // Default value
    if (props.name in context.state) {
        props.value = context.state[props.name];
    } else if ('value' in props) {
        context.state[props.name] = props.value;
    }

    props.onChange = (event) => {
        context.setState({ [event.target.name]: event.target.value });
    }

}

const checkedInputHook = ({ props }, context) => {

    if (props.type === 'radio') {

        props.checked = (props.name in context.state && context.state[props.name] === props.value);

        props.onChange = (event) => {
            context.setState({ [event.target.name]: event.target.value });
        }

    } else if (props.type === 'checkbox') {

        let values = (props.name in context.state && context.state[props.name]) ? context.state[props.name] : [];
        props.checked = (values.includes(props.value));

        props.onChange = (event) => {
            values = (event.target.checked) ? [...values, event.target.value] : values.filter(value => (value !== event.target.value));
            context.setState({ [event.target.name]: values });
        }

    }

}

const selectOptionsHook = ({ props, schema }, context) => {
    props.children = schema.enum.map(key => <option key={key} value={key}>{key}</option>);
}

const ExampleGroupTemplate = ({ children, spec }) => {
    return (
        <fieldset>
            <legend>{spec.data?.legend || spec.label}</legend>
            {children}
        </fieldset>
    );
}

export default {
    checkbox_field: {
        label: "Les checkboxes",
        templates: {
            group: ExampleGroupTemplate,
        },
        group: [
            {
                label: "Checkbox 1",
                element: 'input',
                props: {
                    type: 'checkbox',
                    value: '1',
                }
            },
            {
                label: "Checkbox 2",
                element: 'input',
                props: {
                    type: 'checkbox',
                    value: '2',
                }
            },
            {
                label: "Checkbox 3",
                element: 'input',
                props: {
                    type: 'checkbox',
                    value: '3',
                }
            }
        ],
        hooks: [checkedInputHook]
    },
    radio_field: {
        group: [
            {
                label: "Radio 1",
                element: 'input',
                props: {
                    type: 'radio',
                    value: '1',
                },
                templates: {
                    group: true,  // True (default component) or Component - wrap group elements. (default: false)
                }
            },
            {
                label: "Radio 2",
                element: 'input',
                props: {
                    type: 'radio',
                    value: '2',
                },
                templates: {
                    group: true,
                }
            }
        ],
        templates: {
            group: false,  // Disable default wrapping outside group (default: true)
        },
        hooks: [checkedInputHook]
    },
    example_widget: {
        label: "Reducer widget...",
        templates: {
            group: ExampleGroupTemplate,
        },
        group: [
            {
                element: 'button',
                props: {
                    type: 'button',
                    children: 'Decrease',
                },
                hooks: [
                    ({ props }, context) => {
                        props.onClick = (event) => {
                            context.dispatch({ type: 'decrease' })
                        }
                    }
                ]
            },
            {
                element: 'input',
                props: {
                    name: 'example_widget_count',
                    type: 'number',
                    value: 0,
                },
                hooks: [
                    ({ props }, context) => {
                        // Default value
                        if (!(props.name in context.state)) {
                            context.state[props.name] = props.value;
                        } else {
                            props.value = context.state[props.name];
                        }
                        props.onChange = (event) => {
                            // handled by reducer...
                        }
                    }
                ],
            },
            {
                element: 'button',
                props: {
                    type: 'button',
                    children: 'Increase',
                },
                hooks: [
                    ({ props }, context) => {
                        props.onClick = (event) => {
                            context.dispatch({ type: 'increase' })
                        }
                    }
                ]
            },
        ],
    },
    example_text: {
        label: "Example text",
        element: 'textarea',
        hooks: [
            inputHook,
            (spec, context) => {
                spec.data = {
                    legend: "Override data hook...",
                }
                spec.append = ({ spec }) => <div>Error...</div>
            }
        ],
        data: {
            legend: "Example data...",
        },
        templates: {
            group: ExampleGroupTemplate,
        },
        append: ({ spec }) => <div>Appended...</div>,
    },
    example_markup: {
        html: ({ spec }) => <pre>Example markup...</pre>,
    },
    example_input: {
        label: "Example input",
        element: 'input',
        props: {
            type: 'text',
            placeholder: "Example...",
            // ...
        },
        hooks: [inputHook],
    },
    example_select: {
        label: "Select from SSR schema",
        element: 'select',
        props: {
            value: 'default', // Set default
        },
        hooks: [
            inputHook,
            selectOptionsHook,
            (field, context) => {
                field.props.value = field.schema.enum[0]; // Override default ...
                context.setState({[field.props.name]: field.props.value});
            }
        ],
    },
};

// Form elements options
interface IFormElementSpec {
    key: string;
    element: TElement;
    factory?: TFormElementFactory;
    templates?: IFormElementTemplates;
    schema?: IFormElementJsonSchema;
    props?: TElementProps;
    hooks?: Array<TFormElementHook>;
    label?: string;
    data?: any;
    prepend?: React.ElementType;
    append?: React.ElementType;
    html?: React.ElementType;
}

interface IFormElementWithGroupSpec extends IFormElementSpec {
    group?: Array<IFormGroupElementSpec>;
}
// Form manager context
interface IFormManagerContext {
    state?: React.ComponentState;
    setState?: React.SetStateAction<React.ComponentState>;
    reducer?: React.Reducer<React.ComponentState, React.ReducerAction<any>>;
    dispatch?: React.Dispatch<React.ReducerAction<any>>;
}

Example form component implementation

// client
import React, { useContext, useState } from 'react';
import { withRouter } from 'react-router';

import DataContext from './DataContext';
import formSpecs from './schemas/ExampleForm';

import { Form, FormManager } from '@jslabs/react-forms';

import axios from 'axios';

class ExampleForm extends React.Component {

    static contextType = DataContext;

    constructor(props, context) {
        super(props);
        this.state = context.data.data || {};
        this.submitHandler = this.handleSubmit.bind(this);
        this.stateHandler = this.setState.bind(this);
        this.reducer = this.reducer.bind(this);
    }

    reducerAction(state, action) {
        switch (action.type) {
          case 'increase':
            return { ...state, example_widget_count: state.example_widget_count + 1 };
          case 'decrease':
            return { ...state, example_widget_count: state.example_widget_count - 1 };
          default:
            return state;
        }
    }

    reducer(action) {
        this.setState(this.reducerAction(this.state, action));
    }

    handleSubmit(event) {
        event.preventDefault();

        axios.post(event.target.action, this.state)
            .then(({ data }) => {
                if (data.errors) {
                    // @todo
                    alert(JSON.stringify(data.errors, null, 2));
                } else {
                    console.log('response body....', data.data);
                    this.setState(data.data);
                }
            })
            .catch(error => {
                console.log(error);
            });
    }

    render() {
        
        // Generated form from a JSON Schema.
        // const formSchema = formFromJsonSchema(this.context.data.schema['properties'], this.state, this.context);

        const dispatch = (action) => {
            this.reducer(action);
        }
        return (
            <form method="POST" action={this.props.location.pathname} onSubmit={this.submitHandler}>
                <h3>Profile settings</h3>
                <FormManager.Provider value={{ state: this.state, setState: this.stateHandler, reducer: this.reducer, dispatch }}>
                    <Form specs={formSchema} schema={this.context.data.schema['properties']} />
                </FormManager.Provider>
                <button type="submit">Submit</button>
            </form>
        );
    }
}

export default withRouter(ExampleForm);

Example server side schema serving and form validation

// server
const express = require('express')
const Ajv = require('ajv').default

const router = express.Router()

const errors = require('../handlers/errors')
const exampleSchema = require('../schemas/example')

router.get('/example-form', async (req, res, next) => {

    const data = {
        // ...
    }

    res.locals.data = {
        data: data,
        schema: exampleSchema,
    }

    return next()
})

router.post('/example-form', async (req, res, next) => {

    const body = req.body || {}

    const ajv = new Ajv({ coerceTypes: true })  // Type casting
    const validate = ajv.compile(exampleSchema)
    const valid = validate(body)

    if (valid) {

        // data was validated
    }

    return res.json({
        data: body,
        valid,
        errors: validate.errors || null,
    })
})

Keywords

FAQs

Package last updated on 03 Dec 2020

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc