Formule is a powerful, user-friendly, extensible and mobile-friendly form building library based on JSON Schema and RJSF, which aims to make form creation easier for both technical and non-technical people.
It originated from the need of a flexible tool for physicists at CERN to create their custom forms in the CERN Analysis Preservation application (a process that was originally done by the CAP team who had to manually define the JSON schemas for every member experiment) in a zero-code fashion. This tool proved to be very useful for us to more easily scalate and expand, reaching a wider audience here at CERN. So, we thought it could also be useful for other people and decided to decouple it from CAP and release it as an open source library.
[!WARNING] react-formule has just come out and is undergoing active development, so please feel free to share any issue you find with us and/or to contribute!
A simple setup (see ./formule-demo
) could look like this:
Formule consists of the following main components:
-
FormuleContext
: Formule components need to be wrapped by a FormuleContext. It also allows you to provide an antd theme and your own custom fields and widgets. - The form editor, which has been split into three different components that work together for more flexibility:
-
SelectOrEdit
(or, separately,SelectFieldType
andPropertyEditor
): You can select fields to add to the form and customize their properties. -
SchemaPreview
: A tree view of the fields where you can rearrange or select fields to be edited. -
FormPreview
: A live, iteractive preview of the form which lets you toggle between the editable and the published version. If you only want to show the editable version, useEditablePreview
instead.
-
-
FormuleForm
: You can use it to display a form (JSON Schema) generated by Formule. The editable version will be displayed by default. You can passisPublished
if you want to see the published version.
It also exports the following functions:
-
initFormuleSchema
: Inits or resets the JSONSchema. You can also load an existing schema by passing it as an argument. -
getFormuleState
: Formule has its own internal redux state. You can retrieve it at any moment if you so require for more advanced use cases. If you want to continuosly synchronize the Formule state in your app, you can pass a callback function to FormuleContext instead (see below), which will be called every time the form state changes.
And the following utilities:
-
CodeEditor
: Useful if you want to edit the JSON schemas (or any other code) manually. -
CodeViewer
: Useful if you want to visualize the JSON schemas that are being generated (as you can see in the demo). -
CodeDiffViewer
: Useful if you want to compare two different JSON schemas, for example to see the changes since the last save.
As well as the following utility functions to handle saving and loading schemas from local storage if you need and for unsaved change detection:
getAllFromLocalStorage
saveToLocalStorage
deleteFromLocalStorage
loadFromLocalStorage
isUnsaved
Have a look at src/index.ts
to see all exported components and functions. You can also have a look at formule-demo
to see how they are used there.
Formule includes a variety of predefined field types, grouped in three categories:
-
Simple fields:
Text
,Text area
,Number
,Checkbox
,Switch
,Radio
,Select
andDate
fields. -
Collections:
-
Object
: Use it of you want to group fields or to add several of them inside of aList
. -
List
: It allows you to have as many instances of a field orObject
as you want. -
Accordion
: It works as aList
with collapsible entries. -
Layer
: It works as aList
whose entries will open in a dialog window. -
Tab
: It's commonly supposed to be used as a wrapper around the rest of the elements. You will normally want to add anObject
inside and you can use it to separate the form in different pages or sections.
-
-
Advanced fields: More complex or situational fields such as
URI
,Rich/Latex editor
,Tags
,ID Fetcher
,Code Editor
andFiles
.
You can freely remove some of these predefined fields and add your own custom fields and widgets following the JSON Schema specifications. More details below.
All of these items contain different settings that you can tinker with, separated into Schema Settings (generally affecting how the field works) and UI Schema Settings (generally affecting how the field looks like).
npm install react-formule
# or
yarn add react-formule
import {
FormuleContext,
SelectOrEdit,
SchemaPreview,
FormPreview,
} from "react-formule";
return (
<FormuleContext>
<SelectOrEdit />
<SchemaPreview />
<FormPreview />
</FormuleContext>
);
Override (if existing) or create your own field types (rjsf type definitions) similarly to how it's done in fieldTypes.jsx
, passing them as customFieldTypes
. Implement your own custom fields and widgets (react components) by passing them as customFields
and/or customWidgets
(see forms/fields/
and forms/widgets/
for examples). If you also want to use a different published version of a field or widget, pass the component in customPublishedFields
or customPublishedWidgets
. You can read more about the difference between fields and widgets and how to customize or wrap them in the rjsf docs, but make sure you provide Formule with something like the following:
const CustomWidget = ({value, required, onChange}) => {
return (
<input
type='text'
className='custom'
value={value}
required={required}
onChange={(event) => onChange(event.target.value)}
/>
);
};
const customFieldTypes = {
advanced: {
myCustomWidget: {
title: ...
...
}
}
}
const customWidgets: {
myCustomWidget: CustomWidget
}
<FormuleContext
theme={{token: {colorPrimary: "blue"}}} // antd theme
customFieldTypes={customFieldTypes}
customFields={...}
customWidgets={customWidgets}
customPublishedFields={...}
customPublishedWidgets={...}>
// ...
</FormuleContext>
If you use Formule to edit existing JSON schemas that include extra fields (e.g. metadata fields) that you don't want to show up in the Formule editor (i.e. in SchemaPreview
and SchemaTree
), you can use transformSchema
to exclude them:
const transformSchema = (schema) => {
// Remove properties here...
return transformedSchema;
};
<FormuleContext transformSchema={transformSchema}>/* ... */</FormuleContext>;
You can add a custom transformErrors
function to process, edit or filter the errors from RJSF in the way that best suits our needs:
const transformErrors = (errors) => {
return errors.filter(...)
};
<FormuleForm transformErrors={transformErrors} />
If you want to run some logic in your application every time the current Formule state changes in any way (e.g. to run some action every time a new field is added to the form) you can pass a function to be called back when that happens:
const handleFormuleStateChange = (newState) => {
// Do something when the state changes
};
<FormuleContext synchonizeState={handleFormuleStateChange}>
// ...
</FormuleContext>;
Alternatively, you can pull the current state on demand by calling getFormuleState
at any moment.
If you want to prefill the form with existing data, you can provide the form data to FormuleForm
. This will fill in the corresponding fields with the information in formData
:
<FormuleForm
formData={{
name: "Mule",
age: 20,
weight: 370,
}}
/>
In order to keep Formule's philosophy of storing forms and completion data as simple JSON objects, Formule doesn't directly store files. Instead, it stores only UIDs and leaves the specifics of how, where and when to store the corresponding files up to the user.
More info about fetching and storing files with examples
In order to fetch files from a URL (which can be your backend or a public URL), you will have to provide a fetchFile
callback function in customFunctions
. Formule will call this function when first loading a Files field for each of the file UIDs associated to this field, passing the file UID. This function should return a file URL.
<FormuleContext
customFunctions={{
file: {
// You can either directly return the file URL (useful also if you want
// to do some kind of caching):
fetchFile: (uid) => {
return `https://example.com/files/${uid}`;
},
// Or, if you need to manage e.g. authentication, you can always fetch
// the image yourself, doing any processing you find necessary and finally
// create an object URL with URL.createObjectURL() and return it:
fetchFile: (uid) => {
return fetch(`https://example.com/files/${uid}`)
.then((response) => response.blob())
.then((blob) => URL.createObjectURL(blob));
},
},
}}
>
// ...
</FormuleContext>
Formule temporarily stores object URLs of files uploaded in the current session in the Formule state under files.new
. If you want to persist files you can simply monitor the formule state (see Syncing Formule state) and whenever you want to save them (usually you will want to do it on submission, but you could do it on change) you can read new files from files.new
and deleted files from files.deleted
. You can then use that data to trigger the corresponding upload and delete actions in your backend.
// Example of a custom function to handle form submission
// Only showing file-related logic
// Assumes you have synchronized the formule state on formuleState
const handleSubmit = () => {
// Upload new files
formuleState.files.new.map({uid, url} => {
const response = await fetch(url);
const blob = await response.blob();
const formData = new FormData();
formData.append('file', blob);
formData.append('uid', uid);
const uploadResponse = await fetch('https://example.com/upload', {
method: 'POST',
body: formData,
});
// handle response...
})
// Remove deleted files. You only need to handle this for form edition
// (forms already filled-in and saved that are being further modified)
// unless you are persisting files on change instead of on submission.
formuleState.files.deleted.map(uid => {
const response = await fetch(`https://example.com/files/${uid}`, {
method: 'DELETE',
});
// handle response...
});
};
FormuleAI brings Artificial Intelligence capabilities to Formule, allowing users to generate and modify form schemas using natural language prompts. This feature leverages Large Language Models to understand user requirements and automatically create or update form structures.
FormuleAI integrates AI providers (like OpenAI or Gemini) to process natural language requests and generate corresponding JSON schemas. The AI understands the current form context and can add new fields, modify existing ones, or restructure entire sections based on your prompts.
By default FormuleAI includes providers for OpenAI and Gemini, and has been tested to work particularly with GPT 4.1 Mini
and Gemini 2.0 Flash
, although it allows you to choose any other model offered by these providers. You can also provide your own API key.
Once you send a request and receive a response back from the LLM, a popover will be displayed showing the diff between the current form and the suggestion, as well as between both JSON schemas, and you will be able to either reject or approve the changes. In the FormuleAI settings, you can also activate the "Vibe Mode", which will auto-apply any change without showing you a diff and asking for approval (use at your own risk).
Note: For the moment FormuleAI doesn't keep conversation history, so make sure to always be clear and provide all necessary details in each request.
The main component you will need to use is AiChatFooter
, a chat interface where users can input their prompts, toggle diffs, and accept or reject changes. It will also allow you to configure some settings and provide some usage instructions.
It can receive the following props: onApply
and onReject
callbacks, hideHelp
and hideSettings
, and vibeMode
(false to disable, true to enable, unset to leave the decision up to users via settings).
A basic configuration would be simply:
import { FormuleContext, AiChatFooter } from "react-formule";
<FormuleContext>
// your other main formule components
<AiChatFooter />
</FormuleContext>;
Adding custom providers and advanced configuration
You can add a new provider, whether commercial or self-hosted, in the following way:
import { defaultProviders } from "react-formule";
const customProviders = {
"local-llama": {
label: <span>Local llama</span>,
// Optional, otherwise users can provide their own via settings
apiKey: "your-api-key",
// Optional, otherwise users can select one via settings (you need to define fetchModels for that)
model: "llama3.1",
// Optional (not needed when providing a model), it preselects that model in the model list
recommendedModel: { id: "llama3.1", name: "LLaMA 3.1" }
// Optional, needed only if you don't provide a model
fetchModels: async (apiKey) => {
const response = await fetch("https://your-ai-endpoint/models", {
headers: { Authorization: `Bearer ${apiKey}` },
});
const data = await response.json();
return data.models.map((model) => ({
id: model.id,
name: model.display_name,
}));
},
generateSchema: async (
prompt,
currentSchema,
fieldTypes,
apiKey,
model
) => {
const response = await fetch("https://your-ai-endpoint", {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}` },
body: JSON.stringify({
model: model,
messages: [
{
role: "system",
content: "<your system prompt>",
},
{
role: "user",
content: "<your user prompt including fieldTpes and currentSchema>"
}
],
response_format: { type: "json_object" }
}),
});
return {
schema: content.schema,
uiSchema: content.uiSchema,
usage, // Optionally you can include token usage stats to be displayed to users
};
// If error, `return { error: "the error message" }` instead
},
},
};
<FormuleContext ai={{ providers: { ...defaultProviders, ...customProviders } }}>
// ...
</FormuleContext>;
If you want to keep the default providers along with your custom ones, you can import defaultProviders
and include it in your providers object (as you can see in the previous example). Otherwise your new providers will override that configuration.
If an API key or a model is defined in a provider, users will not be able to modify them in the settings for that provider.
FormuleAI exports several utilities for advanced use cases:
-
useGenerateSchema
: Hook for triggering schema generation programmatically -
useGetProvider
: Hook to access configured AI providers. It will return the provider selected by the user (from localStorage) or otherwise a valid provider which is fully configured (with API key and model) inai.providers
if any. -
generatePatches
: Utility to create JSON patches between schemas, used by FormuleAI for the form diff. -
defaultProviders
: Configuration of the default providers, mentioned in the examples above. -
defaultGenerationPrompt
: The default system prompt used by the current providers. It can help as a starting point to experiment with custom providers, but you will likely have to do some adjustments for each one.
There are also two more components that you would normally not need to use explicitly (they are already used by default from AiChatFooter
) but which are still exposed to give you more flexiility in case you want further customization or to use them on your own custom chat interface implementation:
-
AiDiff
: Shows a visual diff of proposed changes before applying them -
AiSettingsDialog
: Configuration panel for API keys and model selection
For implementation examples and advanced configurations, refer to the default provider implementations in the codebase.
Apart from trying the online demo you can clone the repo and run formule-demo
to play around. Follow the instructions in its README: it will explain how to install react-formule
as a local dependency so that you can modify Formule and test the changes live in your host app, which will be ideal if you want to troubleshoot or contribute to the project. Your contributions are welcome! 🚀
[!TIP] For more examples, feel free to browse around formule-demo and the CERN Analysis Preservation repository, where we use most of the features mentioned above.