LIGHT

  • News
  • Docs
  • Community
  • Reddit
  • GitHub

React Schema Form

In the previous tree-view-menu section, we have added several menu items into the ResponsiveDrawer to form a hierarchical navigation component with react-router integrated. In this section, we are going to hook up react-schema-form to have generic form generation for most of the menu items.

The react-schema-form is a react component we built on top of material-ui to help us to build SPA that interacts with backend services built on top of light-hybrid-4j and light-rest-4j. To learn more about react-schema-form, you can find an online demo. If you want to try a real application, please take a look at the Taiji Blockchain Web Client.

Dependencies

Run the following command to install the react-schema-form.

cd ~/networknt/light-portal/view
yarn add react-schema-form

If you are using npm.

npm install --save react-schema-form

After the installation, the dependencies in package.json should look like this.

 "dependencies": {
    "@material-ui/core": "^3.8.1",
    "@material-ui/icons": "^3.0.1",
    "react": "^16.7.0",
    "react-dom": "^16.7.0",
    "react-redux": "^6.0.0",
    "react-router": "^4.3.1",
    "react-router-dom": "^4.3.1",
    "react-schema-form": "^0.6.10",
    "react-scripts": "2.1.2",
    "redux": "^4.0.1",
    "redux-logger": "^3.0.6",
    "redux-thunk": "^2.3.0"
  },

The vesions might be different when you follow the tutorial yourself.

Form Component

Now let’s create a new form component as Form.js

import React, {Component} from 'react';
import { withStyles } from '@material-ui/core/styles';
import { submitForm} from "../actions";
import connect from "react-redux/es/connect/connect";
import CircularProgress from '@material-ui/core/CircularProgress';
import Button from '@material-ui/core/Button';
import SchemaForm from 'react-schema-form/lib/SchemaForm';
import utils from 'react-schema-form/lib/utils';
import forms from '../data/Forms';

const styles = theme => ({
    root: {
        display: 'flex',
        flexWrap: 'wrap',
    },
    formControl: {
        margin: theme.spacing.unit,
        minWidth: 120,
    },
    selectEmpty: {
        marginTop: theme.spacing.unit * 2,
    },
    progress: {
        margin: theme.spacing.unit * 2,
    },
    button: {
        margin: theme.spacing.unit,
    },
});

class Form extends Component {

    constructor(props) {
        super(props);
        this.state = {
            fetching: false,
            error: null,
            formId: null,
            schema: null,
            form: null,
            actions: null,
            model: {}
        }
    }

    componentWillUpdate(nextProps, nextState, nextContext) {
        if(this.state.formId !== nextProps.match.params.formId) {
            let formData = forms[this.props.match.params.formId];
            this.setState({
                formId: nextProps.match.params.formId,
                schema: formData.schema,
                form: formData.form,
                actions: formData.actions,
                model: formData.model || {}
            });
        }
    }

    componentDidMount() {
        let formData = forms[this.props.match.params.formId];
        this.setState({
            formId: this.props.match.params.formId,
            schema: formData.schema,
            form: formData.form,
            actions: formData.actions,
            model: formData.model || {}
        });
    }

    onModelChange = (key, val) => {
        //console.log(this.state.model);
        utils.selectOrSet(key, this.state.model, val);
    };

    onButtonClick(action) {
        console.log(action, this.state.model);
        let validationResult = utils.validateBySchema(this.state.schema, this.state.model);
        if(!validationResult.valid) {
            this.setState({error: validationResult.error.message});
        } else {
            action.data = this.state.model;
            this.setState({success: action.success, fetching: true});
            this.props.submitForm(action);
        }
    }

    render() {
        const { classes } = this.props;
        //console.log(this.state.actions);
        if(this.state.schema) {
            var actions = [];
            this.state.actions.map((item, index) => {
                let boundButtonClick = this.onButtonClick.bind(this, item);
                actions.push(<Button variant="contained" className={classes.button} color="primary" key={index} onClick={boundButtonClick}>{item.title}</Button>)
            });
            let wait;
            if(this.state.fetching) {
                //console.log('fetching is true');
                wait = <div><CircularProgress className={classes.progress} /></div>;
            } else {
                //console.log("fetching is false");
                wait = <div></div>;
            }
            let title = <h2>{this.state.schema.title}</h2>

            return (
                <div>
                    {wait}
                    {title}
                    <SchemaForm schema={this.state.schema} form={this.state.form} model={this.state.model} onModelChange={this.onModelChange} />
                    <pre>{this.state.error}</pre>
                    {actions}
                </div>
            )
        } else {
            return (<CircularProgress className={classes.progress} />);
        }
    }
}

const mapStateToProps = state => ({
});

