Skip to main content

Using subforms

Tripetto supports the use of subforms (also called nested forms). Simply put, these are forms within another form. They greatly help to structure large forms and simplify manageability. It is also possible to load subforms from external sources. This allows, for example, to create a library of commonly used forms that can then be imported by the builder and used in other forms. In order to let that work, you need to implement some additional events to instruct the builder where to find the forms that can be imported. This guide explains how subforms in Tripetto work and how external form sources can be used.

👩‍🏫 How subforms work

By default, the builder supports the use of subforms (this means you need to opt-out if you want to disable this feature, using the disableNesting property of the builder configuration). The builder user creates new subforms by holding (or right-clicking) the button to create a new section. So a regular tap (or left-click) on that button creates a new section as usual. But holding the button (or right-clicking it) shows a menu with the option to create a new (empty) subform.

Subforms are always stored inside the form definition of the main form. This is a careful design decision to minimize the impact on the complexity of loading a form definition in a runner. Since a single form definition also contains all the used subforms, there is no need for (asynchronous) loading of additional form definitions while the runner is running. That assures the form is valid, and it also stabilizes the form fingerprint, which helps process the response data that comes out of the runner.

So, how does this work? Surprisingly simple! A subform is defined by a branch inside a section in the form definition. This section has a special type, which is nest. Whenever the builder "sees" a section with type nest, it knows that's a subform, and it will render that (nested) branch with everything in it into a separate builder view.

The builder has various functions to convert existing sections and structures into subforms and vice versa. You can also move structures to subforms or parent forms. Just open the menu for a section to see all the possible options.

🗂️ Using external sources

When you want to enable the builder to load subforms from external sources, there are two possible options that can be implemented. First of all, you can supply a list of available forms. This list is shown by the builder when a user wants to create a new subform. The other option is to implement a custom UI component to let the user browse through the available forms. And of course, you can combine both options.

📇 List forms

The list of forms is shown when the builder user wants to add a new subform. It allows the use of submenus and dividers to create a multilayer menu, as shown in the following example:

The forms list is implemented using the onListForms property of the Builder class. This property accepts the list of forms as an array, a function that returns the array, or a Promise that resolves with an array. The latter is useful when you want to retrieve the list of forms from an API or other asynchronous process.

The forms list array accepts three types of items (click the items below to learn how to define them):

  • Forms (can be supplied by either their definition or reference);
  • Categories (used to define submenus as the Examples category in the screenshot above);
  • Dividers (used to separate items in the menu as the Recent forms and Your library sections in the screenshot above).
info

The form itself can be supplied by either the form definition or a reference string of the form. When a reference string is used, the onLoadForm event is required to load the actual form definition for the given reference. See loading subforms for more information. When the form definition is supplied, you may also return a promise that resolves to the form definition. That allows to asynchronous load form definitions from the list, without the need of the onLoadForm event.

Example

import { Builder } from "@tripetto/builder";

// Let's assume we have two form definitions in global variables so
// we can use these variables in the forms list.
declare CONTACT_FORM: IDefinition;
declare FEEDBACK_FORM: IDefinition;

const builder = new Builder({
onListForms: [
{
divider: "Recent forms",
},
{
name: "Contact form",
definition: CONTACT_FORM,
},
{
name: "Feedback form",
// We can also supply a function that returns a promise
definition: () =>
new Promise((resolve, reject) => {
// Let's simulate an asynchronous action
setTimeout(() => {
resolve(FEEDBACK_FORM);
}, 1000);
}),
},
{
divider: "Your library",
},
{
category: "Examples",
forms: [
{
name: "Contact form",
definition: CONTACT_FORM,
},
{
name: "Feedback form",
definition: FEEDBACK_FORM,
},
],
},
],
});

Run Try on CodePen

See the onListForms API reference for more information about this event and the possible options for listing items.

📂 Browse forms

In some cases, supplying forms through a list is not ideal. For example, when you have a lot of forms. Or when you want to enable the user to search for forms based on certain criteria. In that case, Tripetto allows to supply a custom UI for selecting an external form. This is implemented using the onBrowseForms property of the Builder class. The process is quite simple. When the user clicks the option to browse for a form (the label of this option can be configured using the browseFormsLabel property), the builder creates a blank canvas for you to construct the UI on to. A reference to the element of that canvas is supplied. In return, you should give a Promise back that resolves with the form definition (or the form reference) when a form is selected. You may also reject the promise to indicate that the user wants to cancel the action.

Example

The following example uses React to render a custom UI to browse for a form. In this case, a simple list of 2 forms that can be clicked and a cancel button to close the UI without selecting a form.

import { Builder } from "@tripetto/builder";

// Let's assume we have two form definitions in global variables so
// we can use these variables for this demo.
declare CONTACT_FORM: IDefinition;
declare FEEDBACK_FORM: IDefinition;

const builder = new Builder({
onBrowseForms: (element) =>
new Promise((resolve, reject) => {
// We use the styles property of the element to set a background
element.styles.backgroundColor = "rgba(0,0,0,0.2)";
element.styles.backdropFilter = "blur(5px)";

// Create a React root
const root = ReactDOM.createRoot(element.HTMLElement);

// Render the UI
root.render(
<div>
<h1>Browse forms</h1>
<ul>
<li onClick={() => resolve(CONTACT_FORM)}>Contact form</li>
<li onClick={() => resolve(FEEDBACK_FORM)}>Feedback form</li>
</ul>
<button onClick={() => reject()}>Close</button>
</div>
);
}),
});

