javascriptangulartypescriptformbuilderformio

Form.io Custom Layout Component


I am using Form.io v3.27.1, and I am trying to create a custom layout component - specifically an accordion - and I'm using the concepts provided in the CheckMatrix component example for the most part.

I am able to make the accordion component show in the toolbox, and I'm able to drag it onto the form, configure it with a custom edit form etc. I can save it and it renders a Bootstrap themed accordion perfectly.

What it does NOT do however, is allow me to drag and drop other components into the content area similar to the behavior of other layout components (i.e. Tabs, Columns, Fieldset etc.).

I assume by skimming through the source code of the other layout controls that I need to extend NestedComponent in lieu of BaseComponent, but I've not yet been able to make that work.

I feel like I am overlooking something small. I just can't seem to figure out how to render a layout component that accepts other Form.io components as children.

Anyone have a working example or suggestions I can try to get this working? I appreciate your help in advance!

enter image description here enter image description here

import BaseComponent from 'formiojs/components/base/Base';
import NestedComponent from 'formiojs/components/nested/NestedComponent';
import Components from 'formiojs/components/Components';
import * as editForm from './Accordian.form';

export default class AccordionComponent extends BaseComponent {

  /**
   * Define what the default JSON schema for this component is. We will derive from the BaseComponent
   * schema and provide our overrides to that.
   * @return {*}
   */
  static schema() {
    return BaseComponent.schema({
      type: 'accordion',
      label: 'Sections',
      input: false,
      key: 'accordion',
      persistent: false,
      components: [{
        label: 'Section 1',
        key: 'section1',
        components: []
      }]
    });
  }

  /**
   * Register this component to the Form Builder by providing the "builderInfo" object.
   */
  static get builderInfo() {
    return {
      title: 'Accordion',
      group: 'custom',
      icon: 'fa fa-tasks',
      weight: 70,
      schema: AccordionComponent.schema()
    };
  }

  /**
   * Tell the renderer how to build this component using DOM manipulation.
   */
  build() {

    this.element = this.ce('div', {
      class: `form-group formio-component formio-component-accordion ${this.className}`
    }, [
      this.ce('app-formio-accordian', {
          components: JSON.stringify(this.component.components)
        })
    ]);
  }

  elementInfo() {
    return super.elementInfo();
  }

  getValue() {
    return super.getValue();
  }

  setValue(value) {
    super.setValue(value);
  }
}

// Use the table component edit form.
AccordionComponent.editForm = editForm.default;

// Register the component to the Formio.Components registry.
Components.addComponent('accordion', AccordionComponent);
<div class="accordion" id="formioAccordionPreview" *ngIf="components">
    <div class="card" *ngFor="let component of components; first as isFirst">
        <div class="card-header" id="heading-{{component.key}}">
            <h2 class="mb-0">
                <button type="button" class="btn btn-link" data-toggle="collapse" data-target="#collapse-{{component.key}}">{{component.label}}</button>
            </h2>
        </div>
        <div id="collapse-{{component.key}}" class="collapse" [class.show]="isFirst" aria-labelledby="heading-{{component.key}}" data-parent="#formioAccordionPreview">
            <div class="card-body">
                <p>I should be able to &apos;Drag and Drop a form component&apos; here.</p>
            </div>
        </div>
    </div>
</div>


