Skip to main content

Creating a visual block

A visual block for Tripetto is often a new question type you wish to add. It needs two parts to work properly:

  • A builder part that makes the block available to the visual builder, so the block becomes usable in a form;
  • A runner part that handles the rendering of the block in the runner that runs the form (the visual part of the block).

This tutorial covers both parts, beginning with the builder part. Let's assume we want to build a new question type to collect text input (single or multi-line) from the form respondent. In this example we are going to use the default HTML input and textarea controls in the runner part.

🏗️ Builder part

The builder part of a block allows the visual builder to consume the block. It allows the builder user (form editor) to select the new block, attach it to a node, and configure all of its properties and settings. For this tutorial, we'll start from scratch and prepare a new project for our text input block (you may skip this and jump straight to the block implementation when you don't need help preparing your project).

Can't wait to see the result of the builder part tutorial? Click the buttons below to run or try a live code example.

Run Try on CodeSandbox


▶️ Prepare your project

1️⃣ Create a project

First, create a new folder for the project (for example, tripetto-block-text-input). Then open a command-line terminal for that new folder and run the following command to initiate the project:

npm init
tip

You can just hit the enter key for all questions asked when running the init command. The default configuration is good for now.

2️⃣ Add dependencies

Now we can add the required packages for our block. In this example, we'll use webpack as bundler. Besides the webpack dependencies we need the TypeScript compiler and Tripetto's Builder package. We also add image-webpack-loader and url-loader to enable webpack to process images, ts-loader to process TypeScript with webpack and concurrently to allow to run Tripetto's builder and webpack simultaneous during development. Let's add these dependencies by running the following command in your project folder:

npm install tripetto typescript webpack webpack-cli image-webpack-loader url-loader ts-loader concurrently

3️⃣ Add configuration files

Next, add the following files to the root of your project folder. It's the configuration for the TypeScript compiler and webpack bundler.

{
"compileOnSave": false,
"compilerOptions": {
"target": "ES5",
"moduleResolution": "Node",
"newLine": "LF",
"strict": true,
"experimentalDecorators": true
},
"include": ["./src/**/*.ts"]
}
info

Make sure the experimentalDecorators feature is enabled. We need support for decorators.

4️⃣ Project configuration

Now open the package.json file that was generated in the first step and add the highlighted lines to it:

{
"name": "tripetto-block-text-input",
"main": "./dist/builder.bundle.js",
"scripts": {
"test": "webpack --mode development && concurrently -n \"tripetto,webpack\" -c \"blue.bold,green\" -k -s \"first\" \"tripetto ./example.json --verbose\" \"webpack --mode development --watch\""
},
"dependencies": {
"image-webpack-loader": "^8.1.0",
"tripetto": "^4.5.0",
"ts-loader": "^9.2.8",
"typescript": "^4.6.3",
"webpack": "^5.72.0",
"webpack-cli": "^4.9.2"
},
"tripetto": {
"blocks": [
"."
]
}
}

Here the main property specifies the location of the JS bundle that webpack will generate. The tripetto section contains the configuration for the builder. In this case, it tries to load the block that's in the same folder (.). Tripetto will lookup the package.json file and read the main property to find the right JS file for the block.

info

More information about configuring the Tripetto CLI using package.json can be found in the configuration section of the builder documentation.

5️⃣ Prepare source file

The last step before we will test the setup is to add an empty source file that we'll use later on to declare the block in. Create a folder with the name src and add an empty file to it with the name index.ts. This will be the entry point for the block. The structure of the project should now look something like this:

project/
├─ src/
│ └─ index.ts
├─ package.json
├─ tsconfig.json
└─ webpack.config.js

6️⃣ Test it!

Now you should be able to run this setup. Run the following command to start webpack together with the Tripetto builder:

npm test
info

Webpack will now monitor code changes and update the bundle on each change. Hit F5 in the browser to reload the builder with the updated bundle. Or follow the bonus step below to enable live-reloading of the builder.

