Custom Components
Describes the process of building and creating a Custom Form Component.

Introduction

One of the more powerful features of the Form.io platform is the ability to create your own custom form components. The process of creating a custom component involves extending a Base class of the component that is "closest" to the implementation you desire, and then override methods or introduce new methods that will implement your custom logic. All of the existing components within the Form.io platform also use this same method, so you can see the multitude of examples by inspecting how the base components for the Form.io renderer are implemented. Here is the link to all of the existing components for the Form.io renderer.
formio.js/src/components at master · formio/formio.js
GitHub
As you can see, the implementation of any component involves extending a base class and then creating your own implementation on top of those base classes.
In order to create the best components, it is important to understand the critical methods used to define a new component. They are as follows.

Extending Components

Every custom component will derive from a base class, whose behavior is closest to the behavior of the component you wish to create. It is possible to extend any other component within the Form.io renderer and a list of all of these components and their classes can be found @ https://github.com/formio/formio.js/tree/master/src/components
Because of this, the first task in building a custom component is to determine which component most closely resembles the behavior and data model of the component you are looking to achieve. For example, if you wish to build a multi-button select component, it may be best to start with a Radio component since this is the component that most closely resembles the behavior of the component you wish to create.
If you are unsure, then it is also fine to derive from the "core" components which serve as the base for all other components within the renderer. These core components are as follows.

Core Components

Class
Extends
Description
Component
Element
Base component class
Field
Component
Component that derives from Component class that implements a "field" render template
Multivalue
Field
A component that is able to implement the "multiple" configuration allowing for multiple inputs for this field type.
Input
Multivalue
A component type that implements an HTML value input.
Each of the components can be extended by first referencing them from the Components.components object, and then extending them as follows.
1
const Input = Formio.Components.components.input;
2
class MyInput extends Input {
3
...
4
...
5
}
Copied!
For the most generic components, it is fine to derive from "Component", but in most value components, you may wish to derive from the Input component.

Component Methods

