Custom Component

Overview

The custom component code in this tutorial can be found at https://github.com/formio/contrib/tree/master/src/components/rating

  1. All components in Form.io are JavaScript classes. We have created a base class call Component in which all components extend from to create new component classes. Then from those newly created component classes we create more component classes and so on. In this tutorial you will learn how to

    • Create your own custom component class

    • Bundle your custom component via webpack

    • Add a custom component to your [JavaScript / Angular / React] application

    • Add a custom component to the enterprise portal application

  2. The custom component in this tutorial is a Rating component. The features that will be implemented are

    • Changing icons (using bootstrap icons)

    • Changing the size of the icons

    • Changing the color of the icons

    • Changing the number of icons

Prerequisites

  1. Install Node.js

  2. An IDE for developing code such as Visual Studio Code or Webstorm

  3. A understanding of JavaScript, HTML, CSS, and Modules Bundlers

Creating the Custom Component

This section of the tutorial will teach you how to create a custom component. We will be going over some of the basics on custom component creation by creating a Rating Component. This section is to teach you...

  • How to extend from the base component class

  • What are the fundamental methods of the base component should you implement

  • Creating a custom template for your component

  • Creating a edit form for your component

  1. Start by creating a new project folder in your IDE. We will be naming this project RatingComponent

  2. In your project directory run the commands npm init -y and npm install @formio/js

  3. Create a folder structure in your IDE that looks like the following

Custom_Components
  |- package.json
  |- package-lock.json
  |- /src
    |- /rating
      |- /editForm
        |- Rating.edit.display.js
      |- Rating.form.js
      |- Rating.js
    |- /templates
      |- form.js
  |- index.js
  1. Add the following code to Rating.js.

import {Formio} from "@formio/js";
import editForm from './Rating.form.js'

const Field = Formio.Components.components.field;

export default class Rating extends Field {
  static editForm = editForm

  static schema(...extend) {
    return Field.schema({
      type: 'rating',
      label: 'rating',
      key: 'rating',
      icon: 'bi bi-star',
      iconSize: '2rem',
      color: 'blue',
      numberOfIcons: 5,
    }, ...extend);
  }

  static get builderInfo() {
    return {
      title: 'Rating',
      icon: 'star',
      group: 'basic',
      documentation: '/userguide/#rating',
      weight: 0,
      schema: Rating.schema()
    };
  }

  constructor(component, options, data) {
    super(component, options, data);
  }

  render() {
    return super.render(this.renderTemplate('rating', {
      numberOfIcons: this.component.numberOfIcons,
      filledIcons: Number(this.dataValue?.split('/')[0])
    }))
  }

  attachIcon(icons, index) {
    const icon = icons.item(index);
    icon.addEventListener('click', () => {
      if(!this.component.disabled) {
        this.setValue(`${index + 1}/${this.component.numberOfIcons}`);
      }
    })
  }

  attachIcons() {
    const icons = this.refs.icon;
    for (let i = 0; i < icons.length; i++) {
      this.attachIcon(icons, i);
    }
  }

  attach(element) {
    this.loadRefs(element, {
      rating: 'single',
      icon: 'multiple'
    });
    this.attachIcons();
    return super.attach(element);
  }

  get defaultSchema() {
    return Rating.schema();
  }

  setValue(value){
    const changed = super.setValue(value);
    this.redraw();
    return changed;
  }
}
const Field = Formio.Components.components.field;