7️⃣ Live-reloading (bonus)

If you want, you can enable live-reloading of the builder on each code change. To enable that, we need the webpack-livereload-plugin package:

npm install webpack-livereload-plugin

Then, open webpack.config.js and add the highlighted lines:

webpack.config.js
const path = require("path");
const webpackLiveReload = require("webpack-livereload-plugin");

module.exports = {
target: ["web", "es5"],
entry: "./src/index.ts",
output: {
path: path.resolve(__dirname, "dist"),
filename: "builder.bundle.js",
},
module: {
rules: [
{
test: /\.ts$/,
loader: "ts-loader",
},
{
test: /\.svg$/,
use: ["url-loader", "image-webpack-loader"],
},
],
},
resolve: {
extensions: [".ts", ".js"],
},
externals: {
// Never include the builder in the bundle!
tripetto: "Tripetto",
},
plugins: [
new webpackLiveReload({
appendScriptTag: true,
}),
]
};

Now the builder should automatically reload the bundle when the code is changed.


▶️ Block implementation

Now we get to the interesting part: Building the actual block!

1️⃣ Declare the block

Let's start with the basic code of a visual block and then go through that code. Update your empty index.ts file to the code shown below and add the file icon.svg to the same folder (it's the icon that Tripetto will use for this block).

import { tripetto, NodeBlock } from "tripetto";
import icon from "./icon.svg";

@tripetto({
type: "node",
identifier: "text-input",
label: "Text input",
icon
})
class TextInputBlock extends NodeBlock {
// Block implementation here
}

This code is all you need to declare a new class with the name TextInputBlock. The class is derived from the base class NodeBlock that is imported from the Tripetto builder package. The base class has all the core functionality of a block, so you can easily extend it with the functionality you need. This example has no further implementation yet, so the class is still empty. We'll come to that in a moment.

First, let's talk about the @tripetto decorator. This so-called class decorator is used to register the block to the Tripetto builder. Tripetto uses dependency injection to retrieve blocks that are registered using the decorator. The identifier, label, and icon of the block are all supplied to that decorator and are used by the visual builder to allow the user to select the block and assign it to a node.

Now we can add the functionality to the block that we need for the text input question type. We need to do three things for that on the builder part:

  1. Define the data the block will collect;
  2. Add a property that holds if the input type is single or multi-line;
  3. Instruct the builder how the user can manage the block properties and settings.

Let's do that in the following steps!

2️⃣ Define block slots

The next step is to specify the type of data this block will collect. In the Tripetto world, data is collected using slots. Each slot can contain a single data item. In this case, we need a single slot that can hold the text input value, which is a string. So, we are going to use the String slot type for that. To define the slot, we need to add a method to our block class and decorate it with the @slots decorator. This decorator instructs Tripetto to use that method when it needs the slots for the block. Update the highlighted lines in index.ts:

index.ts
import { tripetto, slots, NodeBlock, Slots } from "tripetto";
import icon from "./icon.svg";

@tripetto({
type: "node",
identifier: "text-input",
label: "Text input",
icon
})
class TextInputBlock extends NodeBlock {
@slots
onSlots(): void {
this.slots.static({
type: Slots.String,
reference: "value",
label: "Text input value"
});
}
}

As you can see, we've added a new method onSlots (you can choose any name you want for this method), and decorated it with @slots. The method implements the slots field of the TextInputBlock class to create a new static slot for the text input value. And that's all you need to do on the builder part. You will see later on in this tutorial how we will use this slot to supply a value to it in the runner part of the block.

tip

Read the Slots guide to learn more about slots and its possibilities.

3️⃣ Define block properties

Next, we want to add a property to the TextInputBlock class to allow the user to specify if the block will collect single or multi-line text. This is as simple as adding a field to the class and decorate it with the @definition decorator. This decorator instructs the builder to include the field in the form definition. The property will then be automatically saved to and retrieved from the form definition. Update the highlighted lines in index.ts:

