Blocks   Edit

Taking hardcore charge

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 editor and collector. 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:

  • Holds the properties of a particular building block (e.g. dropdown, checkbox, etc.);
  • And facilitates the management of those properties through the editor.

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.

Concepts   Edit

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 editor, 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 editor for their configuration. A block instructs the editor. And when instructed correctly by properly formatted block interfaces, the editor will know everything it needs to know to handle the pertaining building block on the drawing board and the collector can collect respondent inputs in so-called slots.

Overview

The following diagram shows the root structure of a form in the editor. 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 editor.

You’ll probably notice that the following diagram looks a lot like the diagram we used in the chapter about the collector 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.

Editor structure

Entities

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 collector. 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 editor to build smart forms. As mentioned before blocks come in two flavours:

  • Node blocks: Provide building blocks for nodes (for example text input, dropdown, checkbox etc.);
  • Condition blocks: Provide building blocks for conditions (for example evaluate a certain given answer).

Slots

All data collected through the collector 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 collector 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.

Boilerplate   Edit

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 editor with a (initial empty) form (stored in ./test/example.json). The editor will restart automatically with every code change.

Start with a clean installation

If you want to start from scratch or develop blocks in an existing project, you can install the editor package as a dependency using the following command:

$ npm install tripetto --save-dev

It contains the editor 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.

Implementing blocks   Edit

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

Publishing and consuming your block

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 editor, read the instructions on how to use them in the cli tool or in the library.

Testing during development

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 editor will load the block when you start the editor from the block folder. In the boilerplate we combined this feature with the webpack live reload plugin. This automatically restarts the editor 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.

Alternative method for the entry point

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.

{
  "name": "your-block",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "webpack -d && concurrently -n \"tripetto,webpack\" -c \"blue.bold,green\" -k -s \"first\" \"tripetto ./test/example.json\" \"webpack -d --watch\""
  },
  "devDependencies": {
    "tripetto": "*",
    "concurrently": "*",
    "ts-loader": "*",
    "typescript": "*",
    "webpack": "*",
    "webpack-livereload-plugin": "*"
  },
  "tripetto": {
    "blocks": [
      "."
    ]
  }
}
const webpack = require("webpack");
const webpackLiveReload = require("webpack-livereload-plugin");

module.exports = {
  entry: "./index.ts",
  output: {
    filename: "./index.js"
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: "ts-loader"
      }
    ]
  },
  resolve: {
    extensions: [".ts", ".js"]
  },
  externals: {
    "tripetto": "Tripetto"
  },
  plugins: [
    new webpackLiveReload({
      appendScriptTag: true
    })
  ]
};

Implementing a node block   Edit

Node blocks are used to create node building blocks for the editor. 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.

Next steps

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.

import { NodeBlock, Slots, _, editor, slots, tripetto } from "tripetto";
import * as ICON from "./text.svg";

@tripetto({
  type: "node",
  identifier: "text",
  icon: ICON,
  label: () => _("Text (single line)")
})
export class Text extends NodeBlock {
  public value: Slots.TextSlot;

  @slots
  defineSlot(): void {
    this.value = this.slots.static({
      type: Slots.TextSlot,
      reference: "value",
      label: _("Text value")
   });
  }

  @editor
  defineEditor(): void {
    this.editor.name();
    this.editor.description();
    this.editor.placeholder();
    this.editor.explanation();

    this.editor.groups.options();
    this.editor.required(this.value);
    this.editor.visibility();
    this.editor.alias(this.value);
  }
}

Implementing a condition block   Edit

Condition blocks are used to create condition building blocks for the editor. 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.

Working with interfaces   Edit

If you have read the collector documentation, you probably already noticed that Tripetto requires you to implement each block in two domains:

  • Editor domain: The part of the block that will allow the use of it for smart form building in the editor;
  • Collector domain: The part of the block that renders it to a visual component inside an actual form in a website or application. If you have different collectors with different visual styles, you probably need multiple implementations for each block.

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 editor and consumed by the collector) 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 editor block implementation. This interface then acts as the single point of truth and should be exposed by your editor block package, so you can use it inside each collector implementation. This way the block properties are declared in a single place, making it easier to maintain and more consistent.

Blocks diagram

Editor domain

The following example shows how to achieve this in the editor 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 editor 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"
}

Collector domain

Now you can use the interface that is declared inside the editor block package in your collector implementation. Also make sure to add the block package to your collector. Then you can import the interface type and feed it into your collector 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.

Slots   Edit

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 collector. 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 editor. 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 collector domain the slot collection is available to store collected values.

Slots diagram

Slot types

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.

Slot kinds

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.

Creating slots

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   Edit

Feature cards are used inside the editor 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 editor 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

Creating features and cards

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

Building feature cards

Tripetto contains a collection of form controls that can be used to build form cards for the feature cards in the editor. 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.

Example

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")
    ]
  }
});

Automatically retrieving data from controls

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:

  • A reference to the object that holds the property the control is going to set;
  • The name of the property;
  • The default value that’s applied as long as the control has no value.

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

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

Example

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.

Collections   Edit

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

Screen recording - Collections

Have a look at the following example where we define the type DropdownOption for our dropdown example.

import { Collection, definition, editor, 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.

Implementing the collection

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!

Showing the collection in the feature card

The final step is to show the collection editor 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 editor). 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.

Condition templates   Edit

When you implement a node block, it is possible to supply condition templates to the editor. These templates can be used to allow the creation of actual conditions for your block in the editor 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.

Localization   Edit

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.

_(str, …arguments): string

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.

Arguments

str
Specifies the string that needs to be translated.
...arguments
Optional arguments that can be referenced using %n where n is the argument number (the first argument is %1).

Example:

const label = _("Lorem %1", "ipsum");

console.log(label); // Outputs `Lorem ipsum`
_n(single, plural, count, …arguments): string

Translates a plural string (short version for ngettext).

Arguments

single
Specifies the string containing the single message that needs to be translated.
plural
Specifies the string containing the plural message that needs to be translated.
count
Specifies the count for the plural.
...arguments
Optional string arguments that can be referenced in the translation string using the percent sign followed by the argument index %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.

Translating your block   Edit

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):

  1. First, update the ./translations/sources file and add the source files that need to be translated (each file on a separate line);
  2. Next, generate the POT file by executing the following command: npm run pot (this command uses the xgettext tool, make sure it is installed!);
  3. Verify the file ./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;
  4. Add the PO-files to the ./translations folder of your block;
  5. Convert the PO-files to JSON-files by executing the command: npm run make:po2json;
  6. Make sure to include the generated JSON files in the ./translations folder of your distributable block package.

Community   Edit

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

Add your own block to the list

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.