export default class Rating extends Field {

Every component in Formio extends from the a base component. This allows you to inherit all the methods and instance variables from the component you are extending from. Learn more about class inheritance here

static schema(...extend) {
    return Component.schema({
      type: 'rating',
      label: 'rating',
      key: 'rating',
      icon: 'bi bi-star',
      iconSize: '2rem',
      color: 'blue',
      numberOfIcons: 5,
    }, ...extend);
  }

Schema is a static method that defines the json properties of your component. It also defines the default values of your json properties as well as override some of the json properties you are extending from. The best way to understand what schema is doing is to create a form builder, drag a component onto the builder, and then click the edit JSON option on the component. You will see the components JSON schema.

static get builderInfo() {
    return {
      title: 'Rating', // Title in the builder
      icon: 'star', // Icon in the builder
      group: 'basic', // Group in the builder
      documentation: 'yourcustomlink', // Link to documentation
      weight: 0, // Position in the builder
      schema: Rating.schema() // The schema used when dragged onto the builder
    };
  }

BuilderInfo is a static method that defines how the component will display on the form builder. For example, the above code would make the Rating Component show up in the builder like this

constructor(component, options, data) {
    super(component, options, data);
  }

The constructor is useful for defining instance variables that will be used internally by your component. You can learn more about class constructors here

render() {
    return super.render(this.renderTemplate('rating', {
      numberOfIcons: this.component.numberOfIcons,
      filledIcons: Number(this.dataValue?.split('/')[0])
    }))
  }

The render method is one of the most important functions extended from the base component. It allows you to define how your component will render when using Formio.createForm. In this tutorial we will be using a custom template to define the html of our custom component.

attachIcon(icons, index) {
  const icon = icons.item(index);
  icon.addEventListener('click', () => {
    if(!this.component.disabled) {
      this.setValue(`${index + 1}/${this.component.numberOfIcons}`);
    }
  })
}

attachIcons() {
  const icons = this.refs.icon;
  for (let i = 0; i < icons.length; i++) {
    this.attachIcon(icons, i);
  }
}

attach(element) {
  this.loadRefs(element, {
    rating: 'single',
    icon: 'multiple'
  });
  this.attachIcons();
  return super.attach(element);
}

The attach method is another important function extended from the base component. The attach method is were you will add event listeners and attach functionality to your component. This method is ran after your component has been rendered on the DOM.

It is important to note the this.loadRefs method called at the top of the attach method. This load refs method will look for attributes specified in the method on the component html element and put them in the object refs on the instance of your component. For example, the above this.loadRefs will... - Look for a single ref attribute on the component element and save it in this.refs - Look for multiple ref attributes on the component element and save it in this.refs We will dive deeper into refs in the Creating the Custom Template section of this tutorial

this.setValue(`${index+1}/${this.component.numberOfIcons}`);

The setValue method is a function that takes a value and updates the component data modal. This function is useful for when you want to make updates to what data your component is holding. This data can be of any type (string, number, object, etc). For example, if this.setValue('3/5') was called then when making a submission to the form with this component the submission data would look something like...

{
    data: {
        customComponentKey: '3/5'
    }
}

Its important to note that if you want to retrieve the value that is set by setValue then you can call this.dataValue. The dataValue getter function retrieve the current value of you component. You may have seen this earlier when we called

filledIcons: Number(this.dataValue?.split('/')[0])
get defaultSchema() {
    return Rating.schema();
  }

The get defaultSchema function returns the schema of your component. It is used when merging all the json schemas upon component creation. There is not much more to say about this function other than your component will behave unexpectedly if this function is not included.

static editForm = editForm

Setting the static editForm variable allows you to define the edit form of your component. This is the edit form that is displayed when you drag and drop a component onto the form builder. For example the edit form of a textfield component looks like the following

Lets create this editForm in the Rating.form.js file

  1. Add the following code to Rating.form.js

import {Formio} from "@formio/js";

const baseEditForm = Formio.Components.baseEditForm
import RatingEditDisplay from "./editForm/Rating.edit.display.js";
export default function (...extend){
    return baseEditForm([
        {
            key: 'display',
            components: RatingEditDisplay
        },
        {
            key: 'layout',
            ignore: true
        }
    ], ... extend)
}

A editForm is just a function that returns some JSON. In order to create a custom editForm for your component we use a function called baseEditForm. The baseEditForm is a function that takes an array of json to extend the editForm of the base component class. It is in this array were you will define the component structure of each tab in your editForm. The key property is the tab title in your editForm and the components is a json representation of an editForm within that tab. You can create completely new tabs and override existing base tabs. It is best practice to create separate files for defining the components for your tabs with the following naming convention [ComponentName].edit.[TabName].js

Note the ignore: true . By setting ignore to true you can remove entire tabs of the baseEditForm. For example, the above code will result in the layout tab being removed from the edit form