index.ts
import { tripetto, slots, definition, NodeBlock, Slots } from "tripetto";
import icon from "./icon.svg";

@tripetto({
type: "node",
identifier: "text-input",
label: "Text input",
icon
})
class TextInputBlock extends NodeBlock {
@definition
isMultiline?: boolean;

@slots
onSlots(): void {
this.slots.static({
type: Slots.String,
reference: "value",
label: "Text input value"
});
}
}
tip

Read the Properties guide to learn more about defining properties for your block.

4️⃣ Define block editor

The final step is to instruct the builder how to manage the properties and settings for the block. These properties and settings can be managed by clicking on a node that has the block assigned. It will open an editor panel that shows all the properties and settings for the block. For our text input block, we are going to define six elements that can be managed:

  • The name of the block;
  • A placeholder to show in the text input field when it is empty;
  • A toggle to specify if a value is required or not;
  • A toggle to specify an alias for the slot in the form dataset;
  • A toggle to specify if the slot should be included in the form dataset or not;
  • A feature to specify if the input mode is single or multi-line.

The result will look something like this:

Again we need to add a method to the TextInputBlock class and then decorate it with the @editor decorator. This method will be invoked by the builder when the editor panel for the block is requested. We can use the editor field of the TextInputBlock class to define UI controls to manage the block. Update the highlighted lines in index.ts:

index.ts
import { tripetto, slots, definition, editor, isBoolean, NodeBlock, Slots, Forms } from "tripetto";
import icon from "./icon.svg";

@tripetto({
type: "node",
identifier: "text-input",
label: "Text input",
icon
})
class TextInputBlock extends NodeBlock {
@definition
isMultiline?: boolean;

// Add a field that holds a reference to the slot
valueSlot!: Slots.String;

@slots
onSlots(): void {
this.valueSlot = this.slots.static({
type: Slots.String,
reference: "value",
label: "Text input value"
});
}

@editor
onEdit(): void {
// First add the general group title (see https://tripetto.com/sdk/docs/blocks/api/classes/EditorOrchestrator#groups)
this.editor.groups.general();
// Add the name feature (see https://tripetto.com/sdk/docs/blocks/api/classes/EditorOrchestrator#name)
this.editor.name();
// Add the placeholder feature (see https://tripetto.com/sdk/docs/blocks/api/classes/EditorOrchestrator#placeholder)
this.editor.placeholder();

// Add the settings group title (see https://tripetto.com/sdk/docs/blocks/api/classes/EditorOrchestrator#groups)
this.editor.groups.settings();
// Add the required feature (see https://tripetto.com/sdk/docs/blocks/api/classes/EditorOrchestrator#required)
this.editor.required(this.valueSlot);
// Add the alias feature (see https://tripetto.com/sdk/docs/blocks/api/classes/EditorOrchestrator#alias)
this.editor.alias(this.valueSlot);
// Add the exportable feature (see https://tripetto.com/sdk/docs/blocks/api/classes/EditorOrchestrator#exportable)
this.editor.exportable(this.valueSlot);

// Add a custom feature to toggle the text input mode (see https://tripetto.com/sdk/docs/blocks/api/classes/EditorOrchestrator#option)
this.editor.option({
name: "Input mode",
form: {
title: "Text input mode",
controls: [
new Forms.Checkbox(
"Allow multi-line text input",
Forms.Checkbox.bind(this, "isMultiline", false, true)
)
],
},
activated: isBoolean(this.isMultiline)
});
}
}

Run Try on CodeSandbox

This code utilizes the built-in controls for editing the name, and placeholder of a block, and the required, alias, and exportable properties of the slot. We also use the option method to define a custom feature that controls if the text input supports single- or multi-line text input.

tip

Read the Block editor guide to learn more about building block editor panels.

💯 Builder part done!

And there, we have completed the builder part of the block! You can now create an example form definition that contains your new block. Click the Save button in the toolbar of the builder CLI to save it to ./example.json. The contents of this file can be supplied to the runner while developing/testing the runner part.

