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, Bootstrap, plain JS 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 View 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, Node, Cluster, Await } from "tripetto-collector";

The next step is to implement the collector abstract class. There a multiple options to do this. Below are two examples:

Basic implementation

This basic implementation overrides some methods of the Collector class to implement the collector:

class FormCollector extends Collector {
  public OnInstanceStart(
    instance: Instance): void {
    // Invoked when an instance starts
  }

  public OnInstanceEnd(
    instance: Instance,
    type: "ended" | "stopped" | "paused"): void {
    // Invoked when an instance ends
  }

  public OnInstanceRender(
    instance: Instance,
    what: Cluster | Node,
    action: Await): void {
    // Invoked when an instance wants to render something.

    if (what instanceof Cluster) {
      // Render the cluster
    } else {
      // Render the node
    }
  }

  public OnInstanceUnrender(
    instance: Instance,
    direction: "forward" | "backward"): void {
    // Invoked when an instance wants to undo a rendering.
  }
}

React implementation

If you use React you can create a collector that simply returns the JSX. The method overrides as shown in the basic example above are not necessary.

// Use the collector to create a JSX renderer
export class CollectorRenderer extends Collector<JSX.Element> {
  public render(): JSX.Element {
    return (
      <section>
        <h1>{this.Ontology ? this.Ontology.Name : "Unnamed form"}</h1>
        {this.Nodes.map((node: IObservableNode<JSX.Element>) => {
          // Render the block if it is available
          if (node.Block) {
              return (
                <div key={node.Id}>
                  {node.Block.OnRender(node.Instance, node.Observer)}
                </div>
              );
          }

          // If there is no block the node should be considered as static text
          return (
              <div key={node.Id}>
                  <h2>{node.Props.Name}</h2>
              </div>
          );
        })}
      </section>
    );
  }
}

Then you can create your component:

// Define our props
interface IProps {
  definition: IMap | string;
}

// Create a React component
export class CollectorComponent extends React.Component<IProps> {
  private readonly collector: CollectorRenderer;
  private mounted = false;

  constructor(props: IProps) {
    super();

    // Create a new collector instance
    this.collector = new CollectorRenderer(props.definition, false, this.update.bind(this));

    // Start the collector
    this.collector.Start("single");
  }

  // This function is only invoked when the component should update
  private update(): void {
    if (this.mounted) {
      this.forceUpdate();
    }
  }

  // Render our component
  public render(): JSX.Element {
    return this.collector.render();
  }

  public componentDidMount(): void {
    this.mounted = true;
  }

  public componentWillUnmount(): void {
    this.mounted = false;
  }
}

Decorators, properties and methods

OnInstanceStart(instance)

Invoked when an instance is started. This is a perfect place to render static UI elements that should be available during the whole life cycle of the instance.

Parameters
instance
Reference to the new instance.
OnInstanceEnd(instance, type)

Invoked when the instance ends. The UI can now be destructed.

Parameters
instance
Reference to the instance which has ended.
type
Specifies the reason why the instance has ended. Can be one of the following values:
  • ended: Instance has ended normally;
  • stopped: Instance was forced to stop;
  • paused: Instance was paused.
OnInstanceRender(instance, what, action)

Invoked when an instance wants to render something. This is the place where you should render clusters and nodes.

Parameters
instance
Reference to the active instance.
what
Specifies what needs to be rendered. Can be one of the following types:
  • Cluster: A cluster can contain one or more nodes;
  • Node: A node implements a certain element type. We call these types blocks. The block is available through the property what.Block. If this property is undefined the node does not have a block and should be considered a static element. If it has a block, you should invoke the method what.Block.OnRender to allow the block to render.
action
Contains the await pointer that is used to request the collector to take a step. Invoke action.Done() to step forward or action.Cancel() to step backward.
OnInstanceUnrender(instance, direction)

Invoked when an instance wants to undo a rendering. This method is invoked when the collector makes a step to another cluster. You should prepare the view for the rendering of the new cluster. Typically you should remove the items rendered in the previous OnInstanceRender call from your view.

Parameters
instance
Reference to the active instance.
direction
Specifies the step direction:
  • forward: Instance takes a step forward;
  • backward: Instance takes a step backward.

How it works

So what happens when the collector instance is started? The following steps are then performed:

  1. The OnInstanceStart method is invoked when an instance starts and the collector steps into the first cluster;
  2. The OnInstanceRender method is invoked for each cluster (the what parameter will contain a reference to the cluster);
  3. For each node inside the cluster the OnInstanceRender method is invoked (the what parameter will contain a reference to the node);
  4. When the collector steps to another cluster (invoked by action.Done() or action.Cancel()), the OnInstanceUnrender method is invoked. The collector will only step into the next cluster if the current cluster passes the cluster validation;
  5. When there are no more clusters to step to, the form is considered done and the OnInstanceEnd method is invoked.

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.

import * as Tripetto from "tripetto-collector";
import * as React from "react";

interface IProps {
  definition: Tripetto.IMap | string;
}

// Define our collector React component
export class Collector extends React.Component<IProps> {
  private readonly collector: CollectorRenderer;
  private mounted = false;

  constructor(props: IProps) {
    super();

    // Create a new collector instance and start it
    this.collector = new CollectorRenderer(props.definition, false, this.update.bind(this));
    this.collector.Start("single");
  }

  private update(): void {
    if (this.mounted) {
      this.forceUpdate();
    }
  }

  public render(): JSX.Element {
    return this.collector.render();
  }

  public componentDidMount(): void {
    this.mounted = true;
  }

  public componentWillUnmount(): void {
    this.mounted = false;
  }
}

// Define the collector renderer
export class CollectorRenderer extends Tripetto.Collector<JSX.Element> {
  public render(): JSX.Element {
    if (!this.Instance || !this.Instance.IsRunning) {
      return `No instance available!`;
    }

    return (
      <section>
        <h1>{this.Ontology ? this.Ontology.Name : "Unnamed form"}</h1>
        {this.Nodes.map((node: Tripetto.IObservableNode<JSX.Element>) => {
          // Render the block if it is available
          if (node.Block) {
            return (
              <div key={node.Id}>
                  {node.Block.OnRender(node.Instance, node.Observer)}
              </div>
            );
          }

          // If there is no block the node should be considered as static text
          return (
            <div key={node.Id}>
              {node.Props.NameVisible && node.Props.Name && <h3>{node.Props.Name}</h3>}
              {node.Props.Description && <p>{node.Props.Description}</p>}
            </div>
          );
        })}
        <button type="button" disabled={this.Instance.Steps === 0} onClick={() => this.clickButtonPrevious()}>
          Back
        </button>
        <button type="button" disabled={this.IsValidationFailed} onClick={() => this.clickButtonNext()}>
          {this.Instance.IsAtEnd ? "Complete" : "Next"}
        </button>
        <span>{this.ProgressPercentage}%</span>
      </section>
    );
  }

  // Handle clicks on the `Previous`-button
  private clickButtonPrevious(): void {
    if (this.Observer) {
      this.Observer.Cancel();
    }
  }

  // Handle clicks on the `Next`-button
  private clickButtonNext(): void {
    if (this.Observer) {
      this.Observer.Done();
    }
  }
}

import * as Tripetto from "tripetto-collector";

// Create a collector which renders to the DOM
export class ExampleDOMCollector extends Tripetto.Collector<HTMLElement> {
  public OnInstanceStart(instance: Tripetto.Instance): void {
    // Create a form element with a title

    const form = document.createElement("form");
    const h1 = document.createElement("h1");

    form.id = "form-" + instance.Hash;
    h1.textContent = this.Ontology ? this.Ontology.Name : "Unnamed form";

    form.appendChild(h1);
    document.body.appendChild(form);
  }

  public OnInstanceEnd(instance: Tripetto.Instance, type: "ended" | "stopped" | "paused"): void {
    // We're done, remove the form from the DOM

    const form = document.getElementById("form-" + instance.Hash);

    if (form) {
      form.remove();
    }

    if (type === "ended") {
      // Form completed!
      // Output the collected data to the console
      console.dir(instance.Values);
    }
  }

