Form Renderer
A technical deep-dive into the powerful Form.io JavaScript Renderer
One of the most significant differences between Form.io and other form products is how forms are rendered within applications. Most form platforms render forms on the Server (using technologies like PHP, .NET, Java, etc) and then send the rendered Form HTML up to the browser to be used. This approach has many problems, especially when being utilized within a modern mobile-first web application. In order to service the new Progressive Web Applications being developed today, Form.io has completely redefined how forms should be rendered so that they are flexible, extensible, and performant. Forms created within the Form.io builder are actually represented as JSON schemas that are then rendered directly within the application using a JavaScript rendering engine that we call the "Form Renderer". Here is an image that illustrates how this rendering works within a mobile application.

How the Form.io JavaScript Renderer works
The library responsible for this rendering can be found on Github @ https://github.com/formio/formio.js and is also Open Source so that any developer can fork and extend the functionalities provided by this library.
In order to fully understand how the Form.io Renderer works, it is important to try out the JavaScript renderer using real examples that you can easily walk through. To start, we will first create a new Project within the Form.io Portal @ https://portal.form.io, and then create a new simple form that we will use to test out the JavaScript renderer. Here is just an example of what your form may look like.

We will also want to make sure we copy the Form API URL path (circled in the picture above) and save this for later when we wish to render the form. Now that we have our form, we can test out how this form will be rendered.
To test our JavaScript Renderer, you can use one of many online web application editors. The ones that we recommend are:
For this demonstration, we will just go to JSFiddle and first add the following Resources.
- https://cdn.form.io/formiojs/formio.form.min.js
- https://cdn.form.io/formiojs/formio.form.min.css
- https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css
These urls are described as follows:
- formio.form.min.js - This is the minified JavaScript source code for the Form.io Renderer
- formio.form.min.css- This is the minified CSS style sheets for the Form.io Renderer
- bootstrap.min.css - This is the Bootstrap CSS Framework which Form.io uses to render forms.
Your JSFiddle should now look like the following.

This is the exact same thing as if you were building an HTML application from scratch and had the following HTML code.
<!doctype html>
<html lang="en">
<head>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.form.io/formiojs/formio.form.min.css" crossorigin="anonymous">
</head>
<body>
<script src="https://cdn.form.io/formiojs/formio.form.min.js" crossorigin="anonymous"></script>
</body>
</html>
Next, we will add a place where we will render the form. We can do this by adding a single
div
tag into the HTML region and then giving it an "id" so that we can refer to it within our JavaScript initialization code (which we will write later). We can copy the following code into the HTML section of JSFiddle<div id="formio"></div>
It should look like the following.

This is the exact same thing as if we did the following in raw HTML
<!doctype html>
<html lang="en">
<head>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.form.io/formiojs/formiojs.form.min.css" crossorigin="anonymous">
</head>
<body>
<div id="formio"></div>
<script src="https://cdn.form.io/formiojs/formiojs.form.min.js" crossorigin="anonymous"></script>
</body>
</html>
And now for the fun part. We will now use the Form.io JavaScript SDK to instantiate the form that we just built within the Form.io Portal. We will do this by writing the following code within the JavaScript section of our JSFiddle.
Formio.createForm(document.getElementById('formio'), 'https://myproject.form.io/renderertest');
Make sure you replace the project name "myproject" with the name of your project, and assuming that you named the test form the same as our example, you will see the following when you run the application in JSFiddle.

Here is the JSFiddle link for you to try this yourself.
The Form.io renderer is very flexible and as such can be configured to achieve many different use cases. Because of this, we have built a dedicated application that is used to demonstrate many of the features that our form renderer has to offer. This can be found at the following url.
In addition to rendering a URL, the form renderer can also be used to render simple JSON passed into the renderer like so.
Formio.createForm(document.getElementById('formio'), {
components: [
{
type: 'textfield',
key: 'firstName',
label: 'First Name'
},
{
type: 'textfield',
key: 'lastName',
label: 'Last Name'
},
{
type: 'email',
key: 'email',
label: 'Email'
},
{
type: 'button',
key: 'submit',
label: 'Submit'
}
]
});
Which will render the form as follows.

