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
REST
orLocalStorage
api for testing - Configurable form updates on data change, with
PUT
requests - Static and
REST
form configuration support - Flexible authentication management
Install react-jsonschema-form-manager
by running:
npm install --s react-jsonschema-form-manager
The 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 (
static
orREST
) - Manager (
localStorage
orREST
) - Update strategy (
imediate
orinterval
)
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
form
with loaded configuration, or display anErrorScreen
screen 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
config
setting - 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
,
config
is the configuration to returntimeout
delay 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
url
configuration url to usecredentials
authentication to use with urloutputHandler
manipulate 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
url
configuration url to usecredentials
authentication 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
submit
called when form is submitted, should returnPromise
that will be resolved after successful submissionupdateIfChanged
called 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, orundefined
if there is nothing to change.updateIfChanged
acceptsforce
flag that will always result in update.onChange
called 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
.
stop
get called when component unMounts, or after successful submitonChange
called when everformData
changes, 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.