This project is opinionated implementation of a manager for mozilla form or any of it's derivative projects, it provides a REST based api layer that handles form submission and update.
- Saving react-jsonschema-form related json with configurable
RESTorLocalStorageapi for testing - Configurable form updates on data change, with
PUTrequests - Static and
RESTform configuration support - Flexible authentication management
Install react-jsonschema-form-manager by running:
npm install --s react-jsonschema-form-managerThe simplest use case would be to load static properties and save them in localStorage,
this can be done like this:
import React from "react";
import ReactDOM from "react-dom";
import Form from "react-jsonschema-form";
import withManager, { LocalStorageFormManager, StaticConfigResolver, intervalUpdateStrategy } from "react-jsonschema-form-manager";
let config = {
schema: {
type: "object",
required: ["firstName", "lastName"],
properties: {
firstName: {
type: "string",
title: "First name",
},
lastName: {
type: "string",
title: "Last name",
}
}
}
};
let configResolver = new StaticConfigResolver(config);
let manager = new LocalStorageFormManager();
let updateStrategy = intervalUpdateStrategy(10000);
let FormToDisplay = withManager(manager, configResolver, updateStrategy)(Form);
ReactDOM.render(<FormToDisplay />, document.getElementById("app"));Functional part of API consist of 3 parts
- Configuration loader (
staticorREST) - Manager (
localStorageorREST) - Update strategy (
imediateorinterval)
You can configure and extend them independently of one another, to get functionality you need.
UI workflow consists of 2 phases
LoadingScreen- display loading, while configuration is resolved- Show provided
formwith loaded configuration, or display anErrorScreenscreen in case configuration can't be resolved
You can override default presentations, when you call withManager
import React, { Component } from "react";
import Form from "react-jsonschema-form";
import withManager from "react-jsonschema-form-manager";
class CustomLoadingScreen extends Component {
render() {
return (
<div className="container">
<h1>About to launch</h1>
</div>
);
}
}
class CustomErrorScreen extends Component {
render() {
return (
<div className="container">
<h4>Houston, we have a problem</h4>
<h2>
{this.props.error.message}
</h2>
</div>
);
}
}
...
export default withManager(manager, configResolver)(Form, CustomLoadingScreen, CustomErrorScreen);Each mozilla form needs a configuration, which can be either pre configured or loaded from the server.
LoadingScreen will be displayed while configuration loads. ConfigResolver does not make any assumptions regarding the content
of the configuration, so except for schema and uiSchema you can return any configuration needed.
The simplest configuration for schema configuration load, would look like this:
import React, { Component } from "react";
import Form from "react-jsonschema-form";
import withManager, { StaticConfigResolver, LocalStorageFormManager } from "react-jsonschema-form-manager";
let config = {
schema: {
//...
},
uiSchema: {
//...
},
formData: {
//...
},
};
let configResolver = new StaticConfigResolver(config);
let localStorageManager = new LocalStorageFormManager();
let FormToDisplay = withManager(configResolver, localStorageManager)(Form);
class ResultForm extends Component {
render() {
return (
<FormToDisplay />
);
}
}Here the config will be used as Form configuration and StaticConfigResolver is used to return it.
This is enough for forms to operate properly
There are 2 supported configuration resolvers:
- Static configuration resolver, which uses pre configured
configsetting - REST configuration resolver, which talks to API endpoint for needed
configuration
You can also specify your own configuration resolver.
StaticConfigResolver takes 2 parameters config and timeout,
configis the configuration to returntimeoutdelay before returning configuration, it was added primarily for testing purposes
import { StaticConfigResolver } from "react-jsonschema-form-manager";
let timeout = 10000;
let config = {
schema: {
//...
},
uiSchema: {
//...
},
formData: {
//...
},
};
let configResolver = new StaticConfigResolver(config, timeout);Primary configuration option is by means of REST API, which you can configure with needed authentication
RESTConfigResolver takes 3 parameters
urlconfiguration url to usecredentialsauthentication to use with urloutputHandlermanipulate data returned by the HTTP call before resolving the promise
The simplest RESTConfigResolver will look like this:
import { RESTConfigResolver } from "react-jsonschema-form-manager";
let configResolver = new RESTConfigResolver(`https://example.com/schema`);In this case no authentication is needed and config resolver returnes json result of REST request
There are 3 options to specify credentials for the configuration endpoint
In this case no authentication will be provided for the request
import { RESTConfigResolver } from "react-jsonschema-form-manager";
let configResolver = new RESTConfigResolver(`https://example.com/schema`);In case authentication can be done with domain Cookies, you can simply specify credentials object in accordance with fetch documentation. Refer to whatwg-fetch sending cookies documentation for more details.
import { RESTConfigResolver } from "react-jsonschema-form-manager";
let configResolver = new RESTConfigResolver(
`https://example.com/schema`,
{
credentials: 'same-origin'
});or
import { RESTConfigResolver } from "react-jsonschema-form-manager";
let configResolver = new RESTConfigResolver(
`https://example.com/schema`,
{
credentials: 'include'
});Transformation function, would sign fetch Request with any custom authentication logic needed.
For example to have a Basic authentication can be done like this:
import { RESTConfigResolver } from "react-jsonschema-form-manager";
let credentials = (req) => new Request(req, { headers: {
'Authorization': 'Basic '+ btoa(`${username}:${password}`),
}});
let configResolver = new RESTConfigResolver(
`https://example.com/schema`,
credentials);Use this if your HTTP resource doesn't return the exact JSON data you want to pass to your Form's props. Also useful for adding/merging default values to the HTTP response.
import { RESTConfigResolver } from "react-jsonschema-form-manager";
let outputHandler = obj => {
let output = {
subSetOfData: obj.someProperty,
};
return output;
};
let configResolver = new RESTConfigResolver(
"http://localhost:3000/conf",
{},
outputHandler
);
// if origOutput = JSON response of REST call,
//configResolver resolves to: { subSetOfData: origOutput.someProperty };If neither REST or Static configuration fits your needs, you can create your own configuration, by simply providing
resolve method with no params, that would return Promise with config result
For example GraphQL ConfigResolver can be defined something like this:
import { RESTConfigResolver } from "react-jsonschema-form-manager";
class GraphQLConfigResolver {
constructor(url, credentials) {
this.restResolver = new RESTConfigResolver(url, credentials);
}
resolve = () => {
return this.restResolver.resolve().then(({ data, error }) => {
return new Promise((resolve, reject) => {
if (error) {
reject(new Error(error));
} else {
resolve(data);
}
});
});
};
}Besides schema configuration, you need to specify the way to save formData. System supports 2 ways of saving data
LocalStorage- using LocalStorage for storing submitted data, this is primarily for testing purposeREST- saving data in REST endpoint
The simplest configuration can look like this
import React, { Component } from "react";
import Form from "react-jsonschema-form";
import withManager, { StaticConfigResolver, LocalStorageFormManager } from "react-jsonschema-form-manager";
let config = {
//...
};
let configResolver = new StaticConfigResolver(config);
let localStorageManager = new LocalStorageFormManager();
let FormToDisplay = withManager(configResolver, localStorageManager)(Form);
class ResultForm extends Component {
render() {
return (
<FormToDisplay onSubmit={() => onSuccessCallback}/>
);
}
}In this case data is stored in LocalStorage and on successful submit, if there are no errors onSuccessCallback is called for further processing.
Implementation wraps onSubmit on original form and calls it only after formData was successfully saved with underlying FormManager.
LocalStorage store is there only for testing purposes and not supposed to be used in production.
You can specify a key under which provided form will be stored. By default form is used as a key and you are limited to single form.
As with schema configuration, this is primary option for storing your data.
RESTFormManager takes 2 parameters
urlconfiguration url to usecredentialsauthentication to use with url
Authentication logic is the same as for RESTConfigurationResolver, the only difference is that instead of the GET, we send a POST
with JSON of formData as a request.
As with ConfigurationResolver you can create your custom implementation for FormManager, which needs only 3 methods
submitcalled when form is submitted, should returnPromisethat will be resolved after successful submissionupdateIfChangedcalled when form needs to update it's transient stay to the server. It must return either aPromise, if manager considers data changed and will trigger an update, orundefinedif there is nothing to change.updateIfChangedacceptsforceflag that will always result in update.onChangecalled whenever form data changes, it's needed to manage formData state inside a manager
For example FormManager using SessionStorage can look something like this:
class SessionStorageFormManager {
constructor(key = DEFAULT_KEY) {
this.key = key;
}
onChange = (state) => {
this.formData = state.formData;
}
submit = () => {
return new Promise(resolve => {
sessionStorage.setItem(this.key, JSON.stringify(this.formData));
resolve(formData);
});
}
updateIfChanged = () => {
return new Promise(resolve => {
sessionStorage.setItem(this.key, JSON.stringify(this.formData));
resolve(formData);
});
}
}You can skip logic in update, if you are not planning to use any UpdateStrategy in your case.
UpdateStrategy is needed in case you want to save transient results of work.
For example
import React, { Component } from "react";
import Form from "react-jsonschema-form";
import { instantUpdateStrategy } from "react-jsonschema-form-manager";
...
let FormToDisplay = withManager(configResolver, manager, instantUpdateStrategy)(Form);
class ResultForm extends Component {
render() {
return (
<FormToDisplay/>
);
}
}In this case all changes to the formData, will be instantly submitted by the FormManager to the underlying server.
As with FormManager, UpdateStrategy override embedded onChange mozilla-jsonschema functionality.
There are 2 kinds of UpdateStrategy currently supported
instantUpdateStrategy updates request on every change of the form. This is a lot of work for the server and not recommended.
import React, { Component } from "react";
import Form from "react-jsonschema-form";
import { instantUpdateStrategy } from "react-jsonschema-form-manager";
let FormToDisplay = withManager(configResolver, manager, instantUpdateStrategy)(Form);
class ResultForm extends Component {
render() {
return (
<FormToDisplay onChange={() => onSuccessChange}/>
);
}
}intervalUpdateStrategy updates request in specified interval of time, it takes timeout as parameter.
For example, in order to update server every 100 seconds configuration would look something like this:
import React, { Component } from "react";
import Form from "react-jsonschema-form";
import { intervalUpdateStrategy } from "react-jsonschema-form-manager";
let updateStrategy = intervalUpdateStrategy(100000);
let FormToDisplay = withManager(configResolver, manager, updateStrategy)(Form);
class ResultForm extends Component {
render() {
return (
<FormToDisplay/>
);
}
}As with other components you can easily override UpdateStrategy with custom implementation.
Construction of the object is done with a currying pattern, you can define any parameters you want on first call,
and you will get manager when updateStrategy is going to be used. Returned object needs 2 methods onChange and stop.
stopget called when component unMounts, or after successful submitonChangecalled when everformDatachanges, in originalForm
For example you want to update only on even dates, this would look something like this:
export function evenDaysUpdateStrategy() {
return (manager) => {
return {
onChange: () => {
if (new Date().getDate() % 2 == 0) {
manager.updateIfChanged();
}
},
stop: function() {};
};
};
}If you want to track server data updates, you can do this by specifying onUpdate callback on rendered form
...
let FormToDisplay = withManager(configResolver, manager, updateStrategy)(Form);
class ResultForm extends Component {
render() {
return (
<FormToDisplay onUpdate={() => console.log("Data updated")}/>
);
}
}- Issue Tracker: github.com/RxNT/react-jsonschema-form-manager/issues
- Source Code: github.com/RxNT/react-jsonschema-form-manager
If you are having issues, please let us know here or on StackOverflow.
The project is licensed under the Apache Licence 2.0.