Solution

  • An Accordion is functionally identical to a tabs control, that is, headered content that facilitates switching and selection. The answer to constructing an accordion control was to extend the TabsComponent that's built into Form.io and override the createElement method that constructs the element via DOM manipulation. A couple of other overrides (schema and builderInfo) to provide metadata back to the FormBuilder and voila!

    accordion.js

    import TabsComponent from 'formiojs/components/tabs/Tabs';
    import * as editForm from './Accordian.form';
    
    export default class AccordionComponent extends TabsComponent {
    
      /**
       * Define what the default JSON schema for this component is. We will derive from the BaseComponent
       * schema and provide our overrides to that.
       * @return {*}
       */
      static schema() {
        return TabsComponent.schema({
          type: 'accordion',
          label: 'Sections',
          input: false,
          key: 'accordion',
          persistent: false,
          components: [{
            label: 'Section 1',
            key: 'section1',
            type: 'tab',
            components: []
          }]
        });
      }
    
      /**
       * Register this component to the Form Builder by providing the "builderInfo" object.
       */
      static get builderInfo() {
        return {
          title: 'Accordion',
          group: 'custom',
          icon: 'fa fa-tasks',
          weight: 70,
          schema: AccordionComponent.schema()
        };
      }
    
      /**
       * Tell the builder how to build this component using DOM manipulation.
       */
      createElement() {
        this.tabs = [];
        this.tabLinks = [];
        this.bodies = [];
    
        this.accordion = this.ce('div', {
          id: `accordion-${this.id}`
        });
    
        var _this = this;
    
        this.component.components.forEach(function (tab, index) {
    
          var isFirst = index === 0;
    
          var tabLink = _this.ce('a', {
            class: 'card-link',
            data_toggle: 'collapse',
            href: `#collapse-${tab.key}`
          }, tab.label);
    
          _this.addEventListener(tabLink, 'click', function (event) {
            event.preventDefault();
            _this.setTab(index);
          });
    
          var header = _this.ce('div', {
            class: 'card-header'
          }, [tabLink]);
    
          var tabPanel = _this.ce('div', {
            class: 'tab-pane',
            role: 'tabpanel',
            tabLink: tabLink
          });
    
          var tabContent = _this.ce('div', {
            class: 'tab-content'
          }, [tabPanel]);
    
          var body = _this.ce('div', {
            class: 'card-body',
            id: tab.key
          }, [tabContent]);
    
          var content = _this.ce('div', {
            id: `collapse-${tab.key}`,
            class: 'collapse'.concat(isFirst ? ' show' : ''),
            data_parent: `#accordion-${_this.id}`
          }, [body]);
    
          var card = _this.ce('div', {
            class: 'card'
          }, [header, body]);
    
          _this.tabLinks.push(header); 
          _this.tabs.push(tabPanel);
          _this.bodies.push(body);
          _this.accordion.appendChild(card);
        });
    
        if (this.element) {
          this.appendChild(this.element, [this.accordion]);
          this.element.className = this.className;
          return this.element;
        }
    
        this.element = this.ce('div', {
          id: this.id,
          class: this.className
        }, [this.accordion]);
        this.element.component = this;
        return this.element;
      }
      
      setTab(index, state) {
        super.setTab(index, state);
        var _this = this;
    
        if (this.bodies) {
          this.bodies.forEach(function (body) {
            body.style.display = 'none';
          });
          _this.bodies[index].style.display = 'block';
        }
      }
    }
    
    AccordionComponent.editForm = editForm.default;

    The Accordion requires some different configuration in the edit form, so I also included a definition for the Display tab of the edit form:

    Accordion.edit.display.js

    "use strict";
    
    Object.defineProperty(exports, "__esModule", {
      value: true
    });
    exports.default = void 0;
    var _default = [{
      key: 'components',
      type: 'datagrid',
      input: true,
      label: 'Sections',
      weight: 50,
      reorder: true,
      components: [{
        type: 'textfield',
        input: true,
        key: 'label',
        label: 'Label'
      }, {
        type: 'textfield',
        input: true,
        key: 'key',
        label: 'Key',
        allowCalculateOverride: true,
        calculateValue: {
          _camelCase: [{
            var: 'row.label'
          }]
        }
      }]
    }];
    exports.default = _default;

    And then the form definition override that references the custom Display Tab elements:

    Accordion.form.js

    "use strict";
    
    require("core-js/modules/es.array.concat");
    
    Object.defineProperty(exports, "__esModule", {
      value: true
    });
    exports.default = _default;
    
    var _NestedComponent = _interopRequireDefault(require("../../../../../../../../../node_modules/formiojs/components/nested/NestedComponent.form"));
    
    var _AccordianEdit = _interopRequireDefault(require("./editForm/Accordian.edit.display"));
    
    function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
    
    function _default() {
      for (var _len = arguments.length, extend = new Array(_len), _key = 0; _key < _len; _key++) {
        extend[_key] = arguments[_key];
      }
    
      return _NestedComponent.default.apply(void 0, [[{
        key: 'display',
        components: _AccordianEdit.default
      }]].concat(extend));
    }

    The file structure for the Accordion component looks like this:

    enter image description here

    Then I just need to register the component in my Angular Project:

    app.component.ts

    import { Component } from '@angular/core';
    import { Formio } from 'formiojs';
    import AccordionComponent from './modules/utility/form-shell/cap-forms/cap-form-designer/components/accordian/accordian';
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html'
    })
    export class AppComponent {
      title = 'app';
    
      constructor() {
        Formio.registerComponent('accordion', AccordionComponent);
      }
    }