Runner   Edit

npm license downloads Join the community on Spectrum

Making your forms fly

You use the supplementary runner library to handily deploy smart forms in websites and applications. It turns a form definition (created with the builder) into an executable program; a finite state machine that handles all the complex logic and response collection during the execution of the form. Apply any UI framework you like. Or pick from the out-of-the-box implementations for React, Angular, Material-UI, Angular Material and more.

The runner solves a couple of things at once:

  • Form parsing

    Firstly, the runner takes on the task of parsing the form definition from the builder into an executable program. This altogether eliminates the typically complex programming and editing by hand of logic and flows within forms. Once implemented, the runner will autonomously run whatever you create or alter in the builder.

  • UI freedom

    Also, you can implement your own UI and let the runner do the heavy lifting of running the forms. We don’t impose any particular UI framework or library. You decide how it looks. Just wire it up to any UI you like by using the standard DOM-methods of the browser, or by using a library like React or framework like Angular.

You may even go commando and make something completely different. For instance, something like an interface to a braille device, optimizing the experience for visually impaired users.

Try the demo View the code Get the package

For the implementation of the runner we recommend using TypeScript. The runner package includes typings to enable optimal IntelliSense support.

This step-by-step guide for implementing the runner assumes a good understanding of TypeScript, object-oriented programming and webpack.

# Add the Tripetto runner to your project
$ npm i tripetto-runner-foundation

Concepts   Edit

The runner handles the start-to-finish process of flowing the respondent through the smart form, typically based on conditions met along the way. It does so by presenting the form definition, which consists of nodes, clusters and branches, one appropriate step at a time during a so-called instance. Without us imposing any particular UI. And at the end of this process the supplied user data is returned and you can take it from there.

The runner acts as a finite state machine that handles all the complex logic during the execution of the form. This state machine also emits events to any UI you choose to apply to the form. And because it holds its own state, it has some interesting features like pausing and resuming sessions (instances). And even switching devices.

Also, the runner inherently supports multi-paged forms, even though it is still a purely client-side library. This does require a somewhat different approach for the rendering of the forms. But that particular approach comes with complete UI freedom for you and greatly enhanced form responsiveness because, contrary to traditional form handling, no server round trips are needed once the instance is initiated.

Runner diagram

FYI, we tend to call the forms you build with Tripetto smart because they can contain this advanced logic and conditional flows, allowing for jumps from one part of the form to another or the skipping of certain parts altogether; all depending on the respondent’s input.

Overview

The following structural diagram shows the aforementioned entities and their respective relationships in a typical basic arrangement. Important to understand is that each cluster in a branch can in turn have branches originating from that cluster. So the following basic structure can recursively repeat itself.

Form structure

Entities

Before we dive into the implementation of the runner itself we need to define these entities:

Nodes

A form consists of form elements. These will typically be the form input controls, such as a text input control, dropdown control, etc. In Tripetto we call those elements nodes.

Clusters

One or more nodes can be placed in so-called cluster. Generally speaking a cluster will render as a page or view. Based on the form logic defined with the builder certain clusters are displayed or just skipped.

Branches

One or more clusters can form a branch. A branch can be conditional, meaning it will only be displayed in certain circumstances.

Conditions

A branch can contain one or more conditions, which are used to direct flow into the pertaining branch. They are evaluated when a cluster ends. Only subsequent branches with matching condition(s) will be displayed.

Instances

When a a valid form definition is provided to the runner a so-called instance can be started. An instance represents a single input/user session. As long as the form is not completed, the related instance remains active. When an instance is started, the first cluster with nodes is automatically displayed. And when eventually there are no more clusters to display, the form is considered complete. The instance is then ended, an appropriate event emitted and the collected form input data provided.

BTW, instances can also be paused and resumed later on. In a typical UI-oriented application only one instance at a time can be active. More complex use cases are conceivable, but out of scope of this documentation for now.

Slots

Data collected with a runner needs to be stored somewhere. Tripetto works with a slot system where each data component is stored in a separate slot. The slots are defined in the form definition and are directly accessible inside the runner.

Preparation   Edit

To use the runner library in your project you should install the runner package as a dependency using the following command:

$ npm install tripetto-runner-foundation --save

It contains the library runtime files as well as the TypeScript declaration files (typings). When you import a symbol from the library, TypeScript should be able to automatically find the appropriate type definition for you.

Setting up your IDE and building your application

