Skip to main content

Live form preview

For an optimal user experience, it is recommended to show a live preview of the form along with the builder. To do so, you could display a runner in a side-by-side view with the builder (or use a popup or overlay to display the runner). Besides showing the live preview and updating the form when a change is made in the builder, you can also enable interactions between the two. First, it is possible to listen to an event in the builder that supplies information about the element being edited. This can be used to bring that element into view in the runner live preview. Second, it is possible to open editor panels of elements in the builder using the edit function when the user clicks an element in the live preview. These two features can greatly improve the user experience.

Live preview using stock runners

The stock runners (runners built and maintained by the Tripetto team) already contain all the required functions to get an optimal live preview experience. Those runners have an onEdit event that is invoked when a runner requests an edit action in the builder. They also contain a doPreview function that is invoked by the builder when an element is being edited. It brings the element into view in the runner. The code below shows how to set up this two-way interaction properly.

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

// Load the blocks bundle of the runner
import "tripetto-runner-autoscroll/builder";

// Create a new builder instance
Builder.open(undefined, {
// This example assumes there is a HTML element with id `Builder` for the builder
element: document.getElementById("Builder"),

// When the builder is ready, create the runner for the live preview
onReady: async (builder) => {
const runner = await run({
// This example assumes there is a HTML element with id `Runner` for the runner live preview
element: document.getElementById("Runner"),
definition: builder.definition,
view: "preview",

// Handle edit requests from the live preview runner
onEdit: (type, id?) => {
switch (type) {
case "prologue":
builder.edit("prologue");
break;
case "epilogue":
builder.edit("epilogue", id);
break;
default:
if (id) {
builder.edit("node", id);
}
break;
}
},
});

// When the definition is changed, update the preview
builder.hook("OnChange", "synchronous", (event) => {
runner.definition = event.definition;
});

// When something is edited in the builder, let the preview know
builder.hook("OnEdit", "synchronous", (event) => {
runner.doPreview(event.data);
});
},
});

Run Try on CodePen

Preview vs. test mode

The stock runners have two view modes related to the live preview functionality:

  • Preview mode: Shows all elements in the form by skipping all logic;
  • Test mode: Runs the form like a real one without being able to submit data.

The preview mode is ideal for editing since it can show each element in the form as the logic is not applied. In the test mode, the forms run like a real form. Both modes are useful when creating forms. The preview mode allows viewing each element in the form without filling in the form. The test mode is ideal for actual testing the form and all the logic in it.

If you implement a live preview in your application, we suggest adding a toggle to allow the user to switch between preview and test mode.

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

// Load the blocks bundle of the runner
import "tripetto-runner-autoscroll/builder";

// Create a new builder instance
Builder.open(undefined, {
// This example assumes there is a HTML element with id `builder` for the builder
element: document.getElementById("builder"),

// When the builder is ready, create the runner for the live preview
onReady: async (builder) => {
const runner = await run({
// This example assumes there is a HTML element with id `runner` for the runner live preview
element: document.getElementById("runner"),
definition: builder.definition,
view: "preview",

// Handle edit requests from the live preview runner
onEdit: (type, id?) => {
switch (type) {
case "prologue":
builder.edit("prologue");
break;
case "epilogue":
builder.edit("epilogue", id);
break;
default:
if (id) {
builder.edit("node", id);
}
break;
}
}
});

// When the definition is changed, update the preview
builder.hook("OnChange", "synchronous", (event) => {
runner.definition = event.definition;
});

// When something is edited in the builder, let the preview know
builder.hook("OnEdit", "synchronous", (event) => {
runner.doPreview(event.data);
});

// This example assumes there are two toggle buttons
const togglePreview = document.getElementById("toggle-preview");
const toggleTest = document.getElementById("toggle-test");

togglePreview.addEventListener("click", () => {
runner.view = "preview";

togglePreview.classList.add("selected");
toggleTest.classList.remove("selected");
});

toggleTest.addEventListener("click", () => {
runner.view = "test";

toggleTest.classList.add("selected");
togglePreview.classList.remove("selected");
});
}
});

Run Try on CodePen

Preview runner styles

The stock runners support customizing the styles of the form. Things like the font face, text size, and colors can be changed. The builder contains a special editor panel for managing the styles of a runner. To let this work, each stock runner contains a specific file that contains all the available styles for the runner, called the styles contract. The builder is able to use this styles contract and populate an editor panel with it that allows the user to configure the styles. If you combine this with a live preview, the user can change the style settings and immediately see the result in the live preview panel.

Importing the styles contract

The styles contract is a function that can be imported from the stock runner packages. It is located in the /builder/styles folder.

import stylesContract from "tripetto-runner-autoscroll/builder/styles";

Generating the styles editor panel