  public OnInstanceRender(
    instance: Tripetto.Instance,
    what: Tripetto.Cluster<HTMLElement> | Tripetto.Node<HTMLElement>,
    action: Tripetto.Await
  ): void {
    const form = document.getElementById("form-" + instance.Hash);

    if (!form) {
      return;
    }

    if (what instanceof Tripetto.Cluster) {
      // Render a button
      const button = document.createElement("a");

      button.id = "button-" + instance.Hash;
      button.textContent = "Next";
      button.addEventListener("click", () => action.Done());

      form.appendChild(button);

      return;
    }

    if (what.Block) {
      // Render the block
      form.appendChild(what.Block.OnRender(instance, action));
    } else {
      // Render static item
      const h2 = document.createElement("h2");

      h2.id = "node-" + instance.Hash;
      h2.textContent = what.Props.Name;

      form.appendChild(h2);
    }
  }

  public OnInstanceUnrender(instance: Tripetto.Instance, direction: "forward" | "backward"): void {
    const node = document.getElementById("node-" + instance.Hash);

    if (node) {
      node.remove();
    }
  }
}

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. This class requires two methods: An OnRender method, which renders the block, and an OnValidate method, which allows you to validate your block. The basic implementation of a node block looks like this:

import { NodeBlock, node, Instance, Await, Callback } from "tripetto-collector";

@node("your-block-name")
export class YourBlock extends NodeBlock<Rendering, Properties> {
  public OnRender(
    instance: Instance,
    action: Await): Rendering {
    // Invoked when the block should return or do it's rendering
  }

  public OnValidate(
    instance: Instance,
    current: "unknown" | "fail" | "pass",
    callback: Callback<boolean>): boolean | Callback<boolean> {
    // Invoked when the block should be validated
  }
}

The NodeBlock<Rendering, Properties> class takes two type parameters:

  • Rendering: This is the return type of the render method (set it to void if your render function does not return anything);
  • Properties: This is an interface that describes the available properties of the block. Most block packages for the editor expose an interface with the properties it implements. This interface can be used here to preserve duplicate interface declaration.

Decorators, properties and methods

node(id)

This decorator specifies the identifier of your block and it should exactly match the identifier of the block implementation for the editor.

Parameters
id
Specifies a unique identifier for the block.
OnRender(instance, action): rendering

Invoked when the block should be rendered.

Parameters
instance
Reference to the instance.
action
Use this parameter to notify the collector the node block is ready by invoking action.Done(). It requests the next step in the execution of the form.
OnValidate(instance, current, callback)

Invoked when the block should be validated.

Parameters
instance
Reference to the instance.
current
Contains the previous validation state of the block. Can be one of the following values:
  • unknown: Previous validation unknown;
  • fail: Previous validation failed;
  • pass: Previous validation passed.
callback
If you want to perform an asynchronous validation, you should use this object as a return value for the validation function. When your validation is done, you should invoke callback.Done().

Validation examples:

// Synchronous example
public OnValidate(): boolean {
  // Retrieves data from a certain slot
  const slot = this.SlotAssert("example");
  const data = this.DataAssert<string>(instance, slot);

  return !slot.Required || data.Value !== "";
}

// Asynchronous example
public OnValidate(
  instance: Instance,
  current: "unknown" | "fail" | "pass",
  callback: Callback<boolean>): Callback<boolean> {

  // Let's take some time to do this validation
  setTimeout(() => {
    // Retrieves data from a certain slot
    const slot = this.SlotAssert("example");
    const data = this.DataAssert<string>(instance, slot);

    // Set the payload of the callback
    callback.Payload = !slot.Required || data.Value !== "";
  }, 1000);

  return callback;
}

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. Your properties interface needs to extend INodeBlock.

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.Data<T>(...) or this.DataAssert<T>(...) within your NodeBlock derived class to retrieve the data of a certain slot. In the code sample to the right (or below if you’re reading this on a small screen) you can see how this works. The DataAssert function will throw an error if the requested slot/data is not found. It’s return value is always a valid data slot. The Data function on the other hand does not throw an error in case of an invalid slot/data. In that case it returns undefined. If you want to retrieve the slot itself (this contains the slot metadata) you can use this.Slot(...) or this.SlotAssert(...).

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. The OnInstanceRender of your collector class is a good place to verify whether a node has a block. If so, the block should be rendered. If not, you can render the node Name and optional Description to your UI.

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.

