Skip to content

Templates API

Travis Tidwell edited this page Feb 16, 2021 · 8 revisions

This functionality is new in the 4.x branch. If you are using the 3.x branch, we encourage you to upgrade to get this new functionality.

Introduction

The 4.x branch of the formio.js library (and corresponding framework libraries) have some significant enhancements and improvements. This page will outline the changes and how you can use them to further customize your form.io forms.

There are three primary ways that the 4.x branch has changed from the 3.x branch.

  1. Instantiation of components

In previous versions of formio.js, components were only instantiated when the were visible in the DOM. In 4.x and later all components will be instantiated regardless of whether they are visible or not. This is important for setting initial values, calculating while hidden and running validations on hidden components. It also greatly cleans up more complex components like tables, datagrids and tabs.

  1. Component lifecycles

In previous versions of formio.js, components had only one lifecycle function, the build method. This was triggered whenever a component was added to the DOM and was expected to initialize the component, add DOM elements to a root element (mostly using this.ce()) and attach all events to the dom elements. This was a very static process where functions would build out all the DOM elements of the component element by element and manually add them to the DOM. In addition, the logic of changing the DOM or attaching actions to DOM elements was completely intermingled with the rendering process. The end result was that components had a static layout. Theoretically it would have been possible to override the rendered DOM by overriding the build method but in reality it would have essentially involved creating an entirely new component.

Now, in the 4.x branch, the lifecycle is broken into three phases. These are init, render and attach. The init is run when the component is instantiated. It will not run again unless a rebuild is called. The render method is then called which is expected to return a string of html that represents the whole of the component. This string is then added to the DOM and a reference to the component's root DOM element is returned. Finally, the attach method is called and passed the reference to the root DOM element. The attach method is expected to find relevant parts of the DOM and attach events and logic to them. When the rendering of the DOM needs to change, like a component being hidden, a hidden flag is set on the component and it is redrawn. The component itself does not need to do the hiding. This is more consistent with how newer frameworks (like React and Vue) are rendering where they render based on the current state.

  1. CSS Framework Templates and customizing templates.

Since we now have a proper rendering layer, we now support additional CSS Frameworks to bootstrap. In particular we have bootstrap 3, bootstrap 4, Semantic UI and are planning on adding support for Materialize (a generic Material UI framework). You can now select which CSS Framework template you want to render your forms in and apply the different CSS accordingly. In addition, you can even customize the html output by the form to modify how components render.

Setting a CSS Framework

When setting up an application, you can select which CSS Framework you want to render the output to. Form.io currently supports the following frameworks:

  • Bootstrap 3 (bootstrap3)
  • Bootstrap 4 (bootstrap)
  • Semantic UI (semantic)
  • Other - Contact us for help implementing another framework!

The default template is currently Bootstrap 4 so if you want to use that, you don't have to do anything else. In order to switch to a different framework, you will need to set it globally so that the renderer will use the framework's templates.

import { Templates } from 'formiojs';

Templates.framework = 'semantic';

If you are using a framework wrapper, you should import Templates from that wrapper instead of formio.js so that it will apply to the Formio instance within the wrapper as well. For example, if you are using react-formio you should do:

import { Templates } from 'react-formio';

Templates.framework = 'semantic';

Plugin registration

Templates can also be registered through the Plugin system, where an external template is provided to the renderer. For an example of how this is done, please see https://github.com/formio/semantic

Overriding templates

In addition to setting the global CSS Framework, you can also override specific templates within that framework. All templates within the renderer are pre-compiled templates. This means that instead of using HTML for each template, you will provide a template function, which simply takes a context variable and returns a string that is the HTML you would like to render. An example of a template function looks like the following.

const myInput = function(ctx) {
  return '<div class="' + ctx.component.type + '">' + ctx.value + '</div>';
};

You can override a bunch of template functions by setting the current template with the templates you wish to override. This allows you to change the html output of the component even as the component continues to function the same. In order to do this, simply set the template on the Templates object and the renderer will start using it.

You can do this one of two ways. First, by setting multiple templates:

Templates.current = {
  input: {
    form: function(ctx) {
      return '<div>My custom template</div>';
    }
  },
  html: {
    form: function(ctx) {
      return '<div>My other custom template</div>';
    }
  }
};

Or individually:

Templates.setTemplate('input', {
  form: function(ctx) {
    return '<div>My custom template</div>';
  }
});

You can extend an existing template by first getting the current template function and then call that function in addition to your own. This allows you to provide wrappers around certain elements within the renderer. Here is an example of how to do this.