const mapDispatchToProps = dispatch => ({
    submitForm: action => dispatch(submitForm(action))
});

export default connect(
    mapStateToProps,
    mapDispatchToProps
)(withStyles(styles)(Form));

Form Data

This Form component maintains its states and loads a specific form by formId passed from the router path. The form is loaded from the local static file in data folder called Forms.json. Here is the content in the Forms.json

{
  "longLivedTokenForm": {
    "formId": "longLivedTokenForm",
    "actions": [
      {
        "host": "lightapi.net",
        "service": "playground",
        "action": "generateJwt",
        "version": "0.1.0",
        "title": "Generate Long-Lived Token",
        "success": "/playground/longLivedToken"
      }
    ],
    "schema": {
      "type": "object",
      "required": [
        "client_id",
        "scope"
      ],
      "title": "JWT Claims",
      "properties": {
        "exp": {
          "type": "integer"
        },
        "client_id": {
          "type": "string"
        },
        "user_id": {
          "type": "string"
        },
        "scope": {
          "type": "array",
          "items": {
            "type": "string"
          }
        }
      }
    },
    "form": [
      "*"
    ]
  }
}

As you can see that it is used the same JSON schema published from the backend service to generate the form and it has a section to define actions for the submit buttons and the mapping service so that once a button is clicked, the corresponding service will be automatically invoked.

All the JSON schemas for the light-portal services can be found at https://github.com/networknt/model-config/tree/master/hybrid

In the future, we are going to load the form from a service that is deployed on the portal query side service. Now, it is OK to put all forms into the same json file for development.

App

In the App.js we need to add one more route for path /form/:formId that is routed to the Form component.

<Route path="/form/:formId" component={Form} />

Here is the updated App.js

import React, { Component } from 'react';
import {Switch, Route} from 'react-router-dom';
import { Router } from 'react-router';
import createBrowserHistory from 'history/createBrowserHistory'
import Home from './components/Home';
import About from './components/About';
import ResponsiveDrawer from './components/ResponsiveDrawer';
import Form from './components/Form';

export const history = createBrowserHistory();

class App extends Component {
  render() {
    return (
        <Router history={history}>
          <ResponsiveDrawer>
            <Switch>
              <Route exact path="/" component={Home} />
              <Route path="/about" component={About} />
              <Route path="/form/:formId" component={Form} />
            </Switch>
          </ResponsiveDrawer>
        </Router>
    );
  }
}

export default App;

Reducer

Now we need to create a FormReducer.js to handler the action types for form submission. As we are calling the backend service async, we need to handle three actions: STARTED, SUCCESS and FAILURE.

Here is the FormReducer.js

import {SUBMIT_FORM_STARTED, SUBMIT_FORM_FAILURE, SUBMIT_FORM_SUCCESS} from "../actions/types";

const initialState = {};
export default (state = initialState, action) => {
    switch (action.type) {
        case SUBMIT_FORM_SUCCESS:
            return {
                ...state,
                fetching: false,
                result: action.payload
            };
        case SUBMIT_FORM_FAILURE:
            return {
                ...state,
                fetching: false,
                error: action.payload
            };
        case SUBMIT_FORM_STARTED:
            return {
                ...state,
                fetching: true
            };
        default:
            return state;
    }
}

With the new reducer added, we need to update the index.js in the reducers folder to add this new reducer.

import { combineReducers } from 'redux';
import menuReducer from './menu';
import formReducer from './FormReducer';

export default combineReducers({
    menu: menuReducer,
    form: formReducer
});

Action

Let’s handle the actions for the form submission. First, we need to add the three actions to the types.js

export const LOAD_MENU = 'load_menu';
export const SUBMIT_FORM_STARTED = 'SUBMIT_FORM_STARTED';
export const SUBMIT_FORM_SUCCESS = 'SUBMIT_FORM_SUCCESS';
export const SUBMIT_FORM_FAILURE = 'SUBMIT_FORM_FAILURE';

Next, we need to add a function to handle the submitForm in the index.js in actions folder.

export function submitForm(action) {
    return async (dispatch) => {
        dispatch({type: SUBMIT_FORM_STARTED});
        const request = {
            method: 'POST',
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json',
            },
            body: JSON.stringify(action)
        };
        console.log(request);
        try {
            const response = await fetch('/api/portal', request);
            const data = await response.json();
            console.log("data", data);
            dispatch({ type: SUBMIT_FORM_SUCCESS, payload: data });
            history.push(action.success);
        } catch(e) {
            console.log("error " + e.toString());
            dispatch({ type: SUBMIT_FORM_FAILURE, error: e.toString()})
        }
    }
}

