Skip to main content

Custom runner using plain JS

The Runner library is the workhorse of each Tripetto runner. It turns a form definition into a state machine (more specifically a virtual finite state machine) that holds the state of the form. It handles all the complex logic and the response collection during the execution of the form. It is a headless library, so it has no UI (it can even run at server-side). The idea is to separate concerns, in this case, the actual state and logic of the form, and the visual representation of the form (the UI). You could even attach other means of interaction to the runner, like speech recognition for the input and a speech synthesizer for the output.

tip

If you want to render a visual form, we advise you to use a library like React or a framework like Angular (see the implementation guide for React or Angular to learn how). Rendering and managing a UI using plain JS can be quite challenging (though it is possible if you want).

Basic implementation

The following code shows the basic implementation of a runner. It constructs a new Runner instance. A function handler is specified for the onChange event. Whenever there is a change in the state of the form, this event is invoked. When the form is finished, the onFinish event is invoked. One of the export functions can be used to extract the collected data from the instance.

import { Runner, IRunnerChangeEvent, Instance, Export } from "@tripetto/runner";

const runner = new Runner({
definition: /** Supply your form definition here */,
mode: "paginated"
});

// React to changes
runner.onChange = (event: IRunnerChangeEvent) => {
// 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);
};

Storyline

The onChange event contains the storyline of the runner. This storyline contains all the sections, nodes, and blocks that should render. The mode of operation of the runner 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 (till the point where one of the blocks fails validation). The user does not need to navigate using the next and back buttons (so we can hide those buttons);
  • ahead: Present all blocks to the user, regardless of the section's validation result.

The following example shows how to retrieve all the (active) nodes that should render.

import { Runner, IRunnerChangeEvent } from "@tripetto/runner";

const runner = new Runner({
definition: /** Supply your form definition here */,
mode: "paginated"
});

runner.onChange = (event: IRunnerChangeEvent) => {
// Render all the nodes
event.storyline.nodes.each((node) => {
// Render each node
});
};

Implementation using overrides

If you want a more OOP approach, you can also derive a class from the Runner class and extend it with custom overrides to tap in the functionality of the runner. Let's start with an example.

import { Runner } from "@tripetto/runner";

class CustomRunner extends Runner {
protected onInstanceStart(instance: Instance): void {
// Form is started
}

protected onInstanceFinish(instance: Instance): void {
// Form is finished
}
}

Have a look at the API documentation to see all the methods that you can override.

Creating blocks

Tripetto uses dependency injection to make blocks available to the runner. Blocks are registered using a special class decorator. The following code shows the basic structure of a block:

import { tripetto, NodeBlock } from "@tripetto/runner";

@tripetto({
type: "node",
identifier: "hello-world",
})
export class HelloWorldBlock extends NodeBlock {}

Here we define a block named HelloWorldBlock. It is registered using the @tripetto decorator using the identifier hello-world. This identifier is used in the form definition to select the right block for a node.

tip

See the block implementation guide to learn how to develop blocks for Tripetto.

Render blocks

The block example above does not render anything. We need to add functionality to do so. Let's add an interface we can use as a contract between the runner and the blocks. That allows us to call a render function in the runner for each block.

import { tripetto, NodeBlock, Runner, IRunnerChangeEvent } from "@tripetto/runner";

interface IBlockRenderer extends NodeBlock {
blockToHTML: () => HTMLElement;
}

@tripetto({
type: "node",
identifier: "hello-world",
})
class HelloWorldBlock extends NodeBlock implements IBlockRenderer {
blockToHTML(): HTMLElement {
const element = document.createElement("div");

element.textContent = "Hello world!";

return element;
}
}

const runner = new Runner<IBlockRenderer>({
definition: {
name: "Example form",
sections: [
{
"id": "0abbc4d4aacecbb8f1aa13e77c6ab7a58c416aefcc42f4fd77d6a1a46a4e3afa",
"nodes": [
{
"id": "362221f5ed388ec503a186f66be7829b542cb307b4f43e91473838a5f8c5ac75",
"block": { "type": "hello-world", "version": "1.0.0" }
}
]
}
]
},
mode: "paginated"
});

runner.onChange = (event: IRunnerChangeEvent) => {
// Render all the nodes
event.storyline.nodes.each((node) => {
if (node.block) {
document.body.appendChild(node.block.blockToHTML());
}
});
};

In this example, each block should have a blockToHTML method that generates an HTML element for the block. The runner can then call this method for each block that should render.