To generate the styles editor panel the stylesEditor method of a builder instance is used. The styles contract is generated using the stylesContract function imported from the runner package as shown above. This function needs a reference to the pgettext function for translating the text labels in the styles contract.

import { pgettext } from "tripetto";
import stylesContract from "tripetto-runner-autoscroll/builder/styles";

// Open styles editor using an existing builder instance
builder.stylesEditor(stylesContract(pgettext));

Run Try on CodeSandbox

Update styles in live preview

The stylesEditor has a special argument that allows specifying a callback function that is invoked when the styles change. This callback function can update the live preview. The following example adds a button to open the styles editor panel. When styles are changed, the live preview is updated accordingly.

import { Builder, pgettext } from "tripetto";
import { run } from "tripetto-runner-autoscroll";
import stylesContract from "tripetto-runner-autoscroll/builder/styles";

// Load the blocks bundle of the runner
import "tripetto-runner-autoscroll/builder";

// Create a new builder instance
Builder.open(undefined, {
// This example assumes there is a HTML element with id `builder` for the builder
element: document.getElementById("builder"),

// When the builder is ready, create the runner for the live preview
onReady: async (builder) => {
const runner = await run({
// This example assumes there is a HTML element with id `runner` for the runner live preview
element: document.getElementById("runner"),
definition: builder.definition,
view: "preview",

// Handle edit requests from the live preview runner
onEdit: (type, id?) => {
switch (type) {
case "prologue":
builder.edit("prologue");
break;
case "epilogue":
builder.edit("epilogue", id);
break;
default:
if (id) {
builder.edit("node", id);
}
break;
}
}
});

// When the definition is changed, update the preview
builder.hook("OnChange", "synchronous", (event) => {
runner.definition = event.definition;
});

// When something is edited in the builder, let the preview know
builder.hook("OnEdit", "synchronous", (event) => {
runner.doPreview(event.data);
});

// This example assumes there are two toggle buttons
const togglePreview = document.getElementById("toggle-preview");
const toggleTest = document.getElementById("toggle-test");

togglePreview.addEventListener("click", () => {
runner.view = "preview";

togglePreview.classList.add("selected");
toggleTest.classList.remove("selected");
});

toggleTest.addEventListener("click", () => {
runner.view = "test";

toggleTest.classList.add("selected");
togglePreview.classList.remove("selected");
});

// This example assumes there is an edit button for the styles
const editButton = document.getElementById("edit-styles");

editButton.addEventListener("click", () => {
// Open the styles editor
builder.stylesEditor(
stylesContract(pgettext),
runner.styles,
"standard",
(styles) => {
// Styles are changed, update the runner
runner.styles = styles;
}
);
});
}
});

Run Try on CodePen

Preview runner translations

The stock runners support translations for different languages. The builder has a special translation editor panel that allows users to create custom translations for the stock runners. To let this work, each stock runner contains a specific file that contains translation information, called the localization (l10n) contract. The builder is able to use this l10n contract and populate an editor panel with it that allows the user to translate the text labels. If you combine this with a live preview, the user can translate the runner and immediately see the result in the live preview panel.

info

Currently, this panel only allows translating the static text labels of the runner and not the text labels within the form definition.

Importing the l10n contract

The l10n contract is a function that can be imported from the stock runner packages. It is located in the /builder/l10n folder.

import l10nContract from "tripetto-runner-autoscroll/builder/l10n";

Generating the translations editor panel

To generate the translations editor panel the l10nEditor method of a builder instance is used. The l10n contract is generated using the l10nContract function imported from the runner package as shown above.

import l10nContract from "tripetto-runner-autoscroll/builder/l10n";

// Open translations editor using an existing builder instance
builder.l10nEditor(L10nContract());

Run Try on CodeSandbox

Update translations in live preview

The l10nEditor has a special argument that allows specifying a callback function that is invoked when the translations change. This callback function can update the live preview. The following example adds a button to open the l10n editor panel. When translations are changed, the live preview is updated accordingly.

import { Builder } from "tripetto";
import { run } from "tripetto-runner-autoscroll";
import l10nContract from "tripetto-runner-autoscroll/builder/l10n";

// Load the blocks bundle of the runner
import "tripetto-runner-autoscroll/builder";