You can see that it async dispatch the call and await the fetch function to return. The backend service is using a relative url that means the single page application is served by the light-rotuer instance of light-protal. To learn more about how to use light-router for SPA, please visit light-router tutorial.

With above changes, you can expand the OAuth/PlayGround and click the Long-lived Token menu item. A Jwt Claims form will show up immediately. Once you fill in the form and click the generated GENERATE LONG-LIVED TOKEN button, the form will be submitted to the backend service.

Now let’s check in the code into a branch ui-react-schema-form.

git checkout -b ui-react-schema-form
git add .
git commit -m "add react-schema-form"
git push origin ui-react-schema-form

In the next tutorial, we are going to setup a proxy in package.json and wire in backend service to generate a real long lived token.

  • About Light
    • Overview
    • Testimonials
    • What is Light
    • Features
    • Principles
    • Benefits
    • Roadmap
    • Community
    • Articles
    • Videos
    • License
    • Why Light Platform
  • Getting Started
    • Get Started Overview
    • Environment
    • Light Codegen Tool
    • Light Rest 4j
    • Light Tram 4j
    • Light Graphql 4j
    • Light Hybrid 4j
    • Light Eventuate 4j
    • Light Oauth2
    • Light Portal Service
    • Light Proxy Server
    • Light Router Server
    • Light Config Server
    • Light Saga 4j
    • Light Session 4j
    • Webserver
    • Websocket
    • Spring Boot Servlet
  • Architecture
    • Architecture Overview
    • API Category
    • API Gateway
    • Architecture Patterns
    • CQRS
    • Eco System
    • Event Sourcing
    • Fail Fast vs Fail Slow
    • Integration Patterns
    • JavaEE declining
    • Key Distribution
    • Microservices Architecture
    • Microservices Monitoring
    • Microservices Security
    • Microservices Traceability
    • Modular Monolith
    • Platform Ecosystem
    • Plugin Architecture
    • Scalability and Performance
    • Serverless
    • Service Collaboration
    • Service Mesh
    • SOA
    • Spring is bloated
    • Stages of API Adoption
    • Transaction Management
    • Microservices Cross-cutting Concerns Options
    • Service Mesh Plus
    • Service Discovery
  • Design
    • Design Overview
    • Design First vs Code First
    • Desgin Pattern
    • Service Evolution
    • Consumer Contract and Consumer Driven Contract
    • Handling Partial Failure
    • Idempotency
    • Server Life Cycle
    • Environment Segregation
    • Database
    • Decomposition Patterns
    • Http2
    • Test Driven
    • Multi-Tenancy
    • Why check token expiration
    • WebServices to Microservices
  • Cross-Cutting Concerns
    • Concerns Overview
  • API Styles
    • Light-4j for absolute performance
    • Style Overview
    • Distributed session on IMDG
    • Hybrid Serverless Modularized Monolithic
    • Kafka - Event Sourcing and CQRS
    • REST - Representational state transfer
    • Web Server with Light
    • Websocket with Light
    • Spring Boot Integration
    • Single Page Application
    • GraphQL - A query language for your API
    • Light IBM MQ
    • Light AWS Lambda
    • Chaos Monkey
  • Infrastructure Services
    • Service Overview
    • Light Proxy
    • Light Mesh
    • Light Router
    • Light Portal
    • Messaging Infrastructure
    • Centralized Logging
    • COVID-19
    • Light OAuth2
    • Metrics and Alerts
    • Config Server
    • Tokenization
    • Light Controller
  • Tool Chain
    • Tool Chain Overview
  • Utility Library
  • Service Consumer
    • Service Consumer
  • Development
    • Development Overview
  • Deployment
    • Deployment Overview
    • Frontend Backend
    • Linux Service
    • Windows Service
    • Install Eventuate on Windows
    • Secure API
    • Client vs light-router
    • Memory Limit
    • Deploy to Kubernetes
  • Benchmark
    • Benchmark Overview
  • Tutorial
    • Tutorial Overview
  • Troubleshooting
    • Troubleshoot
  • FAQ
    • FAQ Overview
  • Milestones
  • Contribute
    • Contribute to Light
    • Development
    • Documentation
    • Example
    • Tutorial
“React Schema Form” was last updated: July 5, 2021: fixes #275 checked and corrected grammar/spelling for majority of pages (#276) (b3bbb7b)
Improve this page
  • News
  • Docs
  • Community
  • Reddit
  • GitHub
  • About Light
  • Getting Started
  • Architecture
  • Design
  • Cross-Cutting Concerns
  • API Styles
  • Infrastructure Services
  • Tool Chain
  • Utility Library
  • Service Consumer
  • Development
  • Deployment
  • Benchmark
  • Tutorial
  • Troubleshooting
  • FAQ
  • Milestones
  • Contribute