Collector   Edit

npm license downloads Join the community on Spectrum

Making your forms fly

You use the supplementary collector library to handily deploy smart forms in websites and applications. It turns a form definition (created with the editor) 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 collector solves a couple of things at once:

  • Form parsing

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

  • UI freedom

    Also, you can implement your own UI and let the collector 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 collector we recommend using TypeScript. The collector package includes typings to enable optimal IntelliSense support.

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

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

Concepts   Edit

The collector 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 collector 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 collector 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.

Collector 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 collector 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 editor 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 collector 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 collector 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 collector.

Preparation   Edit

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

$ npm install tripetto-collector --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 collector 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 collector   Edit

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

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

// Or import specific symbols if you prefer
import { Collector, Instance } from "tripetto-collector";

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

const collector = new Collector(/** Supply your form definition here */);

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

// Do something with the output when the collector is finished
collector.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 collector that simply renders the JSX.

// Extend the collector and give it JSX render capabilities!
export class JSXCollector extends Tripetto.Collector {
  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 CollectorComponent extends React.PureComponent<{
  definition: IDefinition | string;
}> {
  // Create a new collector instance
  collector = new JSXCollector(this.props.definition);

  // Listen for changes
  componentDidMount(): void {
    this.collector.onChange = () => {
      // Since the collector 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.collector.render();
  }
}

The storyline

The collector 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 collector.

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 collector to handle the rendering of clusters and nodes, we can dive deeper into the specific types of nodes that we want our collector 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 editor. Upon loading blocks will become available in the editor and ready to be attached to any node in the form.

That’s the editor-part of the story. But how does the collector 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 collector.

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

import { NodeBlock, block } from "tripetto-collector";

@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 collector is stored in slots. The available slots in the collector are determined by the editor block implementation (you can read more about that here). Inside the collector 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 collector. 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 collector library contains a base class ConditionBlock. The basic implementation of a condition block looks like this:

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

@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 collector is stored in slots. The available slots in the collector are determined by the editor block implementation (you can read more about that here). Inside the collector 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 collector 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 collector with the supplied form definition
const collector = new Collector(definition);

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

The instance contains the actual session data. The collector 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 collector and thus unavailable, the form definition cannot be loaded. The construction of the collector will throw an error.

Stopping the collector

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

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

Pausing the collector

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

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

Restoring a collector

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

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

The snapshot data can only be used to resume collectors 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 collector = new ExampleDOMCollector(definition);

// Start the collector
const instance = collector.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-collector";

// 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 collectors for Tripetto in the open source domain. We have a special repository where we collect a list of community driven collectors and blocks.

Add your own collector to the list

If you have created a collector 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 collector? Report them here.

Or go to the support page for more support options.