import * as React from "react";
import { NodeBlock, INodeBlock, node, Instance, Await } from "tripetto-collector";

interface IExample extends INodeBlock {
  Name: string;
}

@node("example")
export class Example extends NodeBlock<JSX.Element, IExample> {
  public OnRender(instance: Instance): JSX.Element {
    const slot = this.SlotAssert("value");
    const data = this.DataAssert<string>(instance, slot);

    return (
        <label title={this.Node.Props.Explanation}>
            {this.Node.Props.Name && this.Node.Props.NameVisible && this.Node.Props.Name}
            {this.Node.Props.Description && <p>{this.Node.Props.Description}</p>}
            <input
                type="text"
                required={slot.Required}
                defaultValue={data.Value}
                placeholder={this.Node.Props.Placeholder}
                onChange={(e: React.ChangeEvent<HTMLInputElement>) => (data.Value = e.target.value)}
                onBlur={(e: React.FocusEvent<HTMLInputElement>) => ((e.target as HTMLInputElement).value = data.Value)}
            />
        </label>
    );
  }

  public OnValidate(
    instance: Instance): boolean {

    const slot = this.SlotAssert("value");
    const value = this.DataAssert<string>(instance, slot);

    return !slot.Required || value.Value !== "";
  }
}
import { NodeBlock, INodeBlock, node, Instance, Await } from "tripetto-collector";

interface IExample extends INodeBlock {
  Name: string;
}

@node("example")
export class Example extends NodeBlock<HTMLElement, IExample> {
  public OnRender(instance: Instance): void {
    const data = this.DataAssert<string>(instance, "value");
    const label = document.createElement("label");
    const input = document.createElement("input");

    input.value = data.Value;

    input.setAttribute("type", "text");
    input.addEventListener("change", () => (data.Value = input.value));

    label.textContent = this.Props.Name;
    label.appendChild(input);

    return label;
  }

  public OnValidate(
    instance: Instance): boolean {

    const slot = this.SlotAssert("value");
    const data = this.DataAssert<string>(instance, slot);

    return !slot.Required || data.Value !== "";
  }
}

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. This class requires a single method OnCondition, which performs the conditional evaluation. The basic implementation of a condition block looks like this:

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

@condition("your-block-name")
export class YourBlock extends ConditionBlock<Properties> {
  public OnCondition(
    instance: Instance,
    callback: Callback<boolean>): boolean | Callback<boolean> {
    // Invoked when the condition should be evaluated
  }
}

The ConditionBlock<Properties> class takes one type parameter:

  • Properties: This is an interface that describes the available properties of the block. Most block packages for the editor expose an interface with the properties it implements. This interface can be used here to preserve duplicate interface declaration.

Decorators, properties and methods

condition(id)

This decorator specifies the identifier of your block and it should exactly match the identifier of the block implementation for the editor.

Parameters
id
Specifies a unique identifier for the block.
OnCondition(instance, callback)

Invoked when the condition should be evaluated.

Parameters
instance
Reference to the instance.
callback
If you want to perform an asynchronous evaluation, you should use this object as a return value for the condition function. When your evaluation is done, you should feed the result to callback.Payload.

Examples:

// Synchronous example
public OnCondition(): boolean {
  // Retrieves data for the attached slot
  const data = this.DataAssert<string>(instance);

  // If the data is not empty, the condition is `true`
  return data.Value !== "" ? true : false;
}

// Asynchronous example
public OnCondition(
  instance: Instance,
  callback: Callback<boolean>): Callback<boolean> {

  // Let's take some time to do this evaluation
  setTimeout(() => {
    // Retrieves data for the attached slot
    const data = this.DataAssert<string>(instance);

    // We're done and always ok :-)
    callback.Payload = data.Value !== "" ? true : false;
  }, 1000);

  return callback;
}

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. Your properties interface needs to extend IConditionBlock.

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.Data(...) within your ConditionBlock derived class to retrieve a certain slot and evaluate its data value. In the code sample to the right (or below if you’re reading this on a small screen) you can see how this works.

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.

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

interface IExample extends IConditionBlock {
  Match: string;
}

