Creating a headless block
Headless blocks can perform actions that don't need a visual counterpart in the runner. A good example of a headless block is the calculator stock block. It can perform complex calculations in a form, and it does that silently in the background. Of course, the outcome of a headless block is useable by other blocks and can often be recalled somewhere else in a form. This tutorial explains how to set up such a headless block. Just like a visual block, 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 does the actual operation of the block in the runner that runs the form.
This tutorial covers both parts, beginning with the builder part. And to keep it practical, we're going to build a simple headless block that retrieves the current day of the week and exposes it to a variable (slot), so it can be recalled somewhere else in the form.
🏗️ Builder part
The builder part of a headless 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 its properties and settings. For this tutorial, we'll start from scratch and prepare a new project for our weekday 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.
▶️ Prepare your project
1️⃣ Create a project
First, create a new folder for the project (for example, weekday-block
). Then open a command-line terminal for that new folder and run the following command to initiate the project:
- npm
- Yarn
- pnpm
npm init
yarn init
pnpm init
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
- Yarn
- pnpm
npm install @tripetto/builder typescript webpack webpack-cli image-webpack-loader url-loader ts-loader concurrently
yarn add @tripetto/builder typescript webpack webpack-cli image-webpack-loader url-loader ts-loader concurrently
pnpm add @tripetto/builder 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.
- tsconfig.json
- webpack.config.js
{
"compileOnSave": false,
"compilerOptions": {
"target": "ES5",
"moduleResolution": "Node",
"newLine": "LF",
"strict": true,
"experimentalDecorators": true
},
"include": ["./src/**/*.ts"]
}
Make sure the experimentalDecorators
feature is enabled. We need support for decorators.
const path = require("path");
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: {
// Make sure to exclude the builder from the bundle!
"@tripetto/builder": "Tripetto",
},
};
4️⃣ Project configuration
Now open the package.json
file that was generated in the first step and add the highlighted lines to it:
{
"name": "weekday-block",
"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": {
"@tripetto/builder": "^5.0.30",
"image-webpack-loader": "^8.1.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.
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
- Yarn
- pnpm
npm test
yarn test
pnpm test
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
- Yarn
- pnpm
npm install webpack-livereload-plugin
yarn add webpack-livereload-plugin
pnpm add webpack-livereload-plugin
Then, open webpack.config.js
and add the highlighted lines:
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: {
// Make sure to exclude the builder from the bundle!
"@tripetto/builder": "Tripetto",
},
plugins: [
new webpackLiveReload({
appendScriptTag: true,
}),
]
};
Now the builder should automatically reload the bundle when the code is changed.
▶️ Block implementation
When everything is prepared properly, we can begin building the actual headless block.
1️⃣ Declare the block
Let's start with the basic code of a headless 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).
- index.ts
- icon.svg
import { tripetto, NodeBlock } from "@tripetto/builder";
import icon from "./icon.svg";
@tripetto({
type: "node",
kind: "headless",
identifier: "weekday",
label: "Retrieve weekday",
icon
})
class WeekdayBlock extends NodeBlock {
// Block implementation here
}
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20" height="20" viewBox="0 0 20 20">
<path d="M18.5 2h-2.5v-0.5c0-0.276-0.224-0.5-0.5-0.5s-0.5 0.224-0.5 0.5v0.5h-10v-0.5c0-0.276-0.224-0.5-0.5-0.5s-0.5 0.224-0.5 0.5v0.5h-2.5c-0.827 0-1.5 0.673-1.5 1.5v14c0 0.827 0.673 1.5 1.5 1.5h17c0.827 0 1.5-0.673 1.5-1.5v-14c0-0.827-0.673-1.5-1.5-1.5zM1.5 3h2.5v1.5c0 0.276 0.224 0.5 0.5 0.5s0.5-0.224 0.5-0.5v-1.5h10v1.5c0 0.276 0.224 0.5 0.5 0.5s0.5-0.224 0.5-0.5v-1.5h2.5c0.276 0 0.5 0.224 0.5 0.5v2.5h-18v-2.5c0-0.276 0.224-0.5 0.5-0.5zM18.5 18h-17c-0.276 0-0.5-0.224-0.5-0.5v-10.5h18v10.5c0 0.276-0.224 0.5-0.5 0.5z" class="tripetto-fill"></path>
<path d="M7.5 10h-1c-0.276 0-0.5-0.224-0.5-0.5s0.224-0.5 0.5-0.5h1c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5z" class="tripetto-fill"></path>
<path d="M10.5 10h-1c-0.276 0-0.5-0.224-0.5-0.5s0.224-0.5 0.5-0.5h1c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5z" class="tripetto-fill"></path>
<path d="M13.5 10h-1c-0.276 0-0.5-0.224-0.5-0.5s0.224-0.5 0.5-0.5h1c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5z" class="tripetto-fill"></path>
<path d="M16.5 10h-1c-0.276 0-0.5-0.224-0.5-0.5s0.224-0.5 0.5-0.5h1c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5z" class="tripetto-fill"></path>
<path d="M4.5 12h-1c-0.276 0-0.5-0.224-0.5-0.5s0.224-0.5 0.5-0.5h1c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5z" class="tripetto-fill"></path>
<path d="M7.5 12h-1c-0.276 0-0.5-0.224-0.5-0.5s0.224-0.5 0.5-0.5h1c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5z" class="tripetto-fill"></path>
<path d="M10.5 12h-1c-0.276 0-0.5-0.224-0.5-0.5s0.224-0.5 0.5-0.5h1c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5z" class="tripetto-fill"></path>
<path d="M13.5 12h-1c-0.276 0-0.5-0.224-0.5-0.5s0.224-0.5 0.5-0.5h1c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5z" class="tripetto-fill"></path>
<path d="M16.5 12h-1c-0.276 0-0.5-0.224-0.5-0.5s0.224-0.5 0.5-0.5h1c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5z" class="tripetto-fill"></path>
<path d="M4.5 14h-1c-0.276 0-0.5-0.224-0.5-0.5s0.224-0.5 0.5-0.5h1c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5z" class="tripetto-fill"></path>
<path d="M7.5 14h-1c-0.276 0-0.5-0.224-0.5-0.5s0.224-0.5 0.5-0.5h1c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5z" class="tripetto-fill"></path>
<path d="M10.5 14h-1c-0.276 0-0.5-0.224-0.5-0.5s0.224-0.5 0.5-0.5h1c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5z" class="tripetto-fill"></path>
<path d="M13.5 14h-1c-0.276 0-0.5-0.224-0.5-0.5s0.224-0.5 0.5-0.5h1c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5z" class="tripetto-fill"></path>
<path d="M16.5 14h-1c-0.276 0-0.5-0.224-0.5-0.5s0.224-0.5 0.5-0.5h1c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5z" class="tripetto-fill"></path>
<path d="M4.5 16h-1c-0.276 0-0.5-0.224-0.5-0.5s0.224-0.5 0.5-0.5h1c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5z" class="tripetto-fill"></path>
<path d="M7.5 16h-1c-0.276 0-0.5-0.224-0.5-0.5s0.224-0.5 0.5-0.5h1c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5z" class="tripetto-fill"></path>
<path d="M10.5 16h-1c-0.276 0-0.5-0.224-0.5-0.5s0.224-0.5 0.5-0.5h1c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5z" class="tripetto-fill"></path>
<path d="M13.5 16h-1c-0.276 0-0.5-0.224-0.5-0.5s0.224-0.5 0.5-0.5h1c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5z" class="tripetto-fill"></path>
<path d="M16.5 16h-1c-0.276 0-0.5-0.224-0.5-0.5s0.224-0.5 0.5-0.5h1c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5z" class="tripetto-fill"></path>
</svg>
This code is all you need to declare a new class with the name WeekdayBlock
. 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 block 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. For headless blocks, you also need to supply the kind
property to the decorator and set it to headless
.
Now we can add the functionality to the block that we need for our weekday block. We need to do three things for that on the builder part:
- Define the data the block will collect;
- Automatically update the node name;
- 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 will hold the current day of the week, 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
:
import { tripetto, slots, NodeBlock, Slots } from "@tripetto/builder";
import icon from "./icon.svg";
@tripetto({
type: "node",
kind: "headless",
identifier: "weekday",
label: "Retrieve weekday",
icon
})
class WeekdayBlock extends NodeBlock {
@slots
onSlots(): void {
this.slots.static({
type: Slots.String,
reference: "weekday",
label: "Current day of the week"
});
}
}
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 WeekdayBlock
class to create a new static slot for the day of the week. 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.
Read the Slots guide to learn more about slots and its possibilities.
3️⃣ Update node name
For most blocks the name of the node is flexible and configurable by the builder user. But for our weekday block, the name can be set to a fixed value Current weekday
. So, how do we do that? We can use the @assigned
decorator to decorate a method that is invoked when the block is attached to a block. That's a perfect moment to update the name of the node to our fixed value. Update the highlighted lines in index.ts
:
import { tripetto, slots, assigned, NodeBlock, Slots } from "@tripetto/builder";
import icon from "./icon.svg";
@tripetto({
type: "node",
kind: "headless",
identifier: "weekday",
label: "Retrieve weekday",
icon
})
class WeekdayBlock extends NodeBlock {
@slots
onSlots(): void {
this.slots.static({
type: Slots.String,
reference: "weekday",
label: "Current day of the week"
});
}
@assigned
onAssign(): void {
this.node.name = "Current weekday";
}
}
4️⃣ Define block editor
The final step is to instruct the builder how to manage the properties and settings for the block. That's simple in this case, as there are no properties or settings that needs to be managed for the weekday block. Instead, we can show a message when the editor panel is opened for this block. Let's implement that.
In this case, we need to add a method to the WeekdayBlock
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 WeekdayBlock
class to define UI controls to manage the block. Update the highlighted lines in index.ts
:
import { tripetto, slots, assigned, editor, NodeBlock, Slots, Forms } from "@tripetto/builder";
import icon from "./icon.svg";
@tripetto({
type: "node",
kind: "headless",
identifier: "weekday",
label: "Retrieve weekday",
icon
})
class WeekdayBlock extends NodeBlock {
@slots
onSlots(): void {
this.slots.static({
type: Slots.String,
reference: "weekday",
label: "Current day of the week"
});
}
@assigned
onAssign(): void {
this.node.name = "Current weekday";
}
@editor
onEdit(): void {
this.editor.form({
controls: [
new Forms.Notification(
"This block will retrieve the current day of the week.",
"success"
)
]
});
}
}
This code uses the form
method to define a custom form with a Notification
control in it.
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! Next, we're going to define the runner part of it.
🏃 Runner part
The runner part of a headless block performs the actual operation. Since headless blocks don't require rendering to a runner UI, they can be reused across runners. So, you don't need a specific implementation for each runner. Let's build the runner part of the weekday example block. The runner part should retrieve the current day of the week and store that value in the weekday slot.
This tutorial assumes you have a runner project up and running. If not, follow the instructions to prepare a project in the visual block tutorial.
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.
▶️ Block implementation
1️⃣ Declare the block
Let's start with the basic code of the runner part of a headless block and then go through that code. Create a file weekday.ts
in your runner project with the following code:
import { tripetto, HeadlessBlock } from "@tripetto/runner";
@tripetto({
type: "headless",
identifier: "weekday",
})
class WeekdayBlock extends HeadlessBlock {
do(): void {
// Perform block operation here
}
}
This code declares a new class with the name WeekdayBlock
. The class is derived from the base class HeadlessBlock
that is imported from the Tripetto Runner library. The @tripetto
decorator is used to register the block to the Tripetto runner.
If you want to use a headless block in one of the stock runners, make sure to make the block available in the blocks namespace of the stock runner. You can do that using the namespace
property of the @tripetto
decorator (when you only target one runner) or by using mountNamespace
.
2️⃣ Define block operation
The next step is to implement the operation for the block. To do so, we need to implement the do
method of the HeadlessBlock
. Update the highlighted lines in the WeekdayBlock
class:
import { tripetto, HeadlessBlock } from "@tripetto/runner";
@tripetto({
type: "headless",
identifier: "weekday",
})
class WeekdayBlock extends HeadlessBlock {
do(): void {
this.valueOf("weekday")?.set(
new Date().toLocaleString("default", { weekday: "long" })
);
}
}
This code retrieves the weekday
slot (that was defined in the builder part of the block) using the valueOf
method and sets the value to the current day of the week.
💯 Runner part done!
That's all we need to do for the runner part of our example weekday block.
⏭️ Up next
Dive deeper into the following topics to master the art of building blocks for Tripetto:
- 🎭 Block icon
- 🏷️ Block properties
- 🎛️ Block editor
- 🗃️ Slots
- 📇 Collections
- ✅ Validation
- ⤵️ Conditions
- 🎬 Post-processing actions
- 🌍 Translations
- 📂 Boilerplate