Run Try on CodePen

See the onBrowseForms API reference for more information about this event.

📤 Loading subforms

When the onListForms or onBrowseForms events return a reference string of a form, the actual form definition for that form still needs to be loaded. In that case, the onLoadForm event of the Builder class comes into play. This event receives the reference of the form and should return the form definition (or a Promise that resolves with the form definition).

info

This event is also invoked when a user tries to update (reload) a subform. Only subforms that have a reference can be updated. You can implement the onUpdateForm event to indicate if there is an update available for a form.

Example

In most scenarios, this event calls an API to fetch the form definition. Here is an example using the Fetch API:

import { Builder } from "@tripetto/builder";

const builder = new Builder({
onLoadForm: (reference) =>
new Promise((resolve, reject) => {
fetch("/api/form/get/" + reference)
.then((response) => response.json())
.then((data) => resolve(data))
.catch(() => reject());
})
});

See the onLoadForm API reference for more information about this event.

🔃 Updating subforms

Subforms that have a reference set can be reloaded when the onLoadForm event is implemented. This action is initiated by the builder user from the subform menu. If you implement the onUpdateForm event, that update menu option is only available when there is an updated (newer) version of the form available. So, the event indicates to the builder if the current subform is the latest version or not. The event receives the reference of the subform, and the current loaded version (if that property is set) as input. The event should return true (or a Promise that resolves to true) when there is an update available for the form.

Example

In most scenarios, this event calls an API to fetch the update state. Here is an example using the Fetch API. In this example, the server would return a JSON object with a needsUpdate property that indicates if the form needs an update or not.

import { Builder } from "@tripetto/builder";

const builder = new Builder({
onUpdateForm: (reference, version) =>
new Promise((resolve, reject) => {
fetch("/api/form/update/" + reference)
.then((response) => response.json())
.then((data) => resolve(data.needsUpdate))
.catch(() => reject());
})
});

See the onUpdateForm API reference for more information about this event.

💾 Saving subforms

If you want to enable the builder user to save subforms, you should implement the onSaveForm event. This event receives the form definition of the subform as input and should return a Promise with the result of the save operation.

The event also receives an element that can be used to render a UI with, for example, a save dialog.

tip

You can change the default label for the save function using the saveFormLabel property.

Example

The following example uses React to render a custom UI to save a form. In a real application, the UI could ask the user for a location to store the form. When the user confirms, the form is stored, and a reference to the form is returned.

import { Builder } from "@tripetto/builder";

const builder = new Builder({
onSaveForm: (definition, element) =>
new Promise((resolve, reject) => {
// We use the styles property of the element to set a background
element.styles.backgroundColor = "rgba(0,0,0,0.2)";
element.styles.backdropFilter = "blur(5px)";

// Create a React root
const root = ReactDOM.createRoot(element.HTMLElement);

// Render the UI
root.render(
<div>
<h1>Save form</h1>
<button
onClick={() => {
fetch("/api/form/save", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(definition),
})
.then((response) => response.json())
.then((data) => resolve(data))
.catch(() => reject());
}}
>
Save
</button>
<button onClick={() => reject()}>Cancel</button>
</div>
);
}),
});

See the onSaveForm API reference for more information about this event.

info

Rendering a UI for saving a form is optional. You can also use this event to save the form directly to an endpoint and return the form reference for headless operation.

Whenever the builder opens a subform, a new builder panel is generated for it. This panel shifts into view, and by default, a floating navigation bar is shown for each subform. This navigation bar contains the title of the subform and contains buttons to edit the form title or return to the parent form. If you want, you can implement a custom navigation bar (or breadcrumb) to assist the user while navigating through subforms. To do so, you need to implement the onBreadcrumb event.

caution

As soon as you implement this event, the default navigation bar to close a subform (this is a floating bar in the builder) will not be shown anymore. So now you are in control and should present navigation buttons to the user.

Example

Here is an example of a custom navigation bar using React:

import React, { useEffect, useRef, useState } from "react";
import { Builder, IDefinition } from "@tripetto/builder";

function TripettoBuilder(props: {
definition?: IDefinition;
onSave?: (definition: IDefinition) => void;
}) {
const elementRef = useRef<HTMLDivElement>(null);
const builderRef = useRef<Builder>();
const [breadcrumb, setBreadcrumb] = useState([]);
const backRef = useRef();

useEffect(() => {
if (!builderRef.current) {
builderRef.current = Builder.open(props.definition, {
element: elementRef.current,
onSave: props.onSave,
onBreadcrumb: (forms, back) => {
backRef.current = back;
setBreadcrumb(forms);
}
});
}
}, []);

return (
<>
<div className="navigation">
<button
onClick={() => backRef.current && backRef.current()}
disabled={!backRef.current}
>
Back
</button>
{breadcrumb.map((form, i) => (
<>
{i > 0 ? " > " : ""}
<span onClick={() => form.edit()}>
{form.name || "Unnamed form"}
</span>
</>
))}
</div>
<div ref={elementRef} className="builder"></div>
</>
);
}

Run Try on CodePen

See the onBreadcrumb API reference for more information about this event.