Custom Component

Overview

  1. All components are javascript classes that extend base classes or classes of other components from the formio.js library. This extending of base classes also needs to be done if you want to create a custom component. This example tutorial will show you how to

    • Create a custom component from scratch

    • Use the formio.js library with webpack

    • Test the component in the form builder

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

    • Copy and paste SVGs to use them as icons

    • Change the width and height of the SVG

    • Change the unfilled and filled color

    • Change the number of icons/svgs

Prerequisites

  1. Install Node.js

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

Note

In this tutorial there will be + and - symbols next to some of the code / files. The + tells you to that something is being added. The - tells you that something is being removed

Initializing Project

  1. Create a new directory to hold your project files (The directory in this tutorial is called Custom_Components)

  2. Create a file called template.html

  3. In the template.html file add the following code

  4. <!DOCTYPE html>
     <html lang="en">
     <head>
         <meta charset="UTF-8">
         <title>Title</title>
         <link rel='stylesheet' href='https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css'>
         <link rel='stylesheet' href='https://cdn.form.io/formiojs/formio.full.min.css'>
         <script src='https://cdn.form.io/formiojs/formio.full.min.js'></script>
     </head>
     <body>
         <div id="formio"></div>
     </body>
     </html>
  5. Create a new file called index.js

  6. Create a new file called Rating.js

  7. Your project structure should be as follows:

Custom_Components
|- Rating.js
|- index.js
|- template.html

Initial Custom Component Code

  1. The first step of any component is to extend from a base class, start by

    • Creating a new class in Rating.js called Rating that extends Field

      const Field = Formio.Components.components.field;
      class Rating extends Field {
      
      }
  2. Great! You now have a custom component, you can test that this is working by doing the following

    • In index.js add the code below

    • Formio.use([
          {
             components: {
                rating: Rating
             }
          }
       ])
      
      Formio.createForm(document.getElementById('formio'), {
         components: [
            {
                type: "rating"
            },
         ]
      }, {})
    • Add your Rating.js and index.js scripts to your template.html file

    •  <!DOCTYPE html>
       <html lang="en">
       <head>
           <meta charset="UTF-8">
           <title>Title</title>
           <link rel='stylesheet' href='https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css'>
           <link rel='stylesheet' href='https://cdn.form.io/formiojs/formio.full.min.css'>
           <script src='https://cdn.form.io/formiojs/formio.full.min.js'></script>
           + <script src='Rating.js'></script>
       </head>
       <body>
           <div id="formio"></div>
           + <script src='index.js'></script>
       </body>
       </html>
    • Open your template.html file in your browser and view the DOM. You should see the following (notice the formio-component-rating):

  3. The Formio library is now picking up our custom component and adding it to the DOM! In order to render content into the DOM add the following to Rating.js and refresh your browser

    • const Field = Formio.Components.components.field
      class Rating extends Field {
         + render(content) {
         +   return super.render("<div>I am a rating component</div>");
         + }
      }
    • You should now see "I am a rating component" text in the browser

    • Notice how in the render function you are calling super.render(). This is because you are overriding the render function defined in Field component.

  4. There are many functions that can be overridden from the base component that you extend from (Learn more here). Add the following function overrides to your Rating.js file

  5. const Field = Formio.Components.components.field;
    
    class Rating extends Field {
    
     + static schema(...extend) {
     +     return Field.schema({
     +         type: 'rating',
     +         label: 'rating',
     +         key: 'rating',
     +     });
     + }
    
     + static get builderInfo() {
     +     return {
     +         title: 'Rating',
     +         icon: 'star',
     +         group: 'basic',
     +         documentation: '/userguide/#textfield',
     +         weight: 0,
     +         schema: Rating.schema()
     +     };
     + }
    
     + constructor(component, options, data) {
     +     super(component, options, data);
     + }
    
     + init() {
     +     super.init();
     + }
    
     + get inputInfo() {
     +     const info = super.inputInfo;
     +     return info;
     + }
    
     render(content) {
         return super.render("<div>I am a rating component</div>");
     }
    
     + attach(element) {
     +     this.loadRefs(element, {
     +         customRef: 'single'
     +     });
     +     return super.attach(element);
     + }
    
     + detach() {
     +     return super.detach();
     + }
    
     + destroy() {
     +     return super.destroy();
     + }
    
     + normalizeValue(value, flags = {}) {
     +     return super.normalizeValue(value, flags);
     + }
    
     + getValue() {
     +     return super.getValue();
     + }
    
     + getValueAt(index) {
     +     return super.getValueAt(index);
     + }
    
     + setValue(value, flags = {}) {
     +     return super.setValue(value, flags);
     + }
    
     + setValueAt(index, value, flags = {}) {
     +     return super.setValueAt(index, value, flags);
     + }
    
     + updateValue(value, flags = {}) {
     +     return super.updateValue(...arguments);
     + }
    }
  6. At this point you have

    • A basic template of a component (Rating.js)

    • The component being rendered by the Formio library (index.js)

    • The html page to view our custom component (template.html)

  7. However, there are a couple of problems if you continue developing the custom component this way

    • You are using the Formio object in Rating.js and index.js, but it is not apparent that this object is available to us

    • You are not able to using npm packages

    • You are not able to use import / export statements

    • There is no syntax highlighting or intellisense

  8. In order to solve these problems and make development of our custom component easier you need Webpack

