@button-inc/form-schema
v1.0.0-alpha.8
Published
Build forms from a schema
Downloads
13
Keywords
Readme
Form Schema
Contents
Overview
This library is built on top of react json schema form, with an aim to make it easier to create forms that have the following properties:
- Progressive enhancement: When used with a SSR platform, the forms will be functional for users who haven't loaded the javascript or css, and progressively enhance when they are added.
- User Experience: This package focuses on making it easy to create single question-per-page forms, to create a smooth user experience.
- Accessibility: This package is designed to plug in easily with the service-development toolkit component-libraries, which have a focus on accessible components.
Note that while this package makes it easier to meet these three goals, it is not limited to them. For example, it can be used with a client-side rendering framework like create react app if you are interested only in accessibility and single-page form questions.
Getting Started
Install package:
npm i @button-inc/form-schema
oryarn add @button-inc/form-schema
depending on your package manager.
Add to your app:
import builder from @button-inc/form-schema
, or to use with themed components,import {govBuilder} from @button-inc/form-schema
See below for specific use cases, or browse our examples for a working setup.
Usage
This package is setup to split a json schema into a series of pages, as well as handle redirection, form data, and front-end/back-end validation for users with or without javascript. To do this, it requires three main configuration pieces:
- Schemas: Schemas defining your forms and their ui.
- Rendering Forms: A dynamic
GET
route is required to handle rendering the split-up forms. - Posting Data: A dynamic
POST
route is required to handle saving data for the split-up forms.
The Form components, functions and middleware to set this up will be returned from the main builder
function (default import). E.g:
import builder from '@button-inc/form-schema';
export const { postMiddleware, getHandler, Forms } = builder(schema, uiSchema, options);
Where the Forms
will be an array of form components.
Schemas
Since this library utilizes react json schema form, the schema configuration is largely the same. The differences are:
form-schema:
- Splitting Pages: The schema you provide will be split into individual properties, each of which can be rendered on a page. However, for fields that need to be displayed together you can group them in a property with
type: 'object'
. They will be shown together on a single page. E.g
{
...
properties: {
groupedFields: {
type: 'object',
properties: {
onPageOne: {
type: 'string',
...
},
alsoOnPageOne: {
type: 'string',
...
}
}
}
}
}
- dependencies: Currently, only the dependency
oneOf
is supported.anyOf
will be added in the future. - files: To allow uploading files with
multipart
encoding, you can add a customhasFiles
boolean property to fields of typestring
. See files for more information. - names: To support non-javascript users, fields will include a name attribute. This needs to be set in the schema property, as
name: string
. - html5 validations: HTML5 validation attributes such as maxlength, minlength, and patterns can be set on the schema to be passed down to fields.
ui-schema: The ui schema has the same properties as react json schema form.
Rendering Forms
To handle rendering forms, you will need to know any previous form data to load into the form, as well as the index of the current form to render. The getHandler returns these for you, as well as whether or not the current route corresponds to an actual form-page. You can make a request to this route, and use the returned props to render the correct form. For example:
- getRoute.js
app.get('/getRoute/:id', (req, res) => {
const { formIndex, formData, validPage } = getHandler(req);
res.json({ formIndex, formData, validPage });
});
Note that this is if you want to use sessions to store data. To handle data in between pages yourself, see the custom data handling section.
- form.jsx
// Import the array of form components from the file you created them in.
import { Forms } from 'form-schema';
export default function Form({ formIndex, formData, validPage }) {
const Form = Forms[formIndex];
return <>{validPage && <Form formData={formData} />}</>;
}
The Form
component will be an rjsf form component, and can be passed in the usual props, such as templates. In addition, it can take a rerouteHandler prop, which is a function to handle redirection when javascript is enabled. This allows you to use whatever SPA front-end router you need. E.g:
form.jsx
...
const rerouteHandler = (nextPage: string, _isValid: boolean, lastPage: boolean) => {
// Re-route using your routing library
router.push(lastPage ? '/end' : nextPage);
};
return <>{validPage && <Form formData={formData} rerouteHandler={rerouteHandler} />}</>;
}
Note: If not providing this prop, redirection will be handled via window.location
which will cause a re-render.
Posting Data
To save data as the user progresses through pages, a dynamic post route needs to be setup. This post route requires body parsing middleware to be enabled, which is enabled by default in express
(4.16+) and nextjs
. In the dynamic route, simply pass the middleware in. E.g:
app.post('/postRoute/:num', postMiddleware);
If the useSession
option is set as true, you should setup session middleware for this route, and it will parse and save the data for you. If not using sessions, see the custom data handling section which allows you to save the data how you want in between forms. Lastly, when a form is completed a final onFormEnd function will fire, allowing you to see if it validated correctly on the backend, and save the data. This is configured in the options
argument of the builder
function.
Files
For handling file uploads, the data needs to be parsed differently. To support this, a separate POST
route needs to be configured ar postRoute/:num/file
, where postRoute
is the route configured in options. This route can be configured similarily to the base route:
postRoute/:num/file.js
app.post('/postRoute/:num', fileMiddleware);
Inside this route, the body-parser middleware needs to be disabled to allow file parsing. To handle the file stream, the options
configuration passes into builder takes two functions, handleReadStream and onFileLoad. These can be used to pipe the stream onward, and notify when the upload is complete.
Custom Data Handling
By default, a session can be used to store incomplete form data. To handle the data yourself, pass in useSessions: false
to the options configuration, along with an onPost function. The onPost
function can be used to clean the form data and save it in between pages. E.g:
options
onPost: (postData: object, schemaIndex: number, cleanSchemaData: Function) => {
const formData = getDataFromDB();
const newData = cleanSchemaData(formData);
saveDataToDB(newData);
return newData;
},
Note that the cleaned data is expected to be returned by this function.
To retrieve the data, in your GET
route, simply retrieve it after calling the handler.
getRoute.js
app.get('/getRoute/:id', (req, res) => {
const { formIndex, validPage } = getHandler(req);
const formData = getDataFromDB(formIndex);
res.json({ formIndex, formData, validPage });
});
Functions
Builder
Builder function is the default export of the package, and can also be imported with certain widgets pre-attached by importing the named import govBuilder. For example:
import { govBuilder } from '@button-inc/form-schema';
or
import builder from '@button-inc/form-schema';
It takes the following arguments:
Builder returns the values:
- getHandler: A function to handle get requests. See getHandler for more details.
- postMiddleware: Middleware to handle post requests. See postMiddleware for more details.
- fileMiddleware: Middleware to handle file post requests. See fileMiddleware for more details.
- Forms: An array of form components corresponding to each page.
validationFunction
A function to generate custom validations. It takes the following arguments:
The return value should be a boolean indicating whether or not the input is valid.
handleReadStream
A function to handle file upload streams. It taks arguments:
onFileLoad
A callback that will fire when files finish uploading. It takes the arguments:
onPost
A callback that will fire when form data is posted to the API. It takes the arguments:
The return value should be the cleaned data.
onFormEnd
A callback that will fire when the form is completes. It takes the arguments:
getHandler
This function should be used at the dynamic route following the getRoute
you set in options
. For example, if you setup your get route to be '/'
,
you should call getHandler
at '/:num'
. It is used to return the information you need to render the correct form, see the return values below. It takes arguments:
The return value for the function is an object with the following properties:
formIndex
: A number showing the index of the form component to render.formData
: An object containing current formData to pass into the form. Intended for use with sessions enabled, if not using sessions will be an empty object.validPage
: A boolean indicating whether or not the current route points to a valid form page.