Next, we're going to define the runner part of the block.

🏃 Runner part

The runner part of a visual block handles the rendering of the block in the runner. It also collects the data for the block and performs any validation if necessary. The rendering of the block greatly depends on the type of runner that is targeted. In this tutorial we will use the Autoscroll runner, but the implementation for the other stock runners is similar. If you have a custom runner, the implementation depends on the structure of that runner, and the implementation may deviate from what is shown here.

Just like for the builder part, we'll start from scratch and prepare a new project for implementing the text input block in the runner (you may skip this and jump straight to the block implementation when you don't need help preparing your project). Let's get started!

Can't wait to see the end result of the runner part tutorial? Click the buttons below to run or try a live code example.

Run Try on CodeSandbox


▶️ Prepare your project

1️⃣ Create a project

First, create a new folder for the project. Then open a command-line shell for that new folder and run the following command to initiate the project:

npm init
tip

You can just hit the enter key for all questions asked when running the init command. The default configuration is good for now.

2️⃣ Add dependencies

Now we can add the required packages for our block. In this example, we'll use webpack as bundler, but you can use any other bundler if you like. Besides the webpack dependencies we need the TypeScript compiler and, of course, the Tripetto Runner Foundation and Tripetto Autoscroll Runner packages. The stock runners use React, and you need to install that as well. In this example we use webpack-dev-server to run it. Let's add all these dependencies by running the following command in your project folder:

npm install tripetto-runner-foundation tripetto-runner-autoscroll react react-dom typescript webpack webpack-cli ts-loader webpack-dev-server

3️⃣ Add configuration files

Next, add the following files to the root of your project folder. It's the configuration for the TypeScript compiler and webpack bundler.

{
"compileOnSave": false,
"compilerOptions": {
"jsx": "react",
"target": "ES5",
"moduleResolution": "Node",
"newLine": "LF",
"strict": true,
"experimentalDecorators": true
},
"include": ["./src/**/*.tsx"]
}
info

Make sure the experimentalDecorators feature is enabled. We need support for decorators.

4️⃣ Project configuration

Now open the package.json file that was generated in the first step and add the highlighted line to it:

{
"name": "tripetto-block-runner",
"scripts": {
"test": "webpack-dev-server --mode development"
},
"dependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0",
"tripetto-runner-autoscroll": "^5.7.1",
"tripetto-runner-foundation": "^6.3.0",
"ts-loader": "^9.2.8",
"typescript": "^4.6.3",
"webpack": "^5.72.0",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.8.1"
}
}

5️⃣ Add files

The last step before we will test the setup is to add some files. Create two folders with the name src and static. Then add the file index.tsx to the src folder and index.html to the static folder.

Make sure to replace the code /* Supply your form definition here */ in index.tsx with a form definition that contains your new block.

import { run } from "tripetto-runner-autoscroll";

run({
display: "page",
definition: /* Supply your form definition here */
});

The structure of the project should now look something like this:

project/
├─ src/
│ └─ index.tsx
├─ static/
│ └─ index.html
├─ package.json
├─ tsconfig.json
└─ webpack.config.js

6️⃣ Test it!

Now you should be able to run this setup. Run the command below to start the development server. Then browse to http://localhost:9000/ to see the runner in action!

npm test

▶️ Block implementation

Now that we have the runner up and running, it's time to implement the runner part of the block.

1️⃣ Declare the block

Let's start with the basic code of the runner part of a visual block and then go through that code. Update the index.tsx file to the code shown below:

index.tsx
import { tripetto, NodeBlock } from "tripetto-runner-foundation";
import { run, namespace, IAutoscrollRendering } from "tripetto-runner-autoscroll";

@tripetto({
type: "node",
namespace,
identifier: "text-input",
})
class TextInputBlock extends NodeBlock implements IAutoscrollRendering {
// Block implementation here
}

