How to Extend Forms with Custom Widgets and Libraries

Kinetic Request CE bundles are created using React, which uses ES6 JavaScript. You can use plain JavaScript or jQuery with Kinetic Request CE forms. These two types of JavaScript are not interchangeable and don't work together easily. This is because React requires a “store” to hold and manipulate data: the code is loaded when the first page is loaded, and components are added as they are used in the portal. jQuery or plain JavaScript gets loaded/reloaded at each page change and interpolated at runtime. We then run into the issue of how to maintain consistency between the form content, layout, and actions and the Bundle/Portal's content, layout, and actions, since they cannot talk to each other easily.

Kinetic has maintained a namespace of configuration variables, JavaScript functions, and widgets that can be used bundle-wide since bundles were introduced, named “BUNDLE” in Kinetic RE or “bundle” in Kinetic CE. This namespace is also available to forms, which allows us to bridge the gap between React bundle code and JavaScript form code.

Setting up your link

We need to expose the components from the React bundle before we can try to use them in the form. This requires inserting a component into the bundle object using React's render function. This allows React to keep control and maintain the state of the component (all the logic that you would write in JavaScript) and just pass the “interface” to the form. We can manipulate how the component displays and the data in the component with the component attributes.

If you are using the default bundle, you can add your code to the index.js file located in the app package under src/lib/bundle/helpers (where other widgets of this type are defined).

Alternatively, you can create a new file with your widget code and add it wherever you find appropriate in your bundle structure. If you use this method, you must import the new file into the globals.js file, which is either located in the src directory of the default bundle's app package or the root of the starter bundle.

You will import your component and then add code that leverages the render function from the react-dom package. See the simple example below:

import React from 'react';
import ReactDOM from 'react-dom';
import { bundle } from '@kineticdata/react';
import { Alert } from 'reactstrap';

bundle.helpers = {
  alert: (element, { type, message }) => {
    ReactDOM.render(<Alert color={type}>{message}</Alert>, element);
  }
};

In this example, we have included the react-dom library and are using the render from that to output the Alert component from reactstrap, which is configured with attributes that manage color and a dynamic message. We have added this component to the bundle object under “helpers”. We are passing the element it will be added to (which will be an element from the form), a “type” that is used in the color attribute, and the alert message we want displayed.

Adding the component to your form

Now that the component is in a function in the bundle object we can call it from the form using the bundle namespace:

bundle.helpers.alert(
  K('form').find('#placeholder')[0],
  {
     type: 'success',
     message: 'You succeed!'
  }
);

Note: We use K('form').find(...), which is a wrapper of jQuery's find function, but it has some special logic in it so that it will not be tricked by subforms (guarantees that matches only belong to the specific form itself, not subforms).

So the above code is passing the “placeholder” element for the component to use for it’s display. Everything else is managed in the React code.

Advanced example

The simple example above takes in data and renders the component. In the typical React dataflow, props are the only way that parent components interact with their children. To modify a child, you re-render it with new props. However, there are a few cases where you need to modify a child outside of the typical dataflow. The child to be modified could be an instance of a React component, or it could be a DOM element. For both of these cases, React provides a way to create a reference (ref).

Refs are created using React.createRef() and attached to React elements via the ref attribute. Refs are commonly assigned to an instance property when a component is constructed so they can be referenced throughout the component.

There are a few good use cases for refs:

  • Managing focus, text selection, or media playback.
  • Triggering imperative animations.
  • Integrating with third-party DOM libraries.

Note: Avoid using refs for anything that can be done declaratively. For more in depth information regarding refs see https://reactjs.org/docs/refs-and-the-dom.html

For this advanced example, we will use a third-party library for creating digital signatures called react-signature-canvas, which allows us to capture a digital signature. Unlike the alert, the signature widget needs to provide the Kinetic Form events a way to interact with the resulting SignatureCanvas React component. For example, we may want a way to clear the signature, and we need to be able to sync the signature data with a Form field for storage.

To use the library effectively, we will need to perform the following actions:

  1. Load an already saved signature from a draft or review submission when the component mounts.
  2. Update the signature data when the user modifies or adds a signature.

To accomplish this, we will wrap the library to include the functions that the Form events can use. In the example below, we’ve effectively created a new component called SignatureCanvasWrapper where we’ve exposed the setValue, componentDidMount, and onEnd functions. We can then call the setValue function to update the signature data in our forms.

import React from 'react';
import ReactDOM from 'react-dom';
import { bundle } from '@kineticdata/react';
import SignatureCanvas from 'react-signature-canvas';

class SignatureCanvasWrapper extends React.Component {
  constructor(props) {
    super(props);
    this.onEnd = this.onEnd.bind(this);
  }

  setValue(value) {
    const { height, width } = this.props;
    if (value) {
      this.signatureCanvas.fromDataURL(value, { height, width });
    } else {
      this.signatureCanvas.clear();
    }
  }

  componentDidMount() {
    const { initialValue, height, width } = this.props;
    if (initialValue) {
      this.signatureCanvas.fromDataURL(initialValue, { height, width });
    }
  }

  onEnd() {
    const { onChange } = this.props;
    if (typeof onChange === 'function') {
      onChange(this.signatureCanvas.toDataURL());
    }
  }

  render() {
    const { height, width } = this.props;
    return (
      <SignatureCanvas
        canvasProps={{ height, width }}
        onEnd={this.onEnd}
        ref={el => (this.signatureCanvas = el)}
      />
    );
  }
}

Again, we will need to link our React component with our bundle object so that it is available to the form. Here you can see we are adding signatureCanvas to our bundle object under “helpers” and it is rendering our wrapped library SignatureCanvasWrapper. We are passing the element we want to render the component, initialValue (the signature data from the submission, if it exists), a height and width for the rendering of the component, and an onChange function.

bundle.helpers = {
  signatureCanvas: ({ element, initialValue, height, width, ref, onChange }) => {
    ReactDOM.render(
      <SignatureCanvasWrapper
        initialValue={initialValue}
        onChange={onChange}
        ref={ref}
        height={height}
        width={width}
      />,
      element
    );
  }
};

Next we will use form actions to load and interact with our signature component. The code below is invoked on a page load event:

bundle.helpers.signatureCanvas({
  element: K("form").find("#signature")[0],
  initialValue: K("field[Signature Value]").value(),
  height: 250,
  width: 500,
  ref: function(el) {
    window.signaturePad = el;
  },
  onChange: function(value) {
    K("field[Signature Value]").value(value);
  }
});

This code passes a ref function that stores the element globally so that it is available to all form events. We also pass an onChange callback that stores the signature data in a specified field.

Note: We currently don't have a better way to store things like these elements, but if it does not need to be accessed by other events, we should store it as a local variable in this event code.

Finally, we add an onChange event to the form field that stores the signature data. This event uses the element instance to update the signature if the field value is changed:

if (window.signaturePad) {
  window.signaturePad.setValue(K("field[Signature Value]").value());
}

Now we can implement a clear button by just setting this field's value to null. It does not need to include any code that is specific to this signature component's implementation.