Perhaps one of the best things about Tripetto is that you decide which form building blocks (e.g. question types) you want to use in the builder and runner. We offer a default set to choose from, but you can also develop your own building blocks.
In one single package a block typically both:
For building blocks we recommend using TypeScript. We supply typings to enable optimal IntelliSense support.
This step-by-step guide for building blocks assumes a good understanding of TypeScript, object-oriented programming and webpack.
This is a good time to highlight again that we built a 2D drawing board because we think that’s the best way to visualize and edit an advanced form or survey; especially if it supports logic and conditional flow to smartly adapt to respondents’ inputs. This generally shortens forms and enhances their responsiveness. So instead of being a WYSIWYG builder, it presents the structural layout of a form’s flow and lets you easily move around building blocks on a self-organizing grid.
This is where blocks also come in. These node blocks and condition blocks essentially define building block behaviors in a form and dictate what properties and feature cards to unlock in the builder for their configuration. A block instructs the builder. And when instructed correctly by properly formatted block interfaces, the builder will know everything it needs to know to handle the pertaining building block on the drawing board and the runner can collect respondent inputs in so-called slots.
The following diagram shows the root structure of a form in the builder. This scheme is actually a pattern that can occur recursively. Each cluster can be a repeat of the shown structure, with varying numbers of branches and clusters per branch of course. You’ll recognize this recursive pattern quite easily when you start using the builder.
You’ll probably notice that the following diagram looks a lot like the diagram we used in the chapter about the runner to explain the core concepts for the structure of Tripetto. That’s because we’re talking about the exact same concepts here. Yet, we’re now taking you a step deeper into how exactly behaviors are coupled with nodes; by injecting blocks.
Before starting your development of blocks you’ll want to familiarize yourself with the following entities:
Nodes
These are the containers for the actual form building blocks (i.e. element types like text input, dropdown, checkbox etc.). A node is basically a placeholder for a block. The node behavior itself is defined in a block.
Clusters
One or more nodes can be placed in a so-called cluster. It is simply a collection of nodes.
Branches
One or more clusters can form a branch. A branch can be conditional. You can define certain conditions for the branch to be taken or skipped.
Conditions
A branch can contain one or more conditions, which are used to direct flow into the branch. They are evaluated when a cluster ends. Only subsequent branches with matching condition(s) are taken by the runner. Just like nodes, the conditions are actually placeholders for condition behaviors. The condition behavior itself is defined in a condition block.
Blocks
So, blocks supply a certain behavior to nodes and conditions that are used in the builder to create smart forms. As mentioned before blocks come in two flavours:
Slots
All data collected through the runner needs to be stored somewhere. In Tripetto we use Slots
to do this. Slots are also defined by blocks. For example, if you build a text-input block, you’re probably going to retrieve a text value from an input control in your runner implementation. In that case your block should create a slot to store this text value. There are different types of slots, such as a String
, Boolean
or Number
.
We’ve created a boilerplate to help you start building blocks. It contains the recommended packages, boilerplate code and tasks to get things up and running smoothly. We suggest using the boilerplate as a starting point when you are going to develop a block. To do so, start by downloading and extracting the boilerplate from the following URL to any local folder you like:
https://gitlab.com/tripetto/blocks/boilerplate/repository/master/archive.zip
Next, open your terminal/command prompt and go to your newly created folder. Execute the following command (make sure you have npm and Node.js installed) to install all the required packages:
$ npm install
To start developing and testing your block run the following command:
$ npm start
This will start the builder with a (initial empty) form (stored in ./test/example.json
). The builder will restart automatically with every code change.
If you want to start from scratch or develop blocks in an existing project, you can install the builder package as a dependency using the following command:
$ npm install tripetto --save-dev
It contains the builder application as well as the TypeScript declaration files (typings) necessary for block development. The typings should be discovered automatically by your IDE when importing symbols from the tripetto
module.
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.
Blocks for Tripetto are actually packages. A package is a directory that is described by a package.json
and so is a block. The most important part of the package configuration is the entry point of the block. Normally defined by the main
-field. The entry point is used when the Tripetto builder wants to load your block. If you want you can include multiple blocks in a single package. But we’ll assume for now that you create a package for each block.
The minimal package file for a block should look like this:
{
"name": "your-block",
"version": "1.0.0",
"main": "index.js"
}
This example assumes the entry point of your block implementation is at index.js
.
When you have a block with a valid package.json
, you can publish it to any registry you like. To use your block in the builder, read the instructions on how to use them in the cli tool or in the library.
You’ll want an easy and quick way to test your block while you are building your block. This is achieved by adding a tripetto
-field with the following content to the package.json
of your block:
{
"name": "your-block",
"version": "1.0.0",
"main": "index.js",
"tripetto": {
"blocks": [
"."
]
}
}
With this the builder will load the block when you start the builder from the block folder. In the boilerplate we combined this feature with the webpack live reload plugin. This automatically restarts the builder with each change in the block code.
An example package.json
with corresponding webpack configuration is displayed to the right (or a bit further down, if you’re reading this on a device with limited screen width) to get you up and running fast.
Instead of supplying the entry point in the main
-field you may also use the entry
field of the tripetto
section to do this. For example:
{
"name": "your-block",
"version": "1.0.0",
"tripetto": {
"entry": "index.js",
"blocks": [
"."
]
}
}
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.
Node blocks are used to create node building blocks for the builder. The minimal implementation should look like this:
import {
NodeBlock, tripetto
} from "tripetto";
import * as ICON from "./icon.svg";
@tripetto({
type: "node",
identifier: "example-block",
icon: ICON,
label: "Example node block"
})
export class Example extends NodeBlock {}
So, let’s walk through the code. We define our node block by implementing the NodeBlock
abstract class, prefixed with the @tripetto
decorator to supply (meta) information about the block.
Now that you have a working simple node block, let’s add more functionality to it by:
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.
Condition blocks are used to create condition building blocks for the builder. The minimal implementation should look like this:
import {
ConditionBlock, _, tripetto
} from "tripetto";
import * as ICON from "./condition.svg";
@tripetto({
type: "condition",
identifier: "example-condition",
context: "*",
icon: ICON,
label: () => _("Example condition block")
})
export class Example extends ConditionBlock {}
Let’s have a closer look at this. We define our condition block by implementing the ConditionBlock
abstract class, prefixed with the @tripetto
decorator to supply (meta) information about the block.
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.
If you have read the runner documentation, you probably already noticed that Tripetto requires you to implement each block in two domains:
There is no technical overlap between the two implementation domains, except for the block’s properties. These properties describe the behavior of your block (they are set with the builder and consumed by the runner) and act as the block contract between the implementation domains.
To avoid duplicate interface declarations or mismatches between the properties, we suggest creating a single interface inside your builder block implementation. This interface then acts as the single point of truth and should be exposed by your builder block package, so you can use it inside each runner implementation. This way the block properties are declared in a single place, making it easier to maintain and more consistent.
The following example shows how to achieve this in the builder domain. First off we define the interface in a separate type declaration file, for example interface.d.ts
.
export interface IExampleProperties {
...
// Define a property
color: string;
...
}
Next, implement a block (in this example index.ts
).
import {
NodeBlock, tripetto
} from "tripetto";
import * as ICON from "./icon.svg";
@tripetto({
type: "node",
identifier: "example-block",
icon: ICON,
label: "Example block"
})
export class Example extends NodeBlock {
...
// Make the property part of the form definition
@definition color = "red";
...
}
And this is all you need to do in the builder domain. Make sure to reference your type definition inside your package.json
as follows.
{
"name": "example-block",
"version": "1.0.0",
"main": "index.js",
"types": "interface.d.ts"
}
Now you can use the interface that is declared inside the builder block package in your runner implementation. Also make sure to add the block package to your runner. Then you can import the interface type and feed it into your runner block.
import { IExampleProperties } from "example-block";
@Tripetto.block({
type: "node",
identifier: "example-block"
})
export class ExampleBlock extends NodeBlock<IExampleProperties> {
...
doSomething(): void {
// Use the property
console.log(this.props.color); // Outputs `red`
}
...
}
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.
Let’s talk about slots now! Because they are a fundamental part of Tripetto. Slots are used for the actual storage of all data retrieved from respondents through the runner. A slot contains all the settings related to the data retrieval; whether data input for it is required, the different data formatting options and much more.
Blocks each hold their own definition of any number of slots and their characteristics in the block implementation in the builder. So if, for example, you create a block for retrieving a string value with a text input control, you would define a single slot of a type String
.
All slots combined make up the complete slot collection of a form. And this slot collection ultimately contains all collected data. Slots are automatically stored inside the form definition. In the runner domain the slot collection is available to store collected values.
The following slot types are available:
Boolean
: Stores boolean values;Number
: Stores numeric values;String
: Stores string values;Date
: Stores dates;Text
: Stores string values with optional transformations and formatting;Numeric
: Stores numeric values with formatting like number of decimals, prefix, suffix, etc.There are four kind of slots available:
static
: Indicates a static slot;dynamic
: Indicates a slot with a dynamic origin (for example when you want to generate a slot for a editable list of values);feature
: Indicates a slot for a certain feature (for example a slot for an optional comment);meta
: Indicates a slot which contains meta data.Slots should be created by your block in a dedicated method decorated wih the @slots
decorator. Inside your node block slots are available through a collection named this.slots
. You can select, create and delete slots. For example:
import { NodeBlock, Slots, tripetto, slots } from "tripetto";
@tripetto({
type: "node",
identifier: "TextInput",
label: "Text input"
})
export class TextInput extends NodeBlock {
...
@slots
defineSlots() {
// Create a static slot to store a string with name `value`
this.slots.static({
type: Slots.String,
reference: "value",
label: "This slot contains a value"
});
}
...
}
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.
Feature cards are used inside the builder to manage the properties of blocks. They are designed specifically to just show the settings needed in average scenarios and add only the desired additional settings options dynamically on the go. The idea behind this approach is that users only select the block features in the builder they actually want to configure and prevent the typical clutter. Hence the name feature cards.
To speed up block development for you, we also supply a list of common features. Those features implement common configuration options, such as the name of the block, description, explanation, etc.
The optional features are listed to the left side of the screen. The configuration options for activated features are presented inside so-called Cards
, which appear to the right of the feature list
Features are implemented inside a dedicated method of your block decorated with the @editor
decorator. Inside your node block you can use the collection this.builder
to supply features. Take a look at the following example. It shows you how to add an optional feature with an empty form card.
...
@editor
defineEditor() {
this.editor.option({
label: "Example option",
form: {
controls: [
...
]
}
});
}
...
This will show the feature in the feature list. When a feature is selected, the appropriate form card will be displayed. Next step is to add actual controls to the form card.
Tripetto contains a collection of form controls that can be used to build form cards for the feature cards in the builder. The table below shows the available controls. These controls can be used in the controls
array of the form card.
Form control | Description |
---|---|
Forms.Button | Button form control. |
Forms.Checkbox | Checkbox form control. |
Forms.Date | Date/time form control. |
Forms.Dropdown | Dropdown form control. |
Forms.Email | Email form control. |
Forms.HTML | HTML form control. |
Forms.Notification | Notification form control. |
Forms.Numeric | Numeric form control. |
Forms.Radiobutton | Radiobutton form control. |
Forms.Spacer | Spacer form control. |
Forms.Static | Static form control. |
Forms.Text | Text form control. |
Forms.Upload | File upload control. |
Forms.Group | Form group. |
The following example shows how to use the controls.
this.editor.option({
label: "Example option",
form: {
title: "Example",
controls: [
new Forms.Text("single").Label("This is a text input"),
new Forms.Checkbox("This is a checkbox")
]
}
});
If you dive deeper into the controls, you will see each control has events you can bind to. One of these events is invoked when the data of the control changes. But there is an easier way to retrieve data from controls by using the bind option of a control. If a control has a bind
method it supports data two-way data binding. The bind method takes 3 parameters:
Let’s extend our example with a binding.
const example = {
text: "",
checkbox: false
};
this.editor.option({
label: "Example option",
form: {
title: "Example",
controls: [
new Forms.Text(
"single",
Forms.Text.bind(example, "text", ""))
.Label("This is a text input"),
new Forms.Checkbox(
"This is a checkbox",
Forms.Checkbox.bind(example, "checkbox", false))
]
}
});
Now the supplied properties text
and checkbox
will be automatically updated when the data of the control changes.
Common features are out-of-the-box available sets of form controls to manage often used properties. The following common features are available on the this.builder
instance:
Feature | Explanation |
---|---|
groups | Contains common group labels. |
name | Manages the name of a node. |
description | Manages the description of a node. |
placeholder | Manages the placeholder of a node. |
explanation | Manages the explanation of a node. |
visibility | Manages the visibility of a node. |
required | Manages the slot requirements. |
alias | Manages the slot alias. |
transformations | Manages the transformation for a TextSlot . |
collection | Manages a collection. |
Common features can be added directly to your @editor
method.
...
@editor
defineEditor() {
this.editor.name();
this.editor.description();
this.editor.explanation();
this.editor.groups.options();
this.editor.visibility();
}
...
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.
Let’s say you’re building some kind of dropdown block. Dropdowns typically hold multiple options to choose from. And so you need a way to add and edit those options in the builder. We’ve got you covered! You need collections and they are quite easy to use. First of all you need to define the type of items for your collection.
Have a look at the following example where we define the type DropdownOption
for our dropdown example.
import { Collection, definition, builder, name } from "tripetto";
import { Dropdown } from "./dropdown";
export class DropdownOption extends Collection.Item<Dropdown> {
@definition
@name
name = "";
@editor
defineEditor(): void {
}
}
As you can see a collection item should extend the abstract class Collection.Item
.
Next step is to add the actual collection to your block. To do so you need to add a member to your block class.
import { Collection, NodeBlock, definition } from "tripetto";
import { DropdownOption } from "./option";
export class Dropdown extends NodeBlock {
@definition
options = Collection.of(DropdownOption, this as Dropdown);
}
When you prefix the collection member with the @definition
decorator, the collection will be automatically stored in the form definition!
The final step is to show the collection builder in the feature card of your block. To do so, you should add a feature for the collection to your @editor
method. Use the common collection
card (a collection builder). It supports adding, editing and deleting items.
@editor
defineEditor(): void {
this.editor.collection({
collection: this.options,
title: "Dropdown options"
});
}
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.
When you implement a node block, it is possible to supply condition templates to the builder. These templates can be used to allow the creation of actual conditions for your block in the builder with one click. The templates will be shown when the user wants to add a branch to a certain cluster. To make this work you have to add a dedicated method decorated with the @conditions
decorator to your block class.
...
@conditions
defineConditions(): void {
this.conditions.template({
condition: ExampleCondition,
label: "Example condition"
});
}
...
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.
We use the gettext convention for the localization of Tripetto and its blocks. You should enclose all your labels/texts that need translation using one of the following functions.
The _
function is actually an alias for the gettext
function. We use it because it is shorter and more distinctive. Supply your (to be translated) string as the first argument. Optional arguments can be referenced in your string using the percent sign followed by the argument index %n
. The first argument is referenced with %1
.
str
...arguments
%n
where n
is the argument number (the first argument is %1
).Example:
const label = _("Lorem %1", "ipsum");
console.log(label); // Outputs `Lorem ipsum`
Translates a plural string (short version for ngettext
).
single
plural
count
...arguments
%n
. The count value count
is automatically included as the first argument (%1
).Example:
const label = _n("1 car", "%1 cars", 2);
console.log(label); // Outputs `2 cars`
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.
If you have followed the localization guidelines from the previous chapter, your block is ready to be translated. Now you can follow these steps to get you going (we assume you’ve started your block implementation using the boilerplate):
./translations/sources
file and add the source files that need to be translated (each file on a separate line);npm run pot
(this command uses the xgettext
tool, make sure it is installed!);./translations/template.pot
is generated. It contains the strings to be translated. Now use this template to create translations. You can use a tool for this (like Poedit) or send the file to a translation service. Each translation should be stored in a PO
-file. The name of the PO
-file should be the locale of that language, for example nl.PO
;PO
-files to the ./translations
folder of your block;PO
-files to JSON
-files by executing the command: npm run make:po2json
;JSON
files in the ./translations
folder of your distributable block package.We hope other enthusiasts will also start to develop blocks for Tripetto in the open source domain. We have a special repository where we collect a list of community driven blocks and runners.
If you have created a block 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.