run({
display: "page",
definition: /* Supply your form definition here */
});

This code declares a new class with the name TextInputBlock. The class is derived from the base class NodeBlock that is imported from the Tripetto Runner Foundation package. It also implements the interface IAutoscrollRendering from the Tripetto Autoscroll runner. That interface dictates what the class must implement in order to work with the runner.

The @tripetto decorator is used to register the block to the Tripetto runner. In this case, the namespace of the Autoscroll runner is supplied. This assures the block is registered under the namespace of that runner.

Now we can add functionality to the block. We need to do three things on the runner part:

  1. Define the block properties that are declared in the builder part;
  2. Implement block validation;
  3. Implement block rendering.

Let's do that in the following steps!

2️⃣ Define block properties

In our builder part we declared a field isMultiline that is used to indicate if the text input control should be single or multi-line. We need this field in the runner part, so we add the property to the runner part class declaration to make it available to the class. Update the highlighted lines in the TextInputBlock class:

index.tsx
import { tripetto, NodeBlock } from "tripetto-runner-foundation";
import { run, namespace, IAutoscrollRendering } from "tripetto-runner-autoscroll";

@tripetto({
type: "node",
namespace,
identifier: "text-input",
})
class TextInputBlock extends NodeBlock<{
isMultiline?: boolean;
}> implements IAutoscrollRendering {
// Block implementation here
}

run({
display: "page",
definition: /* Supply your form definition here */
});

This will make isMultiline available in the props field of the TextInputBlock class.

tip

Read the Properties guide to learn more about defining properties for your block.

3️⃣ Define block validation

The next step is to implement block validation. In this case, that's very simple: It's already done for you! That's because our text input block only has an optional required flag that specifies if a value is required or not. Since this is controlled using the required field of the String slot, the validation is automatically applied in the background. So we don't have to implement anything for this question type. Required slots need a value before the block validation can pass.

info

Read the Validation guide to learn how to implement custom validation and when you need it.

4️⃣ Define block rendering

The final step for the runner part of the block is actual rendering it. The stock runners use React for this and the IAutoscrollRendering defines a render method we need to implement. Update the highlighted lines in index.tsx:

index.tsx
import { tripetto, NodeBlock } from "tripetto-runner-foundation";
import { run, namespace, IAutoscrollRendering, IAutoscrollRenderProps } from "tripetto-runner-autoscroll";
import * as React from "react";

@tripetto({
type: "node",
namespace,
identifier: "text-input",
})
class TextInputBlock extends NodeBlock<{
isMultiline?: boolean;
}> implements IAutoscrollRendering {
render(props: IAutoscrollRenderProps, done?: () => void): React.ReactNode {
const value = this.valueOf("value");

return (
<>
{props.name}
{this.props.isMultiline ? (
<textarea
onChange={(e) => value?.set(e.target.value)}
placeholder={props.placeholder}
>
{value?.string}
</textarea>
) : (
<input
type="text"
value={value?.string}
onChange={(e) => value?.set(e.target.value)}
placeholder={props.placeholder}
onKeyDown={done && ((e) => {
// When the enter key is pressed, indicate we're done!
if (e.key === "Enter") {
done();
}
})}
/>
)}
</>
);
}
}

run({
display: "page",
definition: /* Supply your form definition here */
});

Run Try on CodeSandbox

The render method is very simple. It should return the HTML code for the block using React. The slot value (that was defined in the builder part of the block) is retrieved using the valueOf method of the TextInputBlock class. Based on the value of the isMultiline property, a textarea or input control is rendered. When the value of the field changes, it updates the slot value of the block. This then automatically triggers a validation cycle.

💯 Runner part done!

That's all we need to do for the runner part of our example text input block.

tip

Read the Stock runners custom blocks guide for more information about developing custom blocks for the stock runners.

⏭️ Up next

Now you can extend this block with a condition block to verify the value of the text input.

Dive deeper into the following topics to master the art of building blocks for Tripetto: