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
andYour library
sections in the screenshot above).
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,
},
],
},
],
});
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>
);
}),
});
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).
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.
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.
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.
🧭 Navigation and breadcrumb
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.
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>
</>
);
}
See the onBreadcrumb
API reference for more information about this event.