  1. Add the following code to Rating.edit.display.js

export default [
    {
        type: 'number',
        key: 'numberOfIcons',
        label: 'Number of Icons',
        input: 'true',
        tooltip: "The number of icons displayed in the form"
    },
    {
        type: 'textfield',
        key: 'icon',
        label: 'Icon',
        input: 'true',
        tooltip: 'The bootstrap icon class that will go in the <i> tag'
    },
    {
        type: 'textfield',
        key: 'color',
        label: 'Color',
        input: 'true',
        tooltip: 'The color of the icons'
    },
    {
        type: 'textfield',
        key: 'iconSize',
        label: 'Icon Size',
        tooltip: 'The size of the icon'
    },
    {
        key: 'placeholder',
        ignore: true
    }
]

You may have noticed that the properties in each of these JSONs looks just like the JSON we define in the schema of our component or the json schema of the textfield shown earlier. This is because Form.io editForms are Form.io forms under the hood! When you are interacting with editForms in the form builder, you are actually using a Form.io form. This is possible because all Form.io forms can be created through a simple JSON schema! It is important to note that the key property in the JSON should match the property in the component schema. This allows for changes made to the properties of the editForm to be reflected in the properties of the component. For example the keys numberOfIcons, icon, color, and iconSize need to match the properties in static schema Note the ignore: true . By setting ignore to true you can remove edit fields of the tab you are defining. For example, the above code will result in placeholder field being removed from the display tab

Creating the Custom Template

Templates in Form.io are functions that take a context object and return a html string. They have a structure that looks like the following

const myTemplate = function(ctx) {
  return 'Your custom template html here';
};

These template functions are passed an object conventionally named ctx. This ctx is what we call a context object. It is passed data about the component that is being rendered. Earlier in this tutorial we passed numberOfIcons and filledIcons into an object as the second parameter to the renderTemplate function

render() {
    return super.render(this.renderTemplate('rating', {
      numberOfIcons: this.component.numberOfIcons,
      filledIcons: Number(this.dataValue?.split('/')[0])
    }))
  }

This adds numberOfIcons and filledIcons to the ctx object when rendering our template. For example, the above code would allow you to call ctx.numberOfIcons. Additionally the template is also passed some default context data. You can find the additional data in the evalContext function and the data variable in the renderTemplate function

In this tutorial we will be creating a custom template that will be used by our custom component.

Get started by adding the following code to form.js

export default function (ctx) {
  return `
    <div ref="rating">
      ${(function (){
        let icons = '';
        for (let i = 0; i < ctx.numberOfIcons; i++) {
          icons += `<i style="color: ${ctx.component.color}; font-size: ${ctx.component.iconSize}" class="${ctx.component.icon}${i < ctx.filledIcons ? '-fill' : ''}" ref="icon"></i>`;
        }
        return icons;
      })()}
    </div>
  `
}

A couple things to note here

  • The ctx object is passed data to it that we can use to create our custom component html. For example we call ctx.component.color. This corresponds to the static schema we defined earlier in this tutorial. We can interpolate our component schema values into our html by calling ctx.component.[schemaProperty]

  • The ref attribute. This is the most important attribute of your custom components template. It allows you to attach references to specific html elements by giving the reference a name. For example the ref="rating" ref="icon" allows you to reference 'rating' and 'icon' html elements in your custom component code. I will walk through step by step how this works...

