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. That allows to bring the element that's being edited into view in the live preview. It also makes it possible to open the properties of an element in the builder by clicking on it 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. The only thing you need to do is feed the builder controller to the runners. The code below shows how to set up this up.
- Plain JS
- React
- Angular
import { Builder } from "@tripetto/builder";
import { run } from "@tripetto/runner-autoscroll";
// 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: (builder) => 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",
// Feed the builder instance to the runner to enable live preview
builder,
}),
});
import { useRef } from "react";
import { TripettoBuilder } from "@tripetto/builder/react";
import { AutoscrollRunner } from "@tripetto/runner-autoscroll";
function App() {
const controllerRef = useRef();
return (
<>
<TripettoBuilder controller={controllerRef} />
<AutoscrollRunner builder={controllerRef} view="preview" />
</>
);
}
import { Component, ViewChild, AfterViewInit } from "@angular/core";
import { TripettoBuilderComponent } from "@tripetto/builder/angular";
import { TripettoAutoscrollComponent } from "@tripetto/runner-autoscroll/angular";
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent implements AfterViewInit {
@ViewChild(TripettoBuilderComponent) builder!: TripettoBuilderComponent;
@ViewChild(TripettoAutoscrollComponent) runner!: TripettoAutoscrollComponent;
ngAfterViewInit() {
// Pass the builder instance to the runner
this.runner.builder = this.builder;
}
}
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.
- Plain JS
- React
- Angular
import { Builder } from "@tripetto/builder";
import { run } from "@tripetto/runner-autoscroll";
// 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",
builder,
});
// 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");
});
}
});
import { useState, useRef } from "react";
import { TripettoBuilder } from "@tripetto/builder/react";
import { AutoscrollRunner } from "@tripetto/runner-autoscroll";
function App() {
const [view, setView] = useState("preview");
const controllerRef = useRef();
return (
<>
<TripettoBuilder controller={controllerRef} />
<AutoscrollRunner view={view} builder={controllerRef} />
<div onClick={() => setView("preview")}>Preview mode</div>
<div onClick={() => setView("test")}>Test mode</div>
</>
);
}
import { Component, ViewChild, AfterViewInit } from "@angular/core";
import { IDefinition } from "@tripetto/builder";
import { TripettoBuilderComponent } from "@tripetto/builder/angular";
import { TripettoAutoscrollComponent } from "@tripetto/runner-autoscroll/angular";
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent implements AfterViewInit {
@ViewChild(TripettoBuilderComponent) builder!: TripettoBuilderComponent;
@ViewChild(TripettoAutoscrollComponent) runner!: TripettoAutoscrollComponent;
get isPreview(): boolean {
return (this.runner?.view || "preview") === "preview";
}
ngAfterViewInit() {
this.ref.builder = this.builder;
}
onPreview() {
this.runner.view = "preview";
}
onTest() {
this.runner.view = "test";
}
}
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.
- Autoscroll runner
- Chat runner
- Classic runner
import stylesContract from "@tripetto/runner-autoscroll/builder/styles";
import stylesContract from "@tripetto/runner-chat/builder/styles";
import stylesContract from "@tripetto/runner-classic/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.
- Autoscroll runner
- Chat runner
- Classic runner
import stylesContract from "@tripetto/runner-autoscroll/builder/styles";
// Open styles editor using an existing builder instance
builder.stylesEditor(stylesContract);
import stylesContract from "@tripetto/runner-chat/builder/styles";
// Open styles editor using an existing builder instance
builder.stylesEditor(stylesContract);
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.
- Plain JS
- React
- Angular
import { Builder } from "@tripetto/builder";
import { run } from "@tripetto/runner-autoscroll";
import stylesContract from "@tripetto/runner-autoscroll/builder/styles";
// 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",
builder,
});
// 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";
});
toggleTest.addEventListener("click", () => {
runner.view = "test";
});
// 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,
() => runner,
"unlicensed"
);
});
}
});
import { useState, useRef } from "react";
import { TripettoBuilder } from "@tripetto/builder/react";
import { AutoscrollRunner } from "@tripetto/runner-autoscroll";
import stylesContract from "@tripetto/runner-autoscroll/builder/styles";
function App() {
const [view, setView] = useState("preview");
const builderControllerRef = useRef();
const runnerControllerRef = useRef();
return (
<>
<TripettoBuilder controller={builderControllerRef} />
<AutoscrollRunner
controller={runnerControllerRef}
view={view}
builder={builderControllerRef}
/>
<div onClick={() => {
// Open the styles editor
builderControllerRef.current.stylesEditor(
stylesContract,
() => runnerControllerRef.current,
"standard"
);
}}
>Edit styles</div>
</>
);
}
import { Component, ViewChild, AfterViewInit } from "@angular/core";
import { IDefinition } from "@tripetto/builder";
import { TripettoBuilderComponent } from "@tripetto/builder/angular";
import { TripettoAutoscrollComponent } from "@tripetto/runner-autoscroll/angular";
import stylesContract from "@tripetto/runner-autoscroll/builder/styles";
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent implements AfterViewInit {
@ViewChild(TripettoBuilderComponent) builder!: TripettoBuilderComponent;
@ViewChild(TripettoAutoscrollComponent) runner!: TripettoAutoscrollComponent;
ngAfterViewInit() {
this.runner.builder = this.builder;
}
onEditStyles() {
// Open the styles editor
this.builder.stylesEditor(
stylesContract,
() => this.runner,
"unlicensed"
);
}
}
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.
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.
- Autoscroll runner
- Chat runner
- Classic runner
import l10nContract from "@tripetto/runner-autoscroll/builder/l10n";
import l10nContract from "@tripetto/runner-chat/builder/l10n";
import l10nContract from "@tripetto/runner-classic/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.
- Autoscroll runner
- Chat runner
- Classic runner
import l10nContract from "@tripetto/runner-autoscroll/builder/l10n";
// Open translations editor using an existing builder instance
builder.l10nEditor(L10nContract);
import l10nContract from "@tripetto/runner-chat/builder/l10n";
// Open translations editor using an existing builder instance
builder.l10nEditor(L10nContract);
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.
- Plain JS
- React
- Angular
import { Builder } from "@tripetto/builder";
import { run } from "@tripetto/runner-autoscroll";
import l10nContract from "@tripetto/runner-autoscroll/builder/l10n";
// 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",
builder
});
// 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";
});
toggleTest.addEventListener("click", () => {
runner.view = "test";
});
// 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
);
});
}
});
import { useState, useRef } from "react";
import { TripettoBuilder } from "@tripetto/builder/react";
import { AutoscrollRunner } from "@tripetto/runner-autoscroll";
import l10nContract from "@tripetto/runner-autoscroll/builder/l10n";
function App() {
const [view, setView] = useState("preview");
const builderControllerRef = useRef();
const runnerControllerRef = useRef();
return (
<>
<TripettoBuilder controller={builderControllerRef} />
<AutoscrollRunner
controller={runnerControllerRef}
view={view}
builder={builderControllerRef}
/>
<div onClick={() => {
// Open the translations editor
builderControllerRef.current.l10nEditor(
l10nContract,
() => runnerControllerRef.current
);
}}
>Edit translations</div>
</>
);
}
import { Component, ViewChild, AfterViewInit } from "@angular/core";
import { IDefinition } from "@tripetto/builder";
import { TripettoBuilderComponent } from "@tripetto/builder/angular";
import { TripettoAutoscrollComponent } from "@tripetto/runner-autoscroll/angular";
import l10nContract from "@tripetto/runner-autoscroll/builder/l10n";
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent implements AfterViewInit {
@ViewChild(TripettoBuilderComponent) builder!: TripettoBuilderComponent;
@ViewChild(TripettoAutoscrollComponent) runner!: TripettoAutoscrollComponent;
ngAfterViewInit() {
this.runner.builder = this.builder;
}
onEditTranslations() {
// Open the trsnslations editor
this.builder.l10nEditor(
l10nContract,
() => this.runner
);
}
}
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.
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).
- Plain JS
- React
- Angular
import { Builder, IBuilderChangeEvent, IBuilderEditEvent, mountNamespace, unmountNamespace } from "@tripetto/builder";
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";
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"),
view: "preview",
builder,
onReady: () => updateRunner()
});
const chatRunner = await runChat({
element: document.getElementById("runner-chat"),
view: "preview",
builder,
onReady: () => updateRunner()
});
const classicRunner = await runClassic({
element: document.getElementById("runner-classic"),
view: "preview",
builder,
onReady: () => updateRunner()
});
document
.getElementById("runner-switch")
?.addEventListener("change", (e) => {
activeRunner = (e.target as HTMLSelectElement)?.value;
updateRunner();
});
const togglePreview = document.getElementById("toggle-preview");
const toggleTest = document.getElementById("toggle-test");
togglePreview?.addEventListener("click", () => {
autoscrollRunner.view = "preview";
chatRunner.view = "preview";
classicRunner.view = "preview";
togglePreview.classList.add("selected");
toggleTest?.classList.remove("selected");
});
toggleTest?.addEventListener("click", () => {
autoscrollRunner.view = "test";
chatRunner.view = "test";
classicRunner.view = "test";
toggleTest.classList.add("selected");
togglePreview?.classList.remove("selected");
});
},
disableSaveButton: true,
disableCloseButton: true,
controls: "left"
});
import { useRef, useState, useEffect } from "react";
import { Builder, mountNamespace, unmountNamespace } from "@tripetto/builder";
import { run as autoscrollRun } from "@tripetto/runner-autoscroll";
import { run as chatRun } from "@tripetto/runner-chat";
import { run as classicRun } 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();
function App() {
const [view, setView] = useState<"preview" | "test">("preview");
const [runner, setRunner] = useState("autoscroll");
const builderRef = useRef<Builder>();
if (builderRef.current) {
builderRef.current.useNamespace(runner);
}
return (
<>
<TripettoBuilder controller={builderRef} />
{runner === "autoscroll" && <AutoscrollRunner view={view} builder={builderRef} />}
{runner === "chat" && <ChatRunner view={view} builder={builderRef} />}
{runner === "classic" && <ClassicRunner view={view} builder={builderRef} />}
<select className="runner-switch" onChange={(e) => setRunner(e.target.value)}>
<option value="autoscroll">Autoscroll</option>
<option value="chat">Chat</option>
<option value="classic">Classic</option>
</select>
<div
className={"toggle-preview" + (view === "preview" ? " selected" : "")}
onClick={() => setView("preview")}
>
Preview mode
</div>
<div
className={"toggle-test" + (view === "test" ? " selected" : "")}
onClick={() => setView("test")}
>
Test mode
</div>
</>
);
}
import { Component, ViewChild, AfterViewInit } from "@angular/core";
import { IDefinition, mountNamespace, unmountNamespace } from "@tripetto/builder";
import { TripettoBuilderComponent } from "@tripetto/builder/angular";
import { TripettoAutoscrollComponent } from "@tripetto/runner-autoscroll/angular";
import { TripettoChatComponent } from "@tripetto/runner-chat/angular";
import { TripettoClassicComponent } from "@tripetto/runner-classic/angular";
// 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();
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.scss"]
})
export class AppComponent implements AfterViewInit {
@ViewChild(TripettoBuilderComponent) builder!: TripettoBuilderComponent;
@ViewChild("autoscroll")
autoscroll!: TripettoAutoscrollComponent;
@ViewChild("chat") chat!: TripettoChatComponent;
@ViewChild("classic") classic!: TripettoClassicComponent;
activeRunner: "autoscroll" | "chat" | "classic" = "autoscroll";
get isPreview(): boolean {
return (this.autoscroll?.view || "preview") === "preview";
}
ngAfterViewInit() {
this.autoscroll.builder = this.chat.builder = this.classic.builder = this.builder;
this.builder.controller.useNamespace(this.activeRunner);
}
onRunnerChange(runner: "autoscroll" | "classic" | "chat") {
this.builder.controller.useNamespace((this.activeRunner = runner));
}
onPreview() {
this.autoscroll.view = this.chat.view = this.classic.view = "preview";
}
onTest() {
this.autoscroll.view = this.chat.view = this.classic.view = "test";
}
}
Listening for builder edit events
If you want to listen for edit events sent by the builder, use the following code. It implements the hook
function to attach a listener that receives an object of type IBuilderEditEvent
when the event occurs.
import { Builder } from "@tripetto/builder";
const builder = new Builder();
builder.hook("OnEdit", "framed", (event) => {
// `event.data` contains information about the element being edited.
// See `IBuilderEditEvent` docs for more information.
});