We suggest to use webpack for building your website or application. It can bundle the runner runtime with your project. Take a look at one of our examples to see how to configure webpack for this. If you use webpack to bundle your application, you probably want to install the library package as devDependencies using --save-dev instead of --save.

This documentation is updated as we continue our work on Tripetto and build a community. Please let us know if you run into issues or need additional information. We’re more than happy to keep you going and also improve the documentation.

Implementing the runner   Edit

If you successfully installed the runner package in your project you should be able to import the runner namespace:

// Import the complete namespace as `Tripetto`
import * as Tripetto from "tripetto-runner-foundation";

// Or import specific symbols if you prefer
import { RunnerFoundation, Instance } from "tripetto-runner-foundation";

The next step is to implement the runner. There are multiple ways to do this, but the most easy one is as follows:

const runner = new RunnerFoundation({
  definition: /** Supply your form definition here */
});

// Listen for some changes
runner.onChange = () => {
  // Do stuff here
};

// Do something with the output when the runner is finished
runner.onFinish = (instance: Instance) => {
  // We're done, export the collected data as CSV
  const csv = Export.CSV(instance);

  console.dir(csv);
};

React example

If you use React you can create a runner that simply renders the JSX.

// Extend the runner and give it JSX render capabilities!
export class JSXRunner extends Tripetto.RunnerFoundation {
  render(): React.ReactNode {
    return
      this.storyline && (
        <>
          {this.storyline.map((moment: Tripetto.Moment) =>
            moment.nodes.map((node: Tripetto.IObservableNode) => (
              ...
            )
          }
        </>
      );
  }
}

Then you can create your component:

export class RunnerComponent extends React.PureComponent<{
  definition: IDefinition | string;
}> {
  // Create a new runner instance
  runner = new JSXRunner({
    definition: this.props.definition
  });

  // Listen for changes
  componentDidMount(): void {
    this.runner.onChange = () => {
      // Since the runner has the actual state, we need to update the component.
      // We are good React citizens. We only do this when necessary!
      this.forceUpdate();
    };
  }

  // Render it
  render(): React.ReactNode {
    return this.runner.render();
  }
}

The storyline

The runner generates a storyline that contains all the clusters, nodes and blocks that should be rendered. You can choose from three different operation modes that affect how the storyline is generated:

  • paginated: Blocks are presented page for page and the user navigates through the pages using the next and back buttons;
  • continuous: This will keep all past blocks in view as the user navigates using the next and back buttons;
  • progressive: In this mode all possible blocks are presented to the user. The user does not need to navigate using the next and back buttons (so we can hide those buttons).

Have a look at one of our demos to see how the mode effects the runner.

This documentation is updated as we continue our work on Tripetto and build a community. Please let us know if you run into issues or need additional information. We’re more than happy to keep you going and also improve the documentation.

Implementing nodes   Edit

Now that we have a basic implementation of the runner to handle the rendering of clusters and nodes, we can dive deeper into the specific types of nodes that we want our runner to handle. These node types effectively take care of the implementation of controls in forms, such as text inputs, dropdowns, checkboxes, and the like. But they are not limited to solely visuals controls. They could also encompass a calculation or other ‘invisible’ behavior in the form.

Node types in Tripetto are called blocks. Block packages can be created by anyone and loaded by the builder. Upon loading blocks will become available in the builder and ready to be attached to any node in the form.

That’s the builder-part of the story. But how does the runner then know how to render those blocks for your website or application? Well, it doesn’t. That’s why you need to explicitly implement the blocks you want to support in your runner.

To make this implementation of node blocks as easy as possible, the runner library contains a base class NodeBlock. The basic implementation of a node block looks like this:

import { NodeBlock, block } from "tripetto-runner-foundation";

@block({
  type: "node",
  identifier: "your-block-name"
})
export class YourBlock extends NodeBlock<{
  // Props here
}> {}

Properties

Within your node block class you can access the properties of the block, which are stored in the form definition, through the props member. The type of this data structure is determined by the supplied Properties type in the class definition of your block.

Working with slots

The actual data gathered by the runner is stored in slots. The available slots in the runner are determined by the builder block implementation (you can read more about that here). Inside the runner you can use the method this.valueOf(...) within your NodeBlock derived class to retrieve the data of a certain slot.

Static nodes

Static nodes are used to display static text in the form. These nodes don’t have a block attached to it. So the block property of such nodes is undefined.

This documentation is updated as we continue our work on Tripetto and build a community. Please let us know if you run into issues or need additional information. We’re more than happy to keep you going and also improve the documentation.

Implementing conditions   Edit

Conditions are used to direct flow in a form. They don’t render to a UI. But they do need an implementation in your runner. A typical condition takes a value from a certain slot and evaluates it against an expectation.

To make the implementation of condition blocks as easy as possible, the runner library contains a base class ConditionBlock. The basic implementation of a condition block looks like this:

import { ConditionBlock, block, condition } from "tripetto-runner-foundation";

@Tripetto.block({
    type: "condition",
    identifier: "your-block-name"
})
export class YourBlock extends ConditionBlock<{
  // Props here
}> {
  @condition
  verifyCondition(): boolean {
    return true;
  }
}

Properties

Within your condition block class you can access the properties of the block, which are stored in the form definition, through the props member. The type of this data structure is determined by the supplied Properties type in the class definition of your block.

Working with slots

The actual data gathered by the runner is stored in slots. The available slots in the runner are determined by the builder block implementation (you can read more about that here). Inside the runner you can use the method this.valueOf(...) within your ConditionBlock derived class to retrieve a certain slot and evaluate its data value.

This documentation is updated as we continue our work on Tripetto and build a community. Please let us know if you run into issues or need additional information. We’re more than happy to keep you going and also improve the documentation.

Using instances   Edit

To start a runner session you need to load a form definition and start an instance. The actual loading of the form definition is something you should implement yourself (e.g. by using an HTTP GET). In the following examples we assume there is a form definition stored in the variable definition.

// Creates a runner with the supplied form definition
const runner = new RunnerFoundation({
  definition
});

// Start a new instance
const instance = runner.start();

The instance contains the actual session data. The runner supports instances to run simultaneously, but in a typical UI implementation you can only start one instance at a time (this is the default behavior).

If your form definition includes blocks that are not implemented in your runner and thus unavailable, the form definition cannot be loaded. The construction of the runner will throw an error.

Stopping the runner

To stop the runner, invoke the stop function. This will kill the active instance(s).

// Assuming there is a runner instance with name `runner`
runner.stop();

Pausing the runner

It is possible to pause all instances of a runner using the pause function. All the state data necessary to restore the runner later on is saved to a special data structure called a snapshot. You can save the snapshot data en feed it back to the runner to resume it.

// Assuming there is a runner instance with name `runner`
const snapshot = runner.pause();

Restoring a runner

To restore a runner, simply invoke the restore function of the runner and feed the saved snapshot data to this function. The runner is brought back in the exact state defined by the snapshot.

// Assuming there is a runner instance with name `runner`
// Assuming there is valid snapshot data in the variable `snapshot`
runner.restore(snapshot);

The snapshot data can only be used to resume runners that are loaded with the exact form definition that was used when the snapshot was created. If there is a mismatch between the form definition and the snapshot, the restore-function will fail and return false.

This documentation is updated as we continue our work on Tripetto and build a community. Please let us know if you run into issues or need additional information. We’re more than happy to keep you going and also improve the documentation.

// This example assumes the form definition is loaded to `definition`
const runner = new RunnerFoundation(definition);

// Start the runner
const instance = runner.start();

Using the collected data   Edit

Data is always collected inside an Instance. Tripetto works with a slot system where each data component is stored in a separate slot. The form definition contains the meta information about each slot. More information about slots can be found here.

The easiest way to retrieve all collected data is through the Export API. This API contains functions to easily export collected data to handy data formats, like a fieldset or a CSV file.

import { Export } from "tripetto-runner-foundation";

// Assuming there is an instance with name `instance`

// Export to CSV
const CSV = Export.CSV(instance);

// Export to a fieldset
const fields = Export.fields(instance);

This documentation is updated as we continue our work on Tripetto and build a community. Please let us know if you run into issues or need additional information. We’re more than happy to keep you going and also improve the documentation.

Community   Edit

We hope other enthusiasts will also start to develop runners for Tripetto in the open source domain. We have a special repository where we collect a list of community driven runners and blocks.

Add your own runner to the list

If you have created a runner yourself, create a PR and add yours to the list.

This documentation is updated as we continue our work on Tripetto and build a community. Please let us know if you run into issues or need additional information. We’re more than happy to keep you going and also improve the documentation.

Issues   Edit

Run into issues using the runner? Report them here.

Or go to the support page for more support options.