  1. The renderer renders your custom component onto the DOM. Lets say the render function outputted the following HTML

<div ref="rating">      
  <i style="color: blue; font-size: 2em" class="bi bi-star" ref="icon"></i>
  <i style="color: blue; font-size: 2em" class="bi bi-star" ref="icon"></i>
  <i style="color: blue; font-size: 2em" class="bi bi-star" ref="icon"></i>
  <i style="color: blue; font-size: 2em" class="bi bi-star" ref="icon"></i>
  <i style="color: blue; font-size: 2em" class="bi bi-star" ref="icon"></i>
</div>
  1. In the attach function of your custom component you would call this.loadRefs passing in the element given by attach and an object specifiying the refs you would like to load. In this case we need to load rating and icon. Notice that we give rating the 'single' value and icon the 'multiple' value. This is because there is only one ref attribute that has the value 'rating' and there multiple ref attributes that have the value 'icon'. The 'single' and 'multiple' values will dictate whether the ref will be saved as an HTMLElement or HTMLCollection.

this.loadRefs(element, {
    rating: 'single',
    icon: 'multiple'
});
  1. Now that the refs are loaded we can call this.refs to access the references to the HTMLElement(s). For example, in our attachIcons function we reference the icons by calling this.refs.icon. The idea behind this is now that you have a reference to your HTMLElement(s) you can attach event listeners and properties to those references. In this example we are able to attach a click event listener to our icons because we are able to reference the HTMLElement via this.refs.icon

attachIcon(icons, index) {
    const icon = icons.item(index);
    icon.addEventListener('click', () => { // Attach event listener to icon element
      this.setValue(`${index+1}/${this.component.numberOfIcons}`);
    })
  }

  attachIcons() {
    const icons = this.refs.icon;
    for (let i = 0; i < icons.length; i++) {
      this.attachIcon(icons, i);
    }
  }

Adding the Custom Component to your Application

To add the custom component to your application you can use the Formio.use function. The Formio.use function allows you to add plugins to your formio forms. In this case we will be adding our component and template as plugins. We will be going over how to add your custom component to a...

  • JavaScript Application (via webpack)

  • Angular Application

  • React Application

JavaScript Application (webpack)

To show how to bundle the custom component into a JavaScript app we first need to create a simple application. We will be using same nodejs project folder used to house the custom component files to house our application files as well.

  1. Start by installing the following dependencies

    • npm install webpack webpack-cli webpack-dev-server html-webpack-plugin

  2. Create a webpack.config.js file in the root directory of your project

  3. Create a index.html and index.js files under the src directory. Your project structure should look like

Custom_Components
  |- package.json
  |- package-lock.json
  |- webpack.config.js
  |- /src
    |- index.html
    |- index.js
    |- /rating
      |- /editForm
        |- Rating.edit.display.js
      |- Rating.form.js
      |- Rating.js
    |- /templates
      |- form.js
  1. In webpack.config.js add the following code

 const path = require('path')
 const HtmlWebpackPlugin = require("html-webpack-plugin");
 
 module.exports = {
    mode: "development",
    entry: {
       index: "./src/index.js"
    },
    plugins: [
       new HtmlWebpackPlugin({
          title: "MyApp",
          template: "./src/index.html"
       })
    ],
    devtool: "inline-source-map",
    output: {
       filename: "[name].bundle.js",
       path: path.resolve(__dirname, 'dist'),
       clean: true
    },
    devServer: {
       static: './dist'
    },
 }
  1. In your index.html file add the following html. Notice that we have included the cdn for bootstrap icons. This is necessary in order to load our icons for the custom component

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
    <link rel='stylesheet' href='https://cdn.form.io/js/5.0.0/formio.full.min.css'>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css">
</head>
<body>
<div id="builder"></div>
<div id="formio"></div>
</body>
</html>
  1. In your index.js file add the following JavaScript. Notice how we are using Formio.use here. Formio.use allows you to add your custom component and custom template to formio so that it can be used by functions such as Formio.createForm and Formio.builder.

import {Formio} from '@formio/js'
import rating from "./rating/Rating.js";
import ratingTemplate from './templates/form';

Formio.use(
  {
    components: {
      rating // Adds our rating class to the list of formio components
    },
    templates: {
      bootstrap: {
        rating: {
          form: ratingTemplate // Adds the rating template to the list of formio templates
        }
      }
    }
  }
);

// Formio.createForm(document.getElementById('formio'), {
//   components: [
//     {
//       "label": "rating",
//       "tableView": false,
//       "key": "rating",
//       "type": "rating",
//       "input": true
//     }
//   ]
// });

// Formio.builder(document.getElementById('builder'), {}, {
// })
  1. Try it out! Uncomment Formio.createForm or Formio.builder and run webpack serve to see the custom component in action.

Angular Application

Importing the custom component and custom template into an angular application is very similar to how you would in vanilla JavaScript. To get the custom component in your angular application all you need to do is move your custom component and custom template code into your app and call the Formio.use function at the top level of your angular application. For example, if you created a angular application using the ng new command then your application code may look something like...

import {bootstrapApplication} from '@angular/platform-browser';
import {appConfig} from './app/app.config';
import {AppComponent} from './app/app.component';
import {Formio} from "@formio/js";
import rating from './customcomponent/rating/Rating'
import ratingTemplate from './customcomponent/templates/form';

Formio.use({
  components: {
      rating
    },
  templates: {
    bootstrap: {
      rating: {
        form: ratingTemplate
      }
    }
  }
})

bootstrapApplication(AppComponent, appConfig)
  .catch((err) => console.error(err));

You will either need to allow angular to process Javascript files by setting allowJs: true in tsconfig.json or convert your custom component and custom template files to typescript

You can test to see if the custom component works by using the formio directive in a template

<formio [form]="{components: [{type: 'rating'}]}"></formio>

React Application

Importing the custom component and custom template into a react application is very similar to how you would in vanilla JavaScript. To get the custom component in your react application all you need to do is move your custom component and custom template code into your app and call the Formio.use function at the top level of your react application. For example, if you created a react application using the npm create vite command then your application code may look something like...

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
import rating from './customcomponent/rating/Rating';
import ratingTemplate from './customcomponent/templates/form';
import {Formio} from '@formio/js';

Formio.use({
    components: {
        rating
    },
    templates: {
        bootstrap: {
            rating: {
                form: ratingTemplate
            }
        }
    }
})

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)