Once you derive from a base component, the next step is to define methods that either override base behavior or introduce new behavior into the component class. It is recommended to look at the source code of your "base" component and the classes that it extends to understand what methods you have available to you, but the majority of all behavior can be achieved by implementing some of the following methods.
1
const Input = Formio.Components.components.input;
2
3
class MyComponent extends Input {
4
/**
5
* This is the default schema of your custom component. It will "derive"
6
* from the base class "schema" and extend it with its default JSON schema
7
* properties. The most important are "type" which will be your component
8
* type when defining new components.
9
*
10
* @param extend - This allows classes deriving from this component to
11
* override the schema of the overridden class.
12
*/
13
static schema(...extend) {
14
return Input.schema({
15
type: 'mycomp',
16
label: 'My Component',
17
key: 'mycomp',
18
});
19
}
20
21
/**
22
* This is the Form Builder information on how this component should show
23
* up within the form builder. The "title" is the label that will be given
24
* to the button to drag-and-drop on the buidler. The "icon" is the font awesome
25
* icon that will show next to it, the "group" is the component group where
26
* this component will show up, and the weight is the position within that
27
* group where it will be shown. The "schema" field is used as the default
28
* JSON schema of the component when it is dragged onto the form.
29
*/
30
static get builderInfo() {
31
return {
32
title: 'My Component',
33
icon: 'terminal',
34
group: 'basic',
35
documentation: '/userguide/#textfield',
36
weight: 0,
37
schema: MyComponent.schema()
38
};
39
}
40
41
/**
42
* Called when the component has been instantiated. This is useful to define
43
* default instance variable values.
44
*
45
* @param component - The JSON representation of the component created.
46
* @param options - The global options for the renderer
47
* @param data - The contextual data object (model) used for this component.
48
*/
49
constructor(component, options, data) {
50
super(component, options, data);
51
}
52
53
/**
54
* Called immediately after the component has been instantiated to initialize
55
* the component.
56
*/
57
init() {
58
super.init();
59
}
60
61
/**
62
* For Input based components, this returns the <input> attributes that should
63
* be added to the input elements of the component. This is useful if you wish
64
* to alter the "name" and "class" attributes on the <input> elements created
65
* within this component.
66
*
67
* @return - A JSON object that is the attribute information to be added to the
68
* input element of a component.
69
*/
70
get inputInfo() {
71
const info = super.inputInfo;
72
return info;
73
}
74
75
/**
76
* This method is used to render a component as an HTML string. This method uses
77
* the template system (see Form Templates documentation) to take a template
78
* and then render this as an HTML string.
79
*
80
* @param content - Important for nested components that receive the "contents"
81
* of their children as an HTML string that should be injected
82
* in the {{ content }} token of the template.
83
*
84
* @return - An HTML string of this component.
85
*/
86
render(content) {
87
return super.render('<div ref="customRef">This is a custom component!</div>');
88
}
89
90
/**
91
* The attach method is called after "render" which takes the rendered contents
92
* from the render method (which are by this point already added to the DOM), and
93
* then "attach" this component logic to that html. This is where you would load
94
* any references within your templates (which use the "ref" attribute) to assign
95
* them to the "this.refs" component variable (see comment below).
96
*
97
* @param - The parent DOM HtmlElement that contains the component template.
98
*
99
* @return - A Promise that will resolve when the component has completed the
100
* attach phase.
101
*/
102
attach(element) {
103
/**
104
* This method will look for an element that has the 'ref="customRef"' as an
105
* attribute (like <div ref="customRef"></div>) and then assign that DOM
106
* element to the variable "this.refs". After this method is executed, the
107
* following will point to the DOM element of that reference.
108
*
109
* this.refs.customRef
110
*
111
* For DOM elements that have multiple in the component, you would make this
112
* say 'customRef: "multiple"' which would then turn "this.refs.customRef" into
113
* an array of DOM elements.
114
*/
115
this.loadRefs(element, {
116
customRef: 'single',
117
});
118
119
/**
120
* It is common to attach events to your "references" within your template.
121
* This can be done with the "addEventListener" method and send the template
122
* reference to that object.
123
*/
124
this.addEventListener(this.refs.customRef, 'click', () => {
125
console.log('Custom Ref has been clicked!!!');
126
});
127
return super.attach(element);
128
}
129
130
/**
131
* Called when the component has been detached. This is where you would destroy
132
* any other instance variables to free up memory. Any event registered with
133
* "addEventListener" will automatically be detached so no need to remove them
134
* here.
135
*
136
* @return - A Promise that resolves when this component is done detaching.
137
*/
138
detach() {
139
return super.detach();
140
}
141
142
/**
143
* Called when the component has been completely "destroyed" or removed form the
144
* renderer.
145
*
146
* @return - A Promise that resolves when this component is done being destroyed.
147
*/
148
destroy() {
149
return super.destroy();
150
}
151
152
/**
153
* A very useful method that will take the values being passed into this component
154
* and convert them into the "standard" or normalized value. For exmample, this
155
* could be used to convert a string into a boolean, or even a Date type.
156
*
157
* @param value - The value that is being passed into the "setValueAt" method to normalize.
158
* @param flags - Change propogation flags that are being used to control behavior of the
159
* change proogation logic.
160
*
161
* @return - The "normalized" value of this component.
162
*/
163
normalizeValue(value, flags = {}) {
164
return super.normalizeValue(value, flags);
165
}
166
167
/**
168
* Returns the value of the "view" data for this component.
169
*
170
* @return - The value for this whole component.
171
*/
172
getValue() {
173
return super.getValue();
174
}
175
176
/**
177
* Much like "getValue", but this handles retrieving the value of a single index
178
* when the "multiple" flag is used within the component (which allows them to add
179
* multiple values). This turns a single value into an array of values, and this
180
* method provides access to a certain index value.
181
*
182
* @param index - The index within the array of values (from the multiple flag)
183
* that is getting fetched.
184
*
185
* @return - The view data of this index.
186
*/
187
getValueAt(index) {
188
return super.getValueAt(index);
189
}
190
191
/**
192
* Sets the value of both the data and view of the component (such as setting the
193
* <input> value to the correct value of the data. This is most commonly used
194
* externally to set the value and also see that value show up in the view of the
195
* component. If you wish to only set the data of the component, like when you are
196
* responding to an HMTL input event, then updateValue should be used instead since
197
* it only sets the data value of the component and not the view.
198
*
199
* @param value - The value that is being set for this component's data and view.
200
* @param flags - Change propogation flags that are being used to control behavior of the
201
* change proogation logic.
202
*
203
* @return - Boolean indicating if the setValue changed the value or not.
204
*/
205
setValue(value, flags = {}) {
206
return super.setValue(value, flags);
207
}
208
209
/**
210
* Sets the value for only this index of the component. This is useful when you have
211
* the "multiple" flag set for this component and only wish to tell this component
212
* how the value should be set on a per-row basis.
213
*
214
* @param index - The index within the value array that is being set.
215
* @param value - The value at this index that is being set.
216
* @param flags - Change propogation flags that are being used to control behavior of the
217
* change proogation logic.
218
*
219
* @return - Boolean indiciating if the setValue at this index was changed.
220
*/
221
setValueAt(index, value, flags = {}) {
222
return super.setValueAt(index, value, flags);
223
}
224
225
/**
226
* Similar to setValue, except this does NOT update the "view" but only updates
227
* the data model of the component.
228
*
229
* @param value - The value of the component being set.
230
* @param flags - Change propogation flags that are being used to control behavior of the
231
* change proogation logic.
232
*
233
* @return - Boolean indicating if the updateValue changed the value or not.
234
*/
235
updateValue(value, flags = {}) {
236
return super.updateValue(...args);
237
}
238
}
Copied!

