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.
- Plain JS
- React
- Angular
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);
});
},
});
import React, { useEffect, useRef } from "react";
import { Builder, IDefinition } from "tripetto";
import { run } from "tripetto-runner-autoscroll";
// Load the blocks bundle of the runner
import "tripetto-runner-autoscroll/builder";
function TripettoBuilder(props: {
definition?: IDefinition;
onSave?: (definition: IDefinition) => void;
}) {
const builderElementRef = useRef<HTMLDivElement>(null);
const builderRef = useRef<Builder>();
const runnerElementRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// When there is no builder instance active, create one!
if (!builderRef.current) {
builderRef.current = Builder.open(props.definition, {
element: elementRef.current,
// When the definition is saved, emit an event.
onSave: props.onSave,
// When the builder is ready, create the runner for the live preview runner
onReady: async (builder) => {
const runner = await run({
element: runnerElementRef.current,
definition: builder.definition,
view: "preview",
// Handle edit requests from the live preview
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);
});
}
});
}
// No need for cleanup, the builder instance will be destroyed automagically when the host element is removed from the DOM.
}, []);
return (
<>
<div ref={builderElementRef} className="builder"></div>
<div ref={runnerElementRef} className="runner"></div>
</>
)
}
Though the stock runner packages contain a React component for the runner, it is not used in the example above. Instead we use the run
function of the runner. This function makes it easier to implement the live preview, especially if you want to enable advanced live preview features, like runner styles and runner translations.
import { Component, Input, Output, ElementRef, NgZone, EventEmitter, OnInit, OnDestroy, ChangeDetectionStrategy } from "@angular/core";
import { Builder, IBuilderChangeEvent, IBuilderEditEvent, IDefinition } from "tripetto";
import { run } from "tripetto-runner-autoscroll";
// Load the blocks bundle of the runner
import "tripetto-runner-autoscroll/builder";
@Component({
selector: "tripetto-builder",
template: "",
styleUrls: ["./builder.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BuilderComponent implements OnInit, OnDestroy {
private builder?: Builder;
private initialDefinition?: IDefinition;
/** Specifies the form definition. */
@Input() set definition(definition: IDefinition | undefined) {
if (this.builder) {
this.zone.runOutsideAngular(() => {
this.builder.definition = definition;
});
return;
}
this.initialDefinition = definition;
}
/** Retrieves the form definition. */
get definition(): IDefinition | undefined {
return (this.builder && this.builder.definition) || this.initialDefinition;
}
/**
* Invoked when the form definition is saved.
* @event
*/
@Output() saved = new EventEmitter<IDefinition>();
constructor(private element: ElementRef, private zone: NgZone) {}
ngOnInit() {
// Leave the builder outside of Angular to avoid unnecessary and costly change detection.
this.zone.runOutsideAngular(() => {
this.builder = Builder.open(this.definition, {
// This example assumes component HTML with 2 child elements, the first element for the builder
element: this.element.nativeElement.firstChild,
onReady: async (builder) => {
const runner = await run({
// The second element in the component is for the runner
element: this.element.nativeElement.lastChild,
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: IBuilderChangeEvent) => {
runner.definition = event.definition;
}
);
// When something is edited in the builder, let the preview know
builder.hook("OnEdit", "synchronous", (event: IBuilderEditEvent) => {
runner.doPreview(event.data);
});
builder.hook("OnClose", "synchronous", () => {
runner.destroy();
});
},
onSave: (definition) => {
this.saved.emit(definition);
}
});
});
}
ngOnDestroy() {
this.builder.destroy();
this.builder = undefined;
}
}
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";
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");
});
}
});
import React, { useEffect, useRef } from "react";
import { Builder, IDefinition } from "tripetto";
import { run } from "tripetto-runner-autoscroll";
// Load the blocks bundle of the runner
import "tripetto-runner-autoscroll/builder";
function TripettoBuilder(props: {
definition?: IDefinition;
onSave?: (definition: IDefinition) => void;
}) {
const builderElementRef = useRef<HTMLDivElement>(null);
const builderRef = useRef<Builder>();
const runnerElementRef = useRef<HTMLDivElement>(null);
const runnerRef = React.useRef();
const [view, setView] = React.useState("preview");
if (runnerRef.current) {
runnerRef.current.view = view;
}
useEffect(() => {
// When there is no builder instance active, create one!
if (!builderRef.current) {
builderRef.current = Builder.open(props.definition, {
element: elementRef.current,
// When the definition is saved, emit an event.
onSave: props.onSave,
// When the builder is ready, create the runner for the live preview runner
onReady: async (builder) => {
runnerRef.current = await run({
element: runnerElementRef.current,
definition: builder.definition,
view,
// Handle edit requests from the live preview
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) => {
runnerRef.current.definition = event.definition;
});
// When something is edited in the builder, let the preview know
builder.hook("OnEdit", "synchronous", (event) => {
runnerRef.current.doPreview(event.data);
});
}
});
}
// No need for cleanup. The builder instance destroys itself automagically when the host element is removed from the DOM.
}, []);
return (
<>
<div ref={builderElementRef} className="builder"></div>
<div ref={runnerElementRef} className="runner"></div>
<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, Input, Output, ElementRef, NgZone, EventEmitter, OnInit, OnDestroy, ChangeDetectionStrategy } from "@angular/core";
import { Builder, IBuilderChangeEvent, IBuilderEditEvent, IDefinition } from "tripetto";
import { IAutoscrollRunner, run } from "tripetto-runner-autoscroll";
// Load the blocks bundle of the runner
import "tripetto-runner-autoscroll/builder";
@Component({
selector: "tripetto-builder",
template: "",
styleUrls: ["./builder.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BuilderComponent implements OnInit, OnDestroy {
private builder?: Builder;
private runner?: IAutoscrollRunner;
private initialDefinition?: IDefinition;
/** Specifies the form definition. */
@Input() set definition(definition: IDefinition | undefined) {
if (this.builder) {
this.zone.runOutsideAngular(() => {
this.builder.definition = definition;
});
return;
}
this.initialDefinition = definition;
}
/** Retrieves the form definition. */
get definition(): IDefinition | undefined {
return (this.builder && this.builder.definition) || this.initialDefinition;
}
get isPreview(): boolean {
return (this.runner?.view || "preview") === "preview";
}
set isPreview(preview) {
if (this.runner) {
this.runner.view = preview ? "preview" : "test";
}
}
/**
* Invoked when the form definition is saved.
* @event
*/
@Output() saved = new EventEmitter<IDefinition>();
constructor(private element: ElementRef, private zone: NgZone) {}
ngOnInit() {
// Leave the builder outside of Angular to avoid unnecessary and costly change detection.
this.zone.runOutsideAngular(() => {
this.builder = Builder.open(this.definition, {
element: this.element.nativeElement.children[0],
onReady: async (builder) => {
this.runner = await run({
element: this.element.nativeElement.children[1],
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: IBuilderChangeEvent) => {
this.runner.definition = event.definition;
}
);
// When something is edited in the builder, let the preview know
builder.hook("OnEdit", "synchronous", (event: IBuilderEditEvent) => {
this.runner.doPreview(event.data);
});
builder.hook("OnClose", "synchronous", () => {
this.runner.destroy();
});
},
onSave: (definition) => {
this.saved.emit(definition);
}
});
});
}
ngOnDestroy() {
this.builder.destroy();
this.builder = undefined;
}
onPreview() {
this.isPreview = true;
}
onTest() {
this.isPreview = false;
}
}
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. This function needs a reference to the pgettext
function for translating the text labels in the styles contract.
- Autoscroll runner
- Chat runner
- Classic runner
import { pgettext } from "tripetto";
import stylesContract from "tripetto-runner-autoscroll/builder/styles";
// Open styles editor using an existing builder instance
builder.stylesEditor(stylesContract(pgettext));
import { pgettext } from "tripetto";
import stylesContract from "tripetto-runner-chat/builder/styles";
// Open styles editor using an existing builder instance
builder.stylesEditor(stylesContract(pgettext));
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, 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;
}
);
});
}
});
import React, { useEffect, useRef } from "react";
import { Builder, IDefinition, 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";
function TripettoBuilder(props: {
definition?: IDefinition;
onSave?: (definition: IDefinition) => void;
}) {
const builderElementRef = useRef<HTMLDivElement>(null);
const builderRef = useRef<Builder>();
const runnerElementRef = useRef<HTMLDivElement>(null);
const runnerRef = React.useRef();
const [view, setView] = React.useState("preview");
if (runnerRef.current) {
runnerRef.current.view = view;
}
useEffect(() => {
// When there is no builder instance active, create one!
if (!builderRef.current) {
builderRef.current = Builder.open(props.definition, {
element: elementRef.current,
// When the definition is saved, emit an event.
onSave: props.onSave,
// When the builder is ready, create the runner for the live preview runner
onReady: async (builder) => {
runnerRef.current = await run({
element: runnerElementRef.current,
definition: builder.definition,
view,
// Handle edit requests from the live preview
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) => {
runnerRef.current.definition = event.definition;
});
// When something is edited in the builder, let the preview know
builder.hook("OnEdit", "synchronous", (event) => {
runnerRef.current.doPreview(event.data);
});
}
});
}
// No need for cleanup, the builder instance will be destroyed automagically when the host element is removed from the DOM.
}, []);
return (
<>
<div ref={builderElementRef} className="builder"></div>
<div ref={runnerElementRef} className="runner"></div>
<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>
<div
className="edit-styles"
onClick={() => {
// Open the styles editor
builderRef.current.stylesEditor(
stylesContract(pgettext),
runnerRef.current.styles,
"standard",
(styles) => {
// Styles are changed, update the runner
runnerRef.current.styles = styles;
}
);
}}
>
Edit styles
</div>
</>
)
}
import { Component, Input, Output, ElementRef, NgZone, EventEmitter, OnInit, OnDestroy, ChangeDetectionStrategy } from "@angular/core";
import { Builder, IBuilderChangeEvent, IBuilderEditEvent, IDefinition } from "tripetto";
import { IAutoscrollRunner, 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";
@Component({
selector: "tripetto-builder",
template: "",
styleUrls: ["./builder.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BuilderComponent implements OnInit, OnDestroy {
private builder?: Builder;
private runner?: IAutoscrollRunner;
private initialDefinition?: IDefinition;
/** Specifies the form definition. */
@Input() set definition(definition: IDefinition | undefined) {
if (this.builder) {
this.zone.runOutsideAngular(() => {
this.builder.definition = definition;
});
return;
}
this.initialDefinition = definition;
}
/** Retrieves the form definition. */
get definition(): IDefinition | undefined {
return (this.builder && this.builder.definition) || this.initialDefinition;
}
get isPreview(): boolean {
return (this.runner?.view || "preview") === "preview";
}
set isPreview(preview) {
if (this.runner) {
this.runner.view = preview ? "preview" : "test";
}
}
/**
* Invoked when the form definition is saved.
* @event
*/
@Output() saved = new EventEmitter<IDefinition>();
constructor(private element: ElementRef, private zone: NgZone) {}
ngOnInit() {
// Leave the builder outside of Angular to avoid unnecessary and costly change detection.
this.zone.runOutsideAngular(() => {
this.builder = Builder.open(this.definition, {
element: this.element.nativeElement.children[0],
onReady: async (builder) => {
this.runner = await run({
element: this.element.nativeElement.children[1],
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: IBuilderChangeEvent) => {
this.runner.definition = event.definition;
}
);
// When something is edited in the builder, let the preview know
builder.hook("OnEdit", "synchronous", (event: IBuilderEditEvent) => {
this.runner.doPreview(event.data);
});
builder.hook("OnClose", "synchronous", () => {
this.runner.destroy();
});
},
onSave: (definition) => {
this.saved.emit(definition);
}
});
});
}
ngOnDestroy() {
this.builder.destroy();
this.builder = undefined;
}
onPreview() {
this.isPreview = true;
}
onTest() {
this.isPreview = false;
}
onEditStyles() {
if (this.builder && this.runner) {
// Open the styles editor
this.builder.stylesEditor(stylesContract(pgettext), this.runner.styles, "standard", (styles) => {
// Styles are changed, update the runner
this.runner.styles = styles;
})
}
}
}
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";
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;
}
);
});
}
});
import React, { useEffect, useRef } from "react";
import { Builder, IDefinition, pgettext } 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";
function TripettoBuilder(props: {
definition?: IDefinition;
onSave?: (definition: IDefinition) => void;
}) {
const builderElementRef = useRef<HTMLDivElement>(null);
const builderRef = useRef<Builder>();
const runnerElementRef = useRef<HTMLDivElement>(null);
const runnerRef = React.useRef();
const [view, setView] = React.useState("preview");
if (runnerRef.current) {
runnerRef.current.view = view;
}
useEffect(() => {
// When there is no builder instance active, create one!
if (!builderRef.current) {
builderRef.current = Builder.open(props.definition, {
element: elementRef.current,
// When the definition is saved, emit an event.
onSave: props.onSave,
// When the builder is ready, create the runner for the live preview runner
onReady: async (builder) => {
runnerRef.current = await run({
element: runnerElementRef.current,
definition: builder.definition,
view,
// Handle edit requests from the live preview
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) => {
runnerRef.current.definition = event.definition;
});
// When something is edited in the builder, let the preview know
builder.hook("OnEdit", "synchronous", (event) => {
runnerRef.current.doPreview(event.data);
});
}
});
}
// No need for cleanup, the builder instance will be destroyed automagically when the host element is removed from the DOM.
}, []);
return (
<>
<div ref={builderElementRef} className="builder"></div>
<div ref={runnerElementRef} className="runner"></div>
<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>
<div
className="edit-translations"
onClick={() => {
// Open the translations editor
builderRef.current.l10nEditor(
l10nContract(),
runnerRef.current.l10n,
(l10n) => {
// Translations are changed, update the runner
runnerRef.current.l10n = l10n;
}
);
}}
>
Edit translations
</div>
</>
)
}
import { Component, Input, Output, ElementRef, NgZone, EventEmitter, OnInit, OnDestroy, ChangeDetectionStrategy } from "@angular/core";
import { Builder, IBuilderChangeEvent, IBuilderEditEvent, IDefinition } from "tripetto";
import { IAutoscrollRunner, 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";
@Component({
selector: "tripetto-builder",
template: "",
styleUrls: ["./builder.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BuilderComponent implements OnInit, OnDestroy {
private builder?: Builder;
private runner?: IAutoscrollRunner;
private initialDefinition?: IDefinition;
/** Specifies the form definition. */
@Input() set definition(definition: IDefinition | undefined) {
if (this.builder) {
this.zone.runOutsideAngular(() => {
this.builder.definition = definition;
});
return;
}
this.initialDefinition = definition;
}
/** Retrieves the form definition. */
get definition(): IDefinition | undefined {
return (this.builder && this.builder.definition) || this.initialDefinition;
}
get isPreview(): boolean {
return (this.runner?.view || "preview") === "preview";
}
set isPreview(preview) {
if (this.runner) {
this.runner.view = preview ? "preview" : "test";
}
}
/**
* Invoked when the form definition is saved.
* @event
*/
@Output() saved = new EventEmitter<IDefinition>();
constructor(private element: ElementRef, private zone: NgZone) {}
ngOnInit() {
// Leave the builder outside of Angular to avoid unnecessary and costly change detection.
this.zone.runOutsideAngular(() => {
this.builder = Builder.open(this.definition, {
element: this.element.nativeElement.children[0],
onReady: async (builder) => {
this.runner = await run({
element: this.element.nativeElement.children[1],
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: IBuilderChangeEvent) => {
this.runner.definition = event.definition;
}
);
// When something is edited in the builder, let the preview know
builder.hook("OnEdit", "synchronous", (event: IBuilderEditEvent) => {
this.runner.doPreview(event.data);
});
builder.hook("OnClose", "synchronous", () => {
this.runner.destroy();
});
},
onSave: (definition) => {
this.saved.emit(definition);
}
});
});
}
ngOnDestroy() {
this.builder.destroy();
this.builder = undefined;
}
onPreview() {
this.isPreview = true;
}
onTest() {
this.isPreview = false;
}
onEditTranslations() {
if (this.builder && this.runner) {
// Open the translations editor
this.builder.l10nEditor(l10nContract(), this.runner.l10n, (l10n) => {
// Translations are changed, update the runner
this.runner.l10n = l10n;
});
}
}
}
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";
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);<