You will either need to allow angular to process Javascript files by setting allowJs: true in tsconfig.json or convert your custom component and custom template files to typescript

You can test to see if the custom component works by rendering

<Form form={{components: [{type: 'rating'}]}} />

Getting the Rating Component into the Developer Portal

To get custom components into the Developer Portal Application, you will be modifying the Custom JS & CSS settings in your project. We will be adding our custom component JavaScript code as well as bootstrap icons to the Developer Portal by adding the links to the Custom JS and Custom CSS fields in the settings. Because the Custom JS field only accepts a single JavaScript file we will need to bundle our custom component code into a single .js file.

Please follow the webpack setup in Adding the Custom Component to your Application as this setup will be used to bundle our code.

  1. Before bundling the custom component into a single file we need to make one small change in webpack.config.js. Add the following code to webpack.config.js. This externals is a webpack configuration that excludes formiojs in the bundle of custom component code. Because the Developer Portal Application already includes formiojs in the application we need to exclude formiojs from our custom component code as it is not needed

module.exports = {
    ...
    externals: {
        "@formio/js": "Formio"
    }
}
  1. Now run the command webpack. You shoud now see a /dist directory with your index.bundle.js file in it.

  2. You now need to take the code inside index.bundle.js and host it. If you already have a way to host your code then you can skip the following steps

    • Open up GitHub and create a new repository called MyCustomComponent

    • Click uploading an existing file

    • Upload the index.bundle.js file

    • Click Commit changes

    • You should now have a repository called MyCustomComponent with index.bundle.js inside

    • jsDeliver is an easy way to create CDNs from GitHub repositories. If you followed the steps above your CDN should be

      • https://cdn.jsdelivr.net/gh/user/MyCustomComponent/index.bundle.js

      • Replace user with your GitHub username

  1. Now that you have a CDN with your bundled code you can get the custom component onto the enterprise form builder

  2. Open up your developer portal and create a new project

  3. In the navigation bar click Settings

  4. Click on Custom JS & CSS

  5. Under Custom Javascript and Custom CSS add the links to your custom component code and bootstrap icons. If you followed the steps to using GitHub to host your custom component code then your Custom CSS and JavaScript should look like the following

  6. Save Settings (Click OK if you're asked if you would like to load the JavaScript)

  7. Create a new Web Form

  8. You should now see Rating component under the basic tab in the form builder

  1. You can now use the Rating component when creating Forms!

Last updated

Was this helpful?