Component Modules

Custom components can be registered with the renderer using a module that is also able to include a large number of components. An example of a module that implements some custom components is the Contributed Components repo.
GitHub - formio/contrib: Contributed components for Form.io
GitHub
In this repo, you can see that each component is defined within the "components" folder as follows.
These components can then be included in the module by creating an export of these components like so.
1
import components from './components';
2
import templates from './templates';
3
export default {
4
components,
5
templates
6
};
Copied!

Using Custom Components in applications

Once these components are created as a module, they can then easily be added to the renderer using the Formio.use method as follows.
1
import { Formio } from 'formiojs';
2
import YourModule from './yourmodule';
3
Formio.use(YourModule);
Copied!
Once this is done, any new components that have been added to the renderer / builder will show up and can be used.

Examples

CheckMatrix Component

The following is an example custom component. This is a Check Matrix component that allows you to configure a component to have a grid of checkboxes which can be checked independently to save the data as a multi-dimensional array of checks. Here is what the code would look like for this example.
1
const FieldComponent = Formio.Components.components.field;
2
const editForm = Formio.Components.components.nested.editForm;
3
4
/**
5
* Here we will derive from the base component which all Form.io form components derive from.
6
*
7
* @param component
8
* @param options
9
* @param data
10
* @constructor
11
*/
12
class CheckMatrix extends FieldComponent {
13
constructor(component, options, data) {
14
super(component, options, data);
15
this.checks = [];
16
}
17
18
static schema() {
19
return FieldComponent.schema({
20
type: 'checkmatrix',
21
numRows: 3,
22
numCols: 3
23
});
24
}
25
26
static builderInfo() {
27
return {
28
title: 'Check Matrix',
29
group: 'basic',
30
icon: 'fa fa-table',
31
weight: 70,
32
documentation: 'http://help.form.io/userguide/#table',
33
schema: CheckMatrix.schema()
34
};
35
}
36
37
get tableClass() {
38
let tableClass = 'table ';
39
['striped', 'bordered', 'hover', 'condensed'].forEach((prop) => {
40
if (this.component[prop]) {
41
tableClass += `table-${prop} `;
42
}
43
});
44
return tableClass;
45
}
46
47
renderCell(row, col) {
48
return this.renderTemplate('input', {
49
input: {
50
type: 'input',
51
ref: `${this.component.key}-${row}`,
52
attr: {
53
id: `${this.component.key}-${row}-${col}`,
54
class: 'form-control',
55
type: 'checkbox',
56
}
57
}
58
});
59
}
60
61
render(children) {
62
let table = '<table class="table table-compressed"><tbody>';
63
for (let i = 0; i < this.component.numRows; i++) {
64
table += '<tr>';
65
for (let j = 0; j < this.component.numCols; j++) {
66
table += '<td>';
67
table += this.renderCell(i, j);
68
table += '</td>';
69
}
70
table += '</tr>';
71
}
72
table += '</tbody></table>';
73
return super.render(table);
74
}
75
76
/**
77
* After the html string has been mounted into the dom, the dom element is returned here. Use refs to find specific
78
* elements to attach functionality to.
79
*
80
* @param element
81
* @returns {Promise}
82
*/
83
attach(element) {
84
const refs = {};
85
86
for (let i = 0; i < this.component.numRows; i++) {
87
refs[`${this.component.key}-${i}`] = 'multiple';
88
}
89
90
this.loadRefs(element, refs);
91
this.checks = [];
92
for (let i = 0; i < this.component.numRows; i++) {
93
this.checks[i] = Array.prototype.slice.call(this.refs[`${this.component.key}-${i}`], 0);
94
95
// Attach click events to each input in the row
96
this.checks[i].forEach(input => {
97
this.addEventListener(input, 'click', () => this.updateValue())
98
});
99
}
100
101
// Allow basic component functionality to attach like field logic and tooltips.
102
return super.attach(element);
103
}
104
105
/**
106
* Get the value of the component from the dom elements.
107
*
108
* @returns {Array}
109
*/
110
getValue() {
111
var value = [];
112
for (var rowIndex in this.checks) {
113
var row = this.checks[rowIndex];
114
value[rowIndex] = [];
115
for (var colIndex in row) {
116
var col = row[colIndex];
117
value[rowIndex][colIndex] = !!col.checked;
118
}
119
}
120
return value;
121
}
122
123
/**
124
* Set the value of the component into the dom elements.
125
*
126
* @param value
127
* @returns {boolean}
128
*/
129
setValue(value) {
130
if (!value) {
131
return;
132
}
133
for (var rowIndex in this.checks) {
134
var row = this.checks[rowIndex];
135
if (!value[rowIndex]) {
136
break;
137
}
138
for (var colIndex in row) {
139
var col = row[colIndex];
140
if (!value[rowIndex][colIndex]) {
141
return false;
142
}
143
let checked = value[rowIndex][colIndex] ? 1 : 0;
144
col.value = checked;
145
col.checked = checked;
146
}
147
}
148
}
149
}
150
151
// Use the component.
152
Formio.use({
153
components: {
154
checkmatrix: CheckMatrix
155
}
156
});
Copied!
This component can now be rendered in both the builder and renderer as the following JSFiddle illustrates.