// Create a new builder instance
Builder.open(undefined, {
// This example assumes there is a HTML element with id `builder` for the builder
element: document.getElementById("builder"),

// When the builder is ready, create the runner for the live preview
onReady: async (builder) => {
const runner = await run({
// This example assumes there is a HTML element with id `runner` for the runner live preview
element: document.getElementById("runner"),
definition: builder.definition,
view: "preview",

// Handle edit requests from the live preview runner
onEdit: (type, id?) => {
switch (type) {
case "prologue":
builder.edit("prologue");
break;
case "epilogue":
builder.edit("epilogue", id);
break;
default:
if (id) {
builder.edit("node", id);
}
break;
}
}
});

// When the definition is changed, update the preview
builder.hook("OnChange", "synchronous", (event) => {
runner.definition = event.definition;
});

// When something is edited in the builder, let the preview know
builder.hook("OnEdit", "synchronous", (event) => {
runner.doPreview(event.data);
});

// This example assumes there are two toggle buttons
const togglePreview = document.getElementById("toggle-preview");
const toggleTest = document.getElementById("toggle-test");

togglePreview.addEventListener("click", () => {
runner.view = "preview";

togglePreview.classList.add("selected");
toggleTest.classList.remove("selected");
});

toggleTest.addEventListener("click", () => {
runner.view = "test";

toggleTest.classList.add("selected");
togglePreview.classList.remove("selected");
});

// This example assumes there is an edit button for the translations
const editButton = document.getElementById("edit-translations");

editButton.addEventListener("click", () => {
// Open the translations editor
builder.l10nEditor(
l10nContract(),
runner.l10n,
(l10n) => {
// Translations are changed, update the runner
runner.l10n = l10n;
}
);
});
}
});

Run Try on CodePen

Preview multiple runners

It is possible to use multiple runners so that the user can preview the form in different runners with a simple switch. To do so, it is necessary to specify different namespaces for each runner. The builder blocks of those runners need to be loaded in those namespaces. When the user switches to another runner, the appropriate namespace is selected using the useNamespace method of the builder instance.

The following example implements a dropdown control with a list of runners. The user can switch to another runner by selecting one in the dropdown. The live preview then changes to the selected runner.

info

In this example the runners and block bundles are loaded using a static import. See the Loading blocks guide if you want to load bundles dynamically (lazy load).

import { Builder, IBuilderChangeEvent, IBuilderEditEvent, mountNamespace, unmountNamespace } from "tripetto";
import { run as runAutoscroll } from "tripetto-runner-autoscroll";
import { run as runChat } from "tripetto-runner-chat";
import { run as runClassic } from "tripetto-runner-classic";

// This demo implements multiple runners than run simultaneously. Each runner
// comes with a bundle with the implemented builder blocks for that runner.
// Here we load those bundles in different namespaces. By activating the
// appropriate namespace during runtime in the builder we can switch between
// builder block bundles. This allows for differences between the blocks used
// in the runners.
mountNamespace("autoscroll");
import "tripetto-runner-autoscroll/builder";
unmountNamespace();

mountNamespace("chat");
import "tripetto-runner-chat/builder";
unmountNamespace();

mountNamespace("classic");
import "tripetto-runner-classic/builder";
unmountNamespace();

// Create a new builder instance
Builder.open(undefined, {
element: document.getElementById("builder"),
onReady: async (builder) => {
let activeRunner = "autoscroll";

// This function is invoked when an item is clicked in the runner. It will open the appropriate item in the builder.
const onEdit = (type: string, id?: string) => {
switch (type) {
case "prologue":
builder.edit("prologue");
break;
case "epilogue":
builder.edit("epilogue", id);
break;
default:
if (id) {
builder.edit("node", id);
}
break;
}
};

const updateRunner = () => {
builder.useNamespace(activeRunner);

document
.getElementById("runner-autoscroll")
?.classList.toggle("visible", activeRunner === "autoscroll");
document
.getElementById("runner-chat")
?.classList.toggle("visible", activeRunner === "chat");
document
.getElementById("runner-classic")
?.classList.toggle("visible", activeRunner === "classic");
};

const autoscrollRunner = await runAutoscroll({
element: document.getElementById("runner-autoscroll"),
definition: builder.definition,
view: "preview",
onReady: () => updateRunner(),
onEdit
});

const chatRunner = await runChat({
element: document.getElementById("runner-chat"),
definition: builder.definition,
view: "preview",
onReady: () => updateRunner(),
onEdit
});

const classicRunner = await runClassic({
element: document.getElementById("runner-classic"),
definition: builder.definition,
view: "preview",
onReady: () => updateRunner(),
onEdit
});

// When the definition is changed, update the preview
builder.hook("OnChange", "framed", (event: IBuilderChangeEvent) => {
switch (activeRunner) {
case "autoscroll":
autoscrollRunner.definition = event.definition;
break;
case "chat":
chatRunner.definition = event.definition;
break;
case "classic":
classicRunner.definition = event.definition;
break;
}
});

// When something is edited in the builder, let the preview know
builder.hook("OnEdit", "synchronous", (event: IBuilderEditEvent) => {
switch (activeRunner) {
case "autoscroll":
autoscrollRunner.doPreview(event.data);
break;
case "chat":
chatRunner.doPreview(event.data);<