If you wish to get the JSON of a form as you build it with the form builder, then we suggest that you check out the Form.io Builder Sandbox which can be found @ https://formio.github.io/formio.js/app/builder​
Now that we have rendered a form, the next step will be to inject a submission into the form so that it will show data pre-populated within the form. One interesting thing to note about Form.io is that it completely separates the Form JSON from the Submission JSON and they are treated as separate JSON entities. Submission data will never be included as part of the form JSON. For example, the following Form JSON
{
"components": [
{
"type": "textfield",
"key": "firstName",
"label": "First Name"
},
{
"type": "textfield",
"key": "lastName",
"label": "Last Name"
}
]
}
will take the following submission data JSON.
{
"data": {
"firstName": "Joe",
"lastName": "Smith"
}
}
There are a couple of things to note here.
- 1.The submission data for the form is contained within a "data" object.
- 2.The "keys" for the submission data is determined by the "key" property of each component within the form.
You can alter the data structure by using dot-notation in the Form JSON which will allow you to alter the data construct of the submission. For example, if you wish to include the firstName and lastName fields within a "customer" data object, you could do the following.
{
"components": [
{
"type": "textfield",
"key": "customer.firstName",
"label": "Customer First Name"
},
{
"type": "textfield",
"key": "customer.lastName",
"label": "Customer Last Name"
}
]
}
The dot-notation for the "keys" in this form tell the renderer to structure the submission as follows.
{
"data": {
"customer": {
"firstName": "Joe",
"lastName": "Smith"
}
}
}
Another way to alter the submission data construct is to use any of the Data Components within a form. These are special components that are used to not only visually show the data being collected in a data structured way, but will also change the data structure of the submissions being produced by the rendered form. For example, if you wish to produce the following submission data which is an array of children's first and last names, you can use the Data Grid component as follows.
{
"components": [
{
"type": "datagrid",
"label": "Children",
"key": "children",
"components": [
{
"type": "textfield",
"key": "firstName",
"label": "First Name"
},
{
"type": "textfield",
"key": "lastName",
"label": "Last Name"
}
]
}
]
}
Produces the following submission JSON.
{
"data": {
"children": [
{
"firstName": "Joe",
"lastName": "Smith"
},
{
"firstName": "Mary",
"lastName": "Thompson"
]
}
}
And for the Data Grid component, it looks like the following when being rendered.

There are other kinds of data components as described as follows.
Component | Type | Description |
Data Grid | datagrid | Spreadsheet UI that stores an array of objects |
Edit Grid | editgrid | Table UI that stores an array of objects, with inline edit |
Data Map | datamap | Key-value pair where string key can be provided for dynamic values |
Data Table | datatable | Grid UI that stores an array of objects |
Container | container | Hidden container UI that stores components inside an isolated object. |
Hidden | hidden | Hidden UI that can store any data value in any data structure. |
In order to render a submission, you must first render the form and then set the form submission to the submission data you wish to render within the form. You can either render the form as JSON or as a form URL as described above and then the submission is set once the form is done rendering. As a simple example, you can provide the following to demonstrate how a submission can be rendered within a form.
Formio.createForm(document.getElementById('formio'), {
components: [
{
type: 'textfield',
key: 'firstName',
label: 'First Name'
},
{
type: 'textfield',
key: 'lastName',
label: 'Last Name'
}
]
}).then((form) => {
form.submission = {
data: {
firstName: 'Joe',
lastName: 'Smith'
}
};
});
Which will render as the following.

You can also render the submissions from a Form URL as the following demonstrates.
Formio.createForm(document.getElementById('formio'), 'https://examples.form.io/example')
.then((form) => {
form.submission = {
data: {
firstName: 'Mary',
lastName: 'Thompson',
email: '[email protected]'
}
};
});
which will render as follows.

The last way to render a submission is to render the submission via the API endpoint of that submission. This Submission API is described as follows.
get
https://yourproject.form.io
/:formName/submission/:submissionId
Retrieve a form submission
In order to utilize this rendering correctly, you will need to ensure that the user you are authenticated as has access to this submission. You can authenticate a user by setting a valid JWT token within the "formioToken" localStorage variable of your application.
This URL can then be added to the renderer to render a complete form with submission as follows.
Formio.createForm(
document.getElementById('formio'),
'https://examples.form.io/wizard/submission/5a542c9e2a40bf0001e0f8a9'
);
Which will render the form and submission as follows.

One of the most powerful concepts of Form.io rendered forms is that you can control the rendered form using JavaScript. In most cases, this is done within the section of code that executes once the form has finished rendering. This is commonly referred to as the "Form Controller" section of the form renderer and can be seen as follows.
Formio.createForm(document.getElementById('formio'), 'https://examples.form.io/example')
.then((form) => {
// This section of code is the "Form Controller"
});
The variable that is passed to this function can be called whatever you want, but form or instance are very common names. This variable is actually the Webform instance of the following source code.
Because of this, any method within these classes (and their derived classes) can be executed by referencing them on the form or instance variable. So many things can be accomplished using these variables. Here are just a few use cases that be done.
// This section of code is the "Form Controller"
form.on('change', (changed) => {
console.log('Data was changed!', changed);
});
// This section of code is the "Form Controller"
form.on('submitDone', function(submission) {
window.location = '/app/thanks.html';
});
// This section of code is the "Form Controller"
​
/**
* This code assumes a "wizard" is rendered, and that there are buttons in the
* wizard form that emit the events "gotoNextPage", "gotoPreviousPage" and
* "wizardSave"
**/
​
form.on('gotoNextPage', function() {
form.nextPage();
});
form.on('gotoPreviousPage', function() {
form.prevPage();
});
form.on('wizardSave', function() {
form.submit().then(function() {
form.onChange();
form.nextPage();
});
});
This is such a powerful concept that there is actually a feature called the Form Controller where these controllers can be added to the form JSON and then will be executed in the same fashion as these indicate. This can be configured within the Form Settings of the form and you would use the variable name instance instead of form as shown above.
You can manually set the next page to navigate to by using:
instance.root.setPage(pageNumber);
An example of a button that does this in a Wizard would look like this:

In addition to rendering a form and providing submission data, you can also provide options to the renderer to control its behavior even further. The options are passed as the third parameter to the
Formio.createForm
method as shown below.Formio.createForm(element, src|form, options)
The options available are documented as follows.
Option | Description | Default |
readOnly | Disables all input is set to true | false |
noDefaults | Do not establish default submission values. Leave unset unless the user interacts with the form. | false |
language | The current language for the renderer | en |
i18n | {} | |
viewAsHtml | Boolean to tell the renderer to render this form in "html" mode. | false |
renderMode | The mode that the form should render within. This picks different render modes within the templates section. See Form Templates section for more information. | form |
highlightErrors | Highlight the errors for each field. | true |
componentErrorClass | The default CSS class to be applied to the error dom element. | formio-error-wrapper |
template | The current template name | ​ |
templates | ​ | |
iconset | The iconset to use within the renderer. | ​ |
buttonSettings | For wizards only. Controls the settings and visibility of the wizard button settings. These are configured as follows. ​ { "buttonSettings": {
"showCancel": true,
"showNext": true, "showPrevious": true "showSubmit": true
} } | {} |
components | Allows for overrides for certain components rendered. ​ Example: Adds a prefix to all textfield components rendered. { "components": {
"textfield": {
"prefix": "hello"
}
} } | {} |
disabled | Allows for component overrides for disabled fields. ​ Example: Disable the firstName component. { "disabled": {
"firstName": true
} } | {} |
showHiddenFields | Boolean that, when set to true, will show all the hidden fields regardless of conditionals. | false |
hide | Force certain components to be hidden regardless of conditionals. ​ Example: Hide the firstName and lastName components. { "hide": {"firstName": true, "lastName": true} } | ​ |
show | Force certain components to be shown regardless of conditionals. ​ Example: Show the firstName and lastName components. { "show": {"firstName": true, "lastName": true} } | ​ |
formio | Your own instance of the Formio class. | ​ |
decimalSeparator | Used for Number components. Determines the decimalSeparator. Defaults to the browser default setting. | ​ |
thousandsSeparator | Used for Number components. Determines the thousands separator. Defaults to the browser default setting. | ​ |
fileService | A custom File Service instance. | ​ |
hooks | ​ | |
alwaysDirty | If set to true, the form will validate on initialization. | false |
saveDraft | ​ | |
saveDraftThrottle | The Save As Draft feature is invoked at most once per each period defined by this value in milliseconds. | 5000 |
skipDraftRestore | If set to true, the form will not restore a previous draft when rendered. | ​ |
display | The type of form to be rendered. Must be 'form' (for a WebForm class), 'wizard' (for a Wizard), or 'pdf' (for a PDF form). | form |
cdnUrl | The CDN URL for the renderer's dependency libraries | https://cdn.form.io |
flatten | If set to true, will always render the form as a WebForm. Similar to setting the renderMode option to 'flat'. | ​ |
sanitize | If set to false, the renderer will skip sanitization of HTML strings. | ​ |
sanitizeConfig | An object that maps to the renderer's HTML sanitization settings. Each object property maps to a DOMPurify configuration option.
Example:
{
addAttr: [/* an array that maps to DOMPurify's ADD_ATTR setting */],
addTags: [/* an array that maps to DOMPurify's ADD_TAGS setting */],
allowedTags: [/* an array that maps to DOMPurify's ALLOWED_TAGS setting */],
allowedAttrs: [/* an array that maps to DOMPurify's ALLOW_ATTR setting */],
allowedUriRegex: [/* an array that maps to DOMPurify's ALLOWED_URI_REGEXP setting */],
addUriSafeAttr: [/* an array that maps to DOMPurify's ADD_URI_SAFE_ATTR setting */]
} | ​ |
buttonSettings (Wizard Forms) | The Wizard Form's button settings. | {
showPrevious: true, showNext: true, showSubmit: true, showCancel: !options.readOnly
} |
breadCrumbSettings (Wizard Forms) | Determines whether or not the Wizard Form's breadcrumb bar is clickable. | { clickable: true } |
allowPrevious (Wizard Forms) | Allow the Wizard Form to decrement pages. | ​ |
wizardButtonOrder (Wizard Forms) | An array of values that determines the order in which the wizard buttons are displayed. | ['cancel', 'previous', 'next', 'submit'] |
showCheckboxBackground (PDF Forms) | When set to true, ensures that PDF form checkboxes and radio components have a border and a background | ​ |
zoom (PDF Forms) | Change the initial zoom value of the PDF. | ​ |
These options can be applied to the renderer like the following example shows.
Example: Render a submission in read only mode.
Formio.createForm(document.getElementById('formio'), 'https://examples.form.io/wizard/submission/5a542c9e2a40bf0001e0f8a9', {
readOnly: true
});
There are many different properties and methods that can be called on the form instance. Many of these methods are documented within the auto-generated SDK documentation found @ https://formio.github.io/formio.js/docs/​
Within these documentations, you will see some of these methods described as follows.
Form Class | Documentation |
Webform | |
PDF | |
Wizard |
While this documentation is helpful to understanding all the methods, here are few of the most used properties on the form instance.
The current form JSON that is loaded into the form.
The current form submission JSON that is loaded into the form.
A minified "schema" of the components loaded into the renderer. This is dynamically generated so use sparingly.
A promise that is resolved when the form has finished rendered, submission data has been saturated, and the form is "ready".
form.ready.then(() => {
form.submission = {
data: {
firstName: 'Joe',
lastName: 'Smith'
}
};
});
Boolean to indicate if the form is currently loading (true) or not (false).
The current form source that is loaded within the renderer.
The current language for this form.
Here are few of the most used methods on the form instance.
Sets the JSON schema for the form to be rendered. It returns a promise that resolves when the form has been completely rendered and attached.
Parameter | Description |
form | The JSON schema of the form |
flags | Optional flags to control the behavior of the change event loop. |
form.setForm({
components: [
{
type: 'textfield',
key: 'firstName',
label: 'First Name',
placeholder: 'Enter your first name.',
input: true
},
{
type: 'textfield',
key: 'lastName',
label: 'Last Name',
placeholder: 'Enter your last name',
input: true
},
{
type: 'button',
action: 'submit',
label: 'Submit',
theme: 'primary'
}
]
});
This is a "setter" alias for
form.setForm.
It can be used as follows.form.form = {
components: [
{
type: 'textfield',
key: 'firstName',
label: 'First Name',
placeholder: 'Enter your first name.',
input: true
},
{
type: 'textfield',
key: 'lastName',
label: 'Last Name',
placeholder: 'Enter your last name',
input: true
},
{
type: 'button',
action: 'submit',
label: 'Submit',
theme: 'primary'
}
]
};
Sets a submission and returns the promise when it is ready.
Parameter | Description |
submission | The submission JSON you wish to set. |
flags | Flags to control the behavior of the change event loop |
This is a "setter" alias for
form.setSubmission
. It can be used as follows.Set's the "src" of the rendered form. This is the API endpoint for either the Form URL, or the Submission URL. If you provide just a form src, then it will only load the form. If you provide a Submission URL, then it will load both the form and then saturate that form with the submission data.
A "setter" alias for
form.setSrc
.Sets the language of the renderer.
Sets the form to start loading (showing the spinner icon).
Saves a draft submission.
Restores a draft submission for a specific user ID
Force a redraw of the form.
Force a reset value on the form.
Submit the form.
Performs a check on the submission data for calculations, conditionals, and validations.
Iterate through every component within the form.
Parameter | Description |
fn | A callback that will be called for every component. The signature for this component is as follows. ​ fn(component, components, index)
|
form.everyComponent((component) => {
if (component.component.key === 'firstName') {
component.setValue('Joe');
}
});
Retrieve a component from the form.
Parameter | Description |
path|key | The key of the component you wish to fetch, or the data path of that component. |
fn | Callback function to be called when the component is found. |
const email = form.getComponent('email');
email.setValue('[email protected]');
Checks the form validity and updates the errors if the validity fails.
Parameter | Description |
data | The data to check against. If no data is provided, then the submission data in context will be used. |
dirty | If this should force the "dirty" flag on the components when performing the checks. If "true" this will highlight all invalid form fields as red on the form. |
row | The row data to check against. If no data is provided, then the contextual row data will be used. |
silent | If "true", then this will perform a passive check for validity and will not affect the visuals of the form and highlight any fields red if they are invalid. |
if (!form.checkValidity(null, false, null, true)) {
alert('The form is invalid!);
}
Within the Form.io renderer, there are a number of events that are fired that allow you to respond to these events and then do something once they fire. A very common example of this is to listen for anytime someone changes a field within the form, log that change for audit reasons.
The Form.io renderer uses the EventEmitter3 library to manage all of the event handling that occurs within the renderer, and because of this, any method that this library includes can also be used within the renderer, as the following example illustrates.
// Listen for change events and log the changes as they occur.
form.on('change', (changed) => {
console.log(changed);
});
The following events are triggered within the Form.io renderer.
Event | Description | Arguments |
change |