Webpack

Webpack will compile our javascript modules and allow them to be used in the browser. You will also being using webpack to create a development server to develop the custom rating component

  1. Start by

    • Opening your terminal

    • In your project directory type npm init -y

    • Then type npm install webpack webpack-cli webpack-dev-server html-webpack-plugin --save-dev

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

  3. Your project structure should be as follows:

  4. Custom_Components
    + |- package.json
    + |- package-lock.json
    + |- webpack.config.cjs
      |- Rating.js
      |- index.js
      |- template.html
  5. In webpack.config.cjs add the following code

  6.  const path = require('path')
     const HtmlWebpackPlugin = require("html-webpack-plugin");
     
     module.exports = {
        mode: "development",
        entry: {
           index: "./src/index.js"
        },
        plugins: [
           new HtmlWebpackPlugin({
              title: "output management",
              template: "./template.html"
           })
        ],
        devtool: "inline-source-map",
        output: {
           filename: "[name].bundle.js",
           path: path.resolve(__dirname, 'dist'),
           clean: true
        },
        devServer: {
           static: './dist'
        },
     }
  7. This config file tells webpack how to bundle our modules and run the dev server

  8. Now you can use module syntax in our JS files! Let's go back and make some changes to our code

    • In Rating.js make the following changes

    •  + import {Formio} from 'formiojs'
         const Field = Formio.Components.components.field;
       - class Rating extends Field
       + export default class Rating extends Field {
            static schema(...extend) {
               return Field.schema({
                  type: 'rating',
                  label: 'rating',
                  key: 'rating'
               });
            }
       ...
    • In index.js make the following changes

    • + import {Formio} from 'formiojs'
      + import Rating from './rating/Rating.js'
        Formio.use([
           {
              components: {
                 rating: Rating
              }
           }
        ])
        ...
    • In template.html make the following changes

    • <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <title>Title</title>
          <link rel='stylesheet' href='https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css'>
          <link rel='stylesheet' href='https://cdn.form.io/formiojs/formio.full.min.css'>
          - <script src='https://cdn.form.io/formiojs/formio.full.min.js'></script>
          - <script src='Rating.js'></script>
      </head>
      <body>
          <div id="formio"></div>
          - <script src='index.js'></script>
      </body>
      </html>
  9. In order for webpack to bundle the modules together it looks for a folder called src. You need to put the source code files in a directory called src

    • Create a directory called src

    • Put index.js in src /src/index.js

    • Create a directory called rating in src /src/rating/

    • Put Rating.js in rating directory /src/rating/Rating.js

    • Your project structure should now look like

    •     Custom_Components
          |- package.json
          |- package-lock.json
          |- webpack.config.cjs
       -  |- Rating.js
       -  |- index.js
       +  |- /src
       +     |- /rating
       +        |- Rating.js
       +     |- index.js 
          |- template.html
  10. In order to use the import {Formio} from formiojs you need to install the formiojs library in our node_modules. Type npm install formiojs in your terminal

  11. Now that formiojs is installed, type webpack serve --open to start a development server

  12. You should now have a blank html page with the text I am a rating component. You now have your development environment setup

Creating the Rating Component

schema

  1. The schema function is where you override properties from the component your class extends from and also create new properties

  2. The Rating component will have some new properties that will be custom to the component itself. The properties will be

    • unfilledColor

    • filledColor

    • numOfIcons

    • iconHeight

    • iconWidth

    • icon

  3. Add these properties to your schema in Rating.js

    •  ...
       static schema(...extend) {
          return Field.schema({
               type: 'rating',
               label: 'rating',
               key: 'rating',
              + unfilledColor: "#ddd",
              + filledColor: "yellow",
              + numOfIcons: "5",
              + iconHeight: "25px",
              + iconWidth: "25px",
              + icon: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
              +         \t viewBox="0 0 47.94 47.94" xml:space="preserve">
              +         <path d="M26.285,2.486l5.407,10.956c0.376,0.762,1.103,1.29,1.944,1.412l12.091,1.757
              +         \tc2.118,0.308,2.963,2.91,1.431,4.403l-8.749,8.528c-0.608,0.593-0.886,1.448-0.742,2.285l2.065,12.042
              +         \tc0.362,2.109-1.852,3.717-3.746,2.722l-10.814-5.685c-0.752-0.395-1.651-0.395-2.403,0l-10.814,5.685
              +         \tc-1.894,0.996-4.108-0.613-3.746-2.722l2.065-12.042c0.144-0.837-0.134-1.692-0.742-2.285l-8.749-8.528
              +         \tc-1.532-1.494-0.687-4.096,1.431-4.403l12.091-1.757c0.841-0.122,1.568-0.65,1.944-1.412l5.407-10.956
              +         \tC22.602,0.567,25.338,0.567,26.285,2.486z"/>
              +     </svg>`
           });
       }
       ...

builderInfo

  1. The builderInfo is how the component will show up within the form builder. The builder has 6 main properties

    • title - The text displayed on the component in the form builder

    • icon - The icon next to the title of the component. Formio has Font Awesome integrated in the library so any fa-* icons will work

    • group - The group the component will show up in the form builder

    • documentation - Gives the help button on the form builder a link to the documentation for this component

    • weight - find out what this does

    • schema - The JSON schema used when the component is dragged onto the form builder

  2. You do not need to add any new code

setIconProperties

  1. setIconProperties is a custom function that when called adds iconHeight, iconWidth, and unfilledColor properties to the icon

  2. Add the following code to Rating.js

    •     ...
          static get builderInfo() {
              return {
              title: 'Rating',
              icon: 'star',
              group: 'basic',
              documentation: '/userguide/#textfield',
              weight: 0,
              schema: Rating.schema()
              };
          }
      
          + setIconProperties() {
          +   const domIcon = new DOMParser().parseFromString(this.component.icon, 'text/xml')
          +   domIcon.firstChild.style.fill = this.component.unfilledColor
          +   domIcon.firstChild.setAttribute("height", this.component.iconHeight)
          +   domIcon.firstChild.setAttribute("width", this.component.iconWidth)
          +   this.component.icon = new XMLSerializer().serializeToString(domIcon.documentElement);
          + }

init

  1. Init function will be called immediately after the component has been initialized. This is useful for calling functions or set variables at the time the component is initialized in an application

  2. You will be calling the setIconProperties function inside init function. Add the following code to Rating.js

    • ...
      constructor(component, options, data) {
         super(component, options, data);
      }
      
        init() {
      +    this.setIconProperties()
           super.init();
        }
      ...

render

  1. This function returns the html that will render in the DOM

  2. Add the following code to the render function

    •   render(content) {
           - return super.render("<div>I am a rating component</div>");
           + let component = `<div ref="rating">`
           + for (let i = 0; i < this.component.numOfIcons; i++) {
           +    component += this.component.icon;
           + }
           + component += `</div>`
           + return super.render(component);
        }
  3. Notice this.component.icon and this.component.numOfIcons. You have access to the properties defined in schema by referencing this.component

  4. Notice ref="rating". This will be important when you need to attach event listeners to the custom rating component

attach

  1. The attach function is where you will attach logic to your component such as event listeners.

  2. It is important to have a ref attribute within your component so that attach can reference your ref attribute within the function

  3. Add the following code to the attach function

    •   attach(element) {
           this.loadRefs(element, {
        -     customRef: 'single'
        +     rating: 'single'
           });
        +   function clearRating(ratings, color) {
        +      for (const rating of ratings) {
        +         rating.style.fill = color
        +      }
        +   }
      
        +   if (!this.component.disabled) {
        +      let icons = this.refs.rating.children
        +      for (let i = 0; i < icons.length; i++) {
        +         let svg = icons[i];
        +         svg.addEventListener("click", () => {
        +             clearRating(icons, this.component.unfilledColor);
        +             svg.style.fill = this.component.filledColor
        +             let previousElement = svg.previousElementSibling
        +             while (previousElement) {
        +                 previousElement.style.fill = this.component.filledColor
        +                 previousElement = previousElement.previousElementSibling;
        +             }
        +             this.updateValue(`${i+1}/${icons.length}`);
        +         })
        +     }
        +  }
          return super.attach(element);
       }
  4. this.loadRefs looks for the DOM element that has the attribute ref="property" (in this the property is rating) and adds a reference for you to call on in the code this.refs.rating.

  5. Because this.refs.rating points to a div element (see render) and all of our icons are inside this div element, you can use this.refs.rating.children to loop over all the icons and attach event listeners to each of them

  6. Notice this.updateValue(`${i+1}/${icons.length}`);. Use the updateValue function to give the component data whenever it is submitted

updateValue

  1. Use this function whenever you need to set the submission data of the component before it is submitted

Testing Custom Component

  1. At this point you have added a lot of new code to your Rating.js file. Let check and make sure everything is working up to this point

  2. In index.js change the following code

    •   Formio.use([
           {
              components: {
                 rating: Rating
              }
           }
        ])
      
        Formio.createForm(document.getElementById('formio'), {
           components: [
              - {
              -    type: "rating"
              - }
              + Rating.schema()
           ]
        }, {
        +       sanitizeConfig: {
        +          addTags: ["svg", "path"],
        +          addAttr: ["d", "viewBox"]
        +       }
           })
    • Notice the sanitizeConfig. In order to use SVGs with Formio you need to add this option to the createForm function to allow Formio to render SVGs

    • Click here to learn more about form options

  3. Save your changes in your IDE. Your webpage should automatically update to reflect the changes made due to webpacks hot module reloading

  4. You should now see your rating component on the html page!

Getting the Rating Component in the Form Builder

  1. Now that you are able to display the Rating component using Formio.createForm, its time to test the Rating component in the form builder

    • To add the form builder into your dev environment make the following changes to the code

    • In template.html

    •    <!DOCTYPE html>
         <html lang="en">
         <head>
             <meta charset="UTF-8">
             <title>Title</title>
             <link rel='stylesheet' href='https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css'>
             <link rel='stylesheet' href='https://cdn.form.io/formiojs/formio.full.min.css'>
         </head>
         <body>
      +  <div id="builder"></div>
         <div id="formio"></div>
         </body>
         </html>
    • In index.js

      •   import {Formio} from 'formiojs'
          import Rating from './rating/Rating.js'
        
          Formio.use([
            {
                components: {
                    rating: Rating
                }
            }
          ])
         
          + Formio.builder(document.getElementById("builder"), {}, {
          +   sanitizeConfig: {
          +        addTags: ["svg", "path"],
          +        addAttr: ["d", "viewBox"]
          +    }
          + }).then(() => {
          +
          + });
         
          Formio.createForm(document.getElementById('formio'), {
             components: [
                 Rating.schema()
             ]
          }, {
             sanitizeConfig: {
                 addTags: ["svg", "path"],
                 addAttr: ["d", "viewBox"]
             }
          })
  2. Save changes, and you should now see the formio builder in your dev environment along with the Rating component under Basic tab

  3. Drag and drop the Rating component onto the form builder

    • You should now see the Rating component edit options... However, you may have noticed that you cannot see any options to change the icon, number of icons, the icon width and height and other custom options

  4. In order to customize the Ratings edit menu, you need to set the editForm inside Rating.js

    • Add the following code to Rating.js

    •    import {Formio} from "formiojs";
      
         const Field = Formio.Components.components.field;
         + import ratingEditForm from './Rating.form.js'
           
         export default class Rating extends Field {
         + static editForm = ratingEditForm
         static schema(...extend) {
      ...
  5. Now that you have set the edit form within Rating.js you need to create the Rating.form.js file

    • Inside the /rating directory add a Rating.form.js file

    • Add the following code to Rating.form.js

    •    + import {Formio} from "formiojs";
         + import RatingEditDisplay from "./editForm/Rating.edit.display.js";
         + export default function (...extend){
         +     return Formio.Components.baseEditForm([
         +         {
         +             key: 'data',
         +             ignore: true,
         +         },
         +         {
         +             key: 'display',
         +             components: RatingEditDisplay
         +         },
         +         {
         +             key: 'validation',
         +             ignore: true
         +         }
         +     ], ... extend)
         + }
    • For this component you won't be needing a data tab or a validation tab, so you are able to remove these tabs by defining a json schema with the key being the tab you want to remove and setting ignore to true. You are also able to define custom edit properties by creating a list of components. You will create this list of components in a folder called editForm

    • In the /rating directory create a new directory called editForm

      • In the /editForm directory create a new file called Rating.edit.display.js

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

      •    export default [
                {
                    type: 'number',
                    key: 'numOfIcons',
                    label: 'Number of Icons',
                    input: 'true'
                },
                {
                    type: 'textfield',
                    key: 'filledColor',
                    label: 'Filled Color',
                    input: 'true',
                },
                {
                    type: 'textfield',
                    key: 'unfilledColor',
                    label: 'UnFilled Color',
                    input: 'true'
                },
                {
                    type: 'textfield',
                    key: 'iconHeight',
                    label: 'Icon Height',
                    input: 'true'
                },
                {
                    type: 'textfield',
                    key: 'iconWidth',
                    label: 'Icon Width',
                    input: 'true'
                },
                {
                    type: 'textarea',
                    key: 'icon',
                    label: 'Icon',
                    input: true
                },
                {
                    key: 'placeholder',
                    ignore: true
                }
            ]
      • It is important when creating a custom component that the keys in your editForm javascript files match the properties in the custom component schema function. This allows the properties in the schema to be set dynamically from the input in the edit form

  6. Your project structure should look like the following

        Custom_Components
            |- package.json
            |- package-lock.json
            |- webpack.config.cjs
            |- /src
               |- /rating
       +          |- /editForm
       +              Rating.edit.display.js
       +          |- Rating.form.js
                  |- Rating.js
               |- index.js 
            |- template.html
    
  7. Save your changes and drag and drop the Rating component onto the builder. You should now see your custom properties!

  8. If you are building your own custom component then this is a good point to stop and develop your component. In the next steps you will be adding the rating component into the enterprise form builder. This will break the development setup that you currently have

Getting the Rating Component in the Enterprise Form Builder

  1. In order to bundle the rating component into a file that only contains the code necessary for the enterprise form builder you need to make some changes to you code.

  2. To start create a new webpack config file called webpack.prod.cjs in the root project directory

  3. Add the following code to webpack.prod.cjs

  4. const path = require('path')
    
    module.exports = {
        mode: "production",
        entry: {
          index: "./src/index.js",
        },
        output: {
            filename: "[name].bundle.js",
            path: path.resolve(__dirname, 'dist'),
            clean: true
        }
    }
  5. This webpack config will be used to create a production bundle of our rating component

  6. Because the enterprise form builder already has the Formio object as a global variable you need to remove/comment out all imports of Formio so that it is not included in the bundle

    • In index.js comment out import {Formio} from 'formiojs' and comment out the builder and create form function calls. Commenting out the code allows you to uncomment it whenever you want to go back to developing the component

    • // import {Formio} from 'formiojs'
         import Rating from './rating/Rating.js'
         Formio.use([
             {
                 components: {
                   rating: Rating
                 }
             }
         ])
       // Formio.builder(document.getElementById("builder"), {}, {
       //     sanitizeConfig: {
       //         addTags: ["svg", "path"],
       //         addAttr: ["d", "viewBox"]
       //     }
       // }).then(() => {
       //
       // });
       
       
       // Formio.createForm(document.getElementById('formio'), {
       //     components: [
       //         {
       //             type: "rating",
       //             label: "rating"
       //         },
       //     ]
       // }, {
       //     sanitizeConfig: {
       //         addTags: ["svg", "path"],
       //         addAttr: ["d", "viewBox"]
       //     }
       // })
    • In Rating.js and Rating.form.js comment out import {Formio} from 'formiojs'

  7. Now that the code only has the necessary lines to for the rating component to function you can create a production bundle using webpack.prod.cjs

  8. Save your files and open your terminal and type webpack --config ./webpack.prod.cjs

  9. You should now see a /dist directory with an index.bundle.js file inside

  10. You now need to take the code inside index.bundle.js and host it somewhere that allows a CDN to deliver the code

  11. If you know how to do host and create a link using a CDN 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

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

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

  14. In the navigation bar click Settings

  15. Click on Custom JS & CSS

  16. Under Form Module add the following code

    •  {
           options: {
               form: {
                   sanitizeConfig: {
                       addTags: ["svg", "path"],
                       addAttr: ["d", "viewBox"]
                   }
               },
               builder: {
                   sanitizeConfig: {
                       addTags: ["svg", "path"],
                       addAttr: ["d", "viewBox"]
                   }
               }
           }
       }
    • Just like when you were in developing the Rating component, you also need to add sanitizeConfig to the enterprise form builder as well

  17. Under Custom Javascript add your CDN to the textbox

    • Remember to replace user with your GitHub username

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

  19. Create a new Web Form

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

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

  22. Note: Sometimes the sanitize configs won't take into effect unless you refresh the page. If you are not able to see the component when it is dragged onto the form builder try refreshing your browser

Last updated