Header Component

An example custom component implementation would be to create a Custom Component called Header which will allow the configuration of one of the following <h1>, <h2>, <h3> etc
For this component, we will simply extend the HTML Element component which allows for dynamic HTML to be rendered within a form.
formio.js/HTML.js at master · formio/formio.js
GitHub
This example also shows how you can introduce changes to the Form Builder to introduce new configurations or make changes to other configurations.
Here is the code for a Header component
1
// Get the HTMLComponent from the components listing.
2
const HTMLComponent = Formio.Components.components.htmlelement;
3
4
/**
5
* Create a Header compoennt and extend from the HTMLComponent.
6
*/
7
class HeaderComponent extends HTMLComponent {
8
/**
9
* Define the default schema to change the type and tag and label.
10
*/
11
static schema(...extend) {
12
return HTMLComponent.schema({
13
label: 'Header',
14
type: 'header',
15
tag: 'h1'
16
}, ...extend);
17
}
18
19
static get builderInfo() {
20
return {
21
title: 'Header',
22
group: 'layout',
23
icon: 'code',
24
weight: 2,
25
documentation: '/userguide/#html-element-component',
26
schema: HeaderComponent.schema()
27
};
28
}
29
}
30
31
/**
32
* Change the edit form to make the "tag" component a select dropdown
33
* instead of a textfield so that they can only configure the "h2" fields.
34
*/
35
HeaderComponent.editForm = (...args) => {
36
const editForm = HTMLComponent.editForm(...args);
37
const tagComponent = Formio.Utils.getComponent(editForm.components, 'tag');
38
tagComponent.type = 'select';
39
tagComponent.dataSrc = 'values';
40
tagComponent.data = {
41
values: [
42
{label: 'H1', value: 'h1'},
43
{label: 'H2', value: 'h2'},
44
{label: 'H3', value: 'h3'},
45
{label: 'H4', value: 'h4'},
46
{label: 'H5', value: 'h5'}
47
]
48
};
49
return editForm;
50
};
51
52
Formio.Components.addComponent('header', HeaderComponent);
Copied!
This component can actually be tested by opening up the Form.io hosted portal @ https://portal.form.io, pasting the code within the console of the browser, and then pressing Enter. Then when you navigate to the form builder, you will see the following.
Last modified 4mo ago