@condition("example")
export class Example extends ConditionBlock<IExample> {
  public OnCondition(instance: Instance): boolean {
    const data = this.DataAssert<string>(instance);

    return data.String === this.Props.Match ? true : false;
  }
}

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();

Resuming a collector

To resume a collector, simply invoke the Resume 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.Resume(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 Resume-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 and available through the Values property of that 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 Values property of an instance. This is an array with all the collected data values. It has the following format:

{
  "0042d46c...": { // Hash of the slot
    "Name": "", // Name of the slot
    "Values": { // Collected values for the slot
      "*": { // Context hash (or `*` for global context)
        "Value": "", // The actual value
        "Reference": "", // Optional value reference
        "Time": 0, // UTC time the value was set
        "Context": [] // Context of the value
      }
    }
  }
}

As you can see the values are stored using the slot hash as a key. You can retrieve a collection of slots using the Slots property of an instance. Each slot value contains the name of the slot. If a slot alias is set, this alias is used as name. Otherwise the slot name or placeholder will be used. It is possible multiple values for a certain slot are stored. This will occur when your form definition contains branches that use the culling mode each. This will iterate a branch for each matching condition, instead of only for the first matching condition. In the survey world this feature is often referred as piping: The same set of questions are repeatedly asked for certain given answers or subjects. Tripetto supports piping in its core.

If a slot value is established for normal branches the value of a slot is always stored under the * key. This is the global value of a slot since it is amassed in the global context.

When a slot value is established from a branch that is iterated multiple times for multiple conditions, the key is calculated using the condition hashes that are stored in the Context array of a value. This array contains the condition hash(es) of the active condition(s) for a certain iteration. These conditions effectively determine the context of the stored value.

Let’s explain that using the following example. Imagine you have a form definition with a certain branch which is taken for two conditions (the complete branch is iterated for each condition). This will result in the following data collection:

{
  "0042d46c...": { // Hash of the slot
    "Name": "Example slot",
    "Values": {
      "53a271fa...": { // Hash of condition 1
        "Value": "Value in the context of condition 1",
        "Reference": "",
        "Time": 0,
        "Context": [
          "53a271fa..."
        ]
      },
      "e6a9f1fe...": { // Hash of condition 2
        "Value": "Value in the context of condition 2",
        "Reference": "",
        "Time": 0,
        "Context": [
          "e6a9f1fe..."
        ]
      }
    }
  }
}

The context array of a slot value can contain multiple condition hashes if a combination of conditions is active. This combination of conditions forms the context in which the value is established.

The keys for the contextual slot values are composed using the condition hashes of the context. If the context is global the key of that value is always *. If the context array contains a single condition, the value key is the hash of that single condition. If the context array contains 2 or more conditions, the hashes of these conditions are used to calculate a new hash. The condition hashes are concatenated in chronological order and then a SHA2_256 hash is generated for this concatenated string. This hash is used as key.

Hook to an instance

You can create a hook to get notified when an instance ends:

instance.Hook("OnEnd", "synchronous", (event: IInstanceEndEvent) => {
  // Output the data to the console when the instance ends.
  console.dir(event.Instance.Values);
});

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.

// Log all the collected data to the console
console.dir(instance.Values);

Examples Edit

We have some example implementations that you can use as a template. They’re published under the MIT license. Have a blast!


React collector example

Uses the React library to render a collector as a reusable component. The React implementation is the most simple and compact (in terms of lines of code) way to implement a collector. It is easy to understand and shows the power of the collector. This example uses a simple Bootstrap template to keep things as simple as possible (take a look at the React collector with Material-UI for an example with a more fancy UI).


React collector with Material-UI example

Uses the React library to render a collector as a reusable component using Material-UI for the UI. It is still an easy to understand example, but is has a nice visual style to it.


Angular collector example

Uses the Angular framework to render a collector. Implemented as a reusable module. This example uses a simple Bootstrap template to keep things as simple as possible.


Angular Material collector example

Uses the Angular framework with Angular Material to render a collector.


Bootstrap collector example

Uses the Bootstrap toolkit to render a collector.


Plain collector example

Uses the collector library and only standard DOM manipulation functions of the browser (plain JavaScript). Contains no fancy visual styles and is easy to understand because of the minimal code footprint.


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.