var inputTemplate = Templates.current.input.form;
Templates.current = {
  input: {
    form: function(ctx) {
      return '<div class="input-wrapper">' + inputTemplate(ctx) + '</div>';
    }
  }
};

Specific templates

Templates can be shared by multiple components. For example, the input template is used by many component to render the html input element. Sometimes you may want to override it for only certain components but not all components. You can do this by setting the template name to include the specific information. This can either be the component type or the component property name (api key). You can even do both. The renderer will search in this order for the first match to determine which component to select:

`${templateName}-${component.type}-${component.key}`,
`${templateName}-${component.type}`,
`${templateName}-${component.key}`,
`${templateName}`,

If you set a template for 'input-number' it will only override the input template for number components. If you set a template for 'input-number-total' it would only override the input template for a number component with the property name of 'total';

Render modes

In addition to templates, there are a few render modes that allow different representations of the same component. The default mode is form which will render all the components as form components. Another common mode is ```html`` which will render an html only representation of the component without any form elements like inputs. This is useful for displaying data.

The built in render modes are:

  • form - The default render to render as a form.
  • html - Render the data as generic html, instead of a form.
  • flat - Similar to form but will flatten out components like tabs and wizards that tend to hide data.
  • builder - A builder view of a component (not frequently used outside of the form builder).

To set the render mode, set it on the options when instantiating a form.

import { Form } from 'formiojs';

const form = new Form(document.getElementById('formio'), 'https://examples.form.io/example', {
  renderMode: 'html'
});

You can even create your own render modes and use them with your forms.

import { Form, Templates } from 'formiojs';

Templates.current = {
  input: {
    foo: '<div>bar</div>'
  }
};

const form = new Form(document.getElementById('formio'), 'https://examples.form.io/example', {
  renderMode: 'foo'
});

Custom Component Templates

You can create custom components using the same lifecycle methods.

An example can be found at https://github.com/formio/react-app-starterkit/blob/master/src/components/CheckMatrix.js

Refs

With the form.io templating functionality, the underlying DOM structure can be very different, even entirely custom. Because of this, adding events to the DOM necessitates being able to find the right part of the DOM to add the events. In order to do this, the formio.js library uses refs to refer to parts of the DOM. These can then be selected regardless of where they are in the DOM.

When rendering a template, you can use the ref attribute to set a reference string. Then in the attach phase, you can get any DOM elements that have that ref, regardless of where they are. In order to facilitate this, formio.js has a "loadRefs" function that can find all the refs and adds them to this.refs.

import { Templates, Components, Component } from 'formiojs';

Templates.addTemplate('mytemplate', {
    form: `
<div>
  <div ref="myref">
    {{ ctx.foo }}
    <div ref="mychild">1</div>
    <div ref="mychild">2</div>
    <div ref="mychild">3</div>
  </div>
</div>
`
});

class MyComponent extends Component {
  init() {
    // Init tasks here.
  }

  render() {
    // By calling super.render, it wraps in component wrappers.
    return super.render(this.renderTemplate('mytemplate', {
        foo: 'bar',
        data: 'these are available in the template'
    }));
  }

  attach(element) {
    this.loadRefs(element, {
      myref: 'single',
      mychild: 'multiple',
    });

    this.refs.myref; // This will be either null or the div with "myref" set on it.
    this.refs.mychild; // This will be either null or an array of divs with "mychild" set on them.
  }

  detach() {
    // Called on redraw or rebuild. The opposite of attach.
  }

  destroy() {
    // Called on rebuild. The opposite of init.
  }
}

Components.addComponent('mycomponent', MyComponent);

Layout component refs

Layout components can contain other components within them including other components of the same type. For this reason be careful with refs in layout components and be sure to append the component id to each key. This is so that loadRefs only selects the refs for that component and not any nested components.

Server Side Rendering

One of the goals of the 4.x branch was to make the formio.js renderer compatible with server side rendering. While this should be compatible with it due to the changes that were made, we haven't had a chance to fully test it out yet. We would greatly welcome some help in testing this out.

Here is how it is designed to work:

On the server side:

import { Form } from 'formiojs';

// Instead of a URL, you can also use a form definition JSON instead.
const form = new Form('https://examples.form.io/example');
form.ready.then(function(instance) {
  const html = instance.render();
  // At this point you can put the html in the page.
});

On the client side:

import { Form } from 'formiojs';

const form = new Form('https://examples.form.io/example');
// Find the DOM element wherever it is.
const element = document.getElementById('formio');
form.ready.then(function(instance) {
  instance.attach(element);
});

We welcome your feedback on how to improve this process. Please contact support with any help or suggestions.

Clone this wiki locally