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