Steps
These steps will guide you through the process of getting a fully functional, end-to-end example of a custom integration for Uniform with a demo site up and running in your development environment.
Create dev environment
You must create your development environment.
Enter the following commands:
git clone https://github.com/uniformdev/examples
cd examples
cd examples/docs/custom-integrations/basic/nextjs/no-uniformEnter the following command:
yarn
cautionFor this tutorial you must use yarn to install the dependencies instead of npm.
Set the variables in
apps\demo-monsterpedia\.env
to match the settings that apply to your Uniform project.Enter the following command:
yarn canvas:basic:push
In a new browser window, open Uniform and publish the composition Home Page.
Return to the CLI.
Enter the following command:
yarn dev:basic
About this stepThis command starts the following web apps:
- Port 4030 - Configuration app
- Port 4040 - Demo app
Make sure both apps are up and running.
Define custom integration
Register integration
Registering the custom integration in Uniform enables Uniform to incorporate the integration into the Uniform dashboard.
For more information about the registration process and the values involved, see the register custom integration guide.
In Uniform, open team settings.
Navigate to the Custom Integrations tab.
Click ADD INTEGRATION.
Enter the following integration settings within the drawer.
For the field Name, enter the following value:
Monsterpedia
For the Category, dropdown, select
Content
.For the field Badge Icon Url, enter the following value:
http://localhost:4030/monster-badge.svg
For the field Logo Icon Url, enter the following value:
http://localhost:4030/monster-logo.svg
For the field Location Configuration, enter the following value:
{
"baseLocationUrl": "http://localhost:4030/",
"locations": {
"canvas": {
"parameterTypes": [
{
"type": "monster-list",
"editorUrl": "/monster-list-parameter-editor",
"displayName": "Monster List",
"configureUrl": "/monster-list-parameter-config"
}
]
},
"install": {
"description": [
"Monsterpedia is an encyclopedia of monsters. This integration enables you to include information about various monsters in your digital experiences."
]
},
"settings": {
"url": "/settings"
}
}
}Click Save.
About this stepYour custom integration will appear in the list of custom integrations.
Add integration to project
In Uniform, open your project.
Navigate to the Integrations tab.
Scroll to the section Browse Integrations.
Click Monsterpedia.
Click Add to project.
The settings page is displayed.
Configure Canvas
In the create dev environment section, you used the Uniform CLI to push components and a composition to Canvas. In this section you will add a parameter to the Dragon Details that allows a Canvas use to use your custom integration to select a monster.
The following is a description of the current state of the elements you pushed to Canvas:
Type | Name | Description |
---|---|---|
Component | Dragon Details |
|
Component | Landing Page |
|
Composition | Home Page |
|
Add parameter
In Uniform, navigate to Canvas > Component Library > Dragon Details.
Scroll to the Parameters section.
Click the red (+) button.
Enter the following values:
- Parameter Name:
Monster
- Public ID:
monster
- Type: Monster List
About this stepThe filter input field is not displayed because it comes from the parameter config page, which you haven't implemented yet.
- Parameter Name:
Click OK.
Scroll to the top of the page and click Save and close.
Add Mesh SDK
A context provider for Mesh SDK enables the Next.js app to be integrated into the Uniform dashboard.
In the CLI, enter the following command, which adds a package with Uniform Mesh SDK.
cd apps/mesh-monsterpedia
yarn add @uniformdev/mesh-sdk-reactcautionThis package requires ESM externals, which, as of the time this tutorial was written, is an experimental feature in Next.js. This app is pre-configured with this feature enabled.
Open
apps/mesh-monsterpedia/pages/_app.jsx
and add the following import. This imports the hook that initializes the Mesh SDK.import React from "react";
import { MeshApp } from "@uniformdev/mesh-sdk-react";
import "../styles/globals.css";
function MyApp({ Component, pageProps }) {
return (
<Component {...pageProps} />
);
}
export default MyApp;Add the following code that initializes the Mesh SDK. The two objects returned can be used to determine the status of the initialization process.
import React from "react";
import { MeshApp } from "@uniformdev/mesh-sdk-react";
import "../styles/globals.css";
function MyApp({ Component, pageProps }) {
return (
<MeshApp>
<Component {...pageProps} />
</MeshApp>
)
}
export default MyApp;Refresh the integration settings page in your browser in order to make sure it renders without errors.
Create settings page
The settings page enables a Canvas user to set the endpoint used to read data from an external system. It also enables the user to test the settings to ensure a connection can be made.
The settings page you create will look like the following:

Get started
Open
apps/mesh-monsterpedia/pages/settings.jsx
import React from "react";
export default function Settings() {
return (
<>
[!!! HEADING COMPONENT !!!]
<p>These settings are used to establish a connection Monsterpedia.</p>
<div className="mt-4">
[!!! LOADING COMPONENT !!!]
[!!! CALLOUT COMPONENT !!!]
</div>
<div className="mt-4">
[!!! URL COMPONENT !!!]
</div>
<div className="mt-4">
[!!! SAVE BUTTON COMPONENT !!!]
[!!! TEST BUTTON COMPONENT !!!]
</div>
</>
);
}
Page heading
The heading component ensures the heading is displayed using styles that are consistent with the rest of the Uniform dashboard.
Add the following import statement to the top of the file:
import React from 'react';
import {
Heading,
} from '@uniformdev/design-system';
...Replace
[!!! HEADING COMPONENT !!!]
with the following code:<Heading>Monsterpedia settings</Heading>
The page heading is styled to match the Uniform dashboard theme.
Base URL field
By default, the D&D API available at https://www.dnd5eapi.co/
is used. This field lets the Uniform user override this URL.
This demonstrates how you can configure settings that are
available to any parameter type you define in your custom
integration.
Add the following import statements to the top of the file:
import React, { useState } from 'react';
import {
Heading,
Input,
} from "@uniformdev/design-system";
...Add the following code inside the function:
...
export default function Settings() {
const [baseUrl, setBaseUrl] = useState();
...
}About this codeThis enables you to mainstain state for the base URL input field. For now, the value is set only when the input field is changed. When you implement the save button, you will configure the value to be set when the page loads.
Replace
[!!! URL COMPONENT !!!]
with the following code:<Input
caption="Specify the base URL for the D&D 5th Edition API."
id="baseUrl"
label="Base URL (optional)"
type="text"
onChange={(e) => setBaseUrl(e?.target?.value)}
/>Add the following import statement to the top of the file:
import React, { useState } from 'react';
import {
Heading,
Input,
} from "@uniformdev/design-system";
import {
DEFAULT_BASE_MONSTER_URL,
} from "monsterpedia";
...Make the following change to the input component:
<Input
caption="Specify the base URL for the D&D 5th Edition API."
id="baseUrl"
label="Base URL (optional)"
placeholder={DEFAULT_BASE_MONSTER_URL}
type="text"
onChange={(e) => setBaseUrl(e?.target?.value)}
/>The base URL field appears with the placeholder text.
Callout
You will add a button that lets the user test the URL they entered. You need to communicate the results of the test to the user. A callout is a component that lets you display short messages to get the user's attention.
In a later step you will populate the callout with a message based on the test result. Now you are just adding the component to the page.
Add the following import statements to the top of the file:
import React, { useState } from 'react';
import {
Heading,
Input,
Callout,
} from '@uniformdev/design-system';
import {
DEFAULT_BASE_MONSTER_URL,
} from "monsterpedia";
...Add the following code inside the function:
...
export default function Settings() {
const [baseUrl, setBaseUrl] = useState();
const [message, setMessage] = useState();
...
...About this codeThis enables you to mainstain state for the message displayed in the callout. The value is set when the process that is triggered by the test button finishes.
Replace
[!!! CALLOUT COMPONENT !!!]
with the following code:{message ? (
<Callout title={message.title} type={message.type}>
{message.text}
</Callout>
) : null}About this codeThis ensures the callout is only rendered when a message is set.
Loading overlay
The the user tests the URL they entered, the browser sends a request using the URL. It can take some time for the request to be handled. While the request is being handled, you want to let the user know that work is in process. The loading overlay component adds a visual indicator that a process is running.
Add the following import statements to the top of the file:
import React, { useState } from 'react';
import {
Heading,
Input,
Callout,
LoadingOverlay,
} from '@uniformdev/design-system';
import {
DEFAULT_BASE_MONSTER_URL,
} from "monsterpedia";
...Add the following code before the return statement:
...
export default function Settings() {
const [baseUrl, setBaseUrl] = useState(value?.baseUrl);
const [message, setMessage] = useState();
const [isWorking, setIsWorking] = useState(false);
...
...About this codeThis enables you to mainstain state for the loading overlay so it can be activated and deactivated. The value is set when the test button is clicked.
Replace
[!!! LOADING COMPONENT !!!]
with the following code:<LoadingOverlay isActive={isWorking} statusMessage="Testing settings..." />
Test button
The test button lets the user confirm that the URL entered is valid and that API calls can be made to it.
Add the following import statement to the top of the file:
import React, { useState } from 'react';
import {
Heading,
Input,
Button,
LoadingOverlay,
Callout,
} from "@uniformdev/design-system";
import {
DEFAULT_BASE_MONSTER_URL,
} from "monsterpedia";
...Add the following import statement to the top of the file:
import React, { useState } from 'react';
import {
Heading,
Input,
Button,
LoadingOverlay,
Callout,
} from "@uniformdev/design-system";
import {
createClient,
DEFAULT_BASE_MONSTER_URL,
} from "monsterpedia";
...Add the following code after the state variables:
...
export default function Settings() {
const [baseUrl, setBaseUrl] = useState();
const [message, setMessage] = useState();
const [isWorking, setIsWorking] = useState(false);
function isValidUrl(url) {
try {
if (url) new URL(url);
return true;
} catch {
return false;
}
}
...Add the following code below the function
isValidUrl()
async function onTest() {
try {
setIsWorking(true);
if (!isValidUrl(baseUrl)) {
setMessage({ type: "error", text: "URL is not valid." });
return;
}
const client = createClient(baseUrl);
const monsters = await client.getMonsters();
if (monsters?.length >= 0) {
setMessage({
type: "success",
text: `This is a valid endpoint (${monsters.length} monsters returned)`,
});
} else {
setMessage({
type: "error",
text: "The endpoint did not return the expected output.",
});
}
return;
}
catch (error) {
setMessage({ type: "error", text: error.message });
} finally {
setIsWorking(false);
}
}About this codeThis code will be triggered when the test button is clicked. It validates the URL and that API calls can be made to it. It also provides feedback to the user during and after the process.
Replace
[!!! TEST BUTTON COMPONENT !!!]
with the following code:<Button buttonType="primary" className="mr-4" onClick={onTest}>
Test
</Button>The test button appears.
Click the test button to check the connection to the external system.
Enter a value for the base URL that will not work and click the test button to see an error message.
Save button
When the user clicks the save button, the value entered in the form is saved as a string to Uniform persistent storage. You have complete control over the string that is saved.
For this example, you will store the form data in a JSON string with the following shape:
{
"baseUrl": "https://www.dnd5eapi.co/"
}
Add the following import statement to the top of the file:
import React, { useState } from "react";
import {
Heading,
Input,
Button,
LoadingOverlay,
Callout,
} from "@uniformdev/design-system";
import { useMeshLocation } from "@uniformdev/mesh-sdk-react";
import {
createClient,
DEFAULT_BASE_MONSTER_URL,
} from "monsterpedia";
...Add the following code before the state variables:
...
export default function Settings() {
const { value, setValue } = useMeshLocation();
const [baseUrl, setBaseUrl] = useState();
const [message, setMessage] = useState();
const [isWorking, setIsWorking] = useState(false);
...About this codeThis enables you to maintain state for the page itself. When the state value is saved, Uniform write it to permanent storage. The value is set when the save button is clicked.
Add the following code after the function
onTest()
async function onSave() {
if (!isValidUrl(baseUrl)) {
setMessage({ type: "error", text: "Base URL is not valid." });
return;
}
setIsWorking(true);
try {
await setValue({ baseUrl });
setMessage({ type: "success", text: "Settings were saved." });
}
catch (error) {
setMessage({
type: "error",
text: `Unable to save settings: ${error.message}`,
});
} finally {
setIsWorking(false);
}
}About this codeThis code will be trigged when the save button is clicked. It saves the value the user entered to Uniform persistent storage. It also provides feedback to the user during and after this process.
Set a default value for the
baseUrl
variable:...
export default function Settings() {
const { value, setValue } = useMeshLocation();
const [baseUrl, setBaseUrl] = useState(value?.baseUrl);
const [message, setMessage] = useState();
const [isWorking, setIsWorking] = useState(false);
...About this codeThe state variable used to store the base URL should be set to the currently stored value when the page loads.
Replace
[!!! SAVE BUTTON COMPONENT !!!]
with the following code:<Button buttonType="secondary" className="mr-4" onClick={onSave}>
Save
</Button>The save button appears.
Create parameter config page
When a Canvas user adds a parameter to a component, options specific to the parameter type can be configured. The parameter config page represents the user interface for those options.
The parameter config page you create will look like the following:

Get started
Open
apps/mesh-monsterpedia/pages/monster-list-parameter-config.jsx
import React from "react";
export default function MonsterListParameterConfig() {
return (
<div>
<div>
[!!! FILTER INPUT FIELD COMPONENT !!!]
</div>
</div>
);
}
Filter field
If you open the Dragon Details component in Canvas and open the parameter Monster, you will be able to see the changes you make to the code in the page in real time.
Add the following import statement to the top of the file:
import React from "react";
import { Input } from "@uniformdev/design-system";
...Replace
[!!! FILTER INPUT FIELD COMPONENT !!!]
with the following code:<Input
caption="This filtering is very basic. Only enter one word & no wildcards."
id="url"
label="Filter"
placeholder="Enter a value to include monsters with a matching name"
type="text"
value=""
onChange=""
/>Add the following import statement to the top of the file:
import React from "react";
import { Input } from "@uniformdev/design-system";
import { useMeshLocation } from "@uniformdev/mesh-sdk-react";Add the following code inside the function:
...
export default function MonsterListParameterConfig() {
const { value, setValue } = useMeshLocation();
...
}Add the following code inside the function:
...
export default function MonsterListParameterConfig() {
const { value, setValue } = useMeshLocation();
async function onChangeFilter(e) {
await setValue({ filter: e.target.value });
}
...
}Make the following change to the filter input field component:
<Input
caption="This filtering is very basic. Only enter one word & no wildcards."
id="url"
label="Filter"
placeholder="Enter a value to include monsters with a matching name"
type="text"
value={value?.filter}
onChange=""
/>Make the following change to the filter input field component:
<Input
caption="This filtering is very basic. Only enter one word & no wildcards."
id="url"
label="Filter"
placeholder="Enter a value to include monsters with a matching name"
type="text"
value={value?.filter}
onChange={onChangeFilter}
/>
Update Dragon Detail component
You added a parameter to the Dragon Details component using a custom parameter type in the add parameter section. The parameter config page will now be available when you go to configure a parameter using your custom parameter type.
Since the Dragon Details component is designed to let the Canvas user select a dragon, you should use the filter field to limit the list of monsters to those with the word "dragon" in their name.
In Uniform, navigate to Canvas > Component Library > Dragon Details.
Click the parameter Monster.
For the field Filter, enter the following value:
dragon
Click OK.
Click Save and close.
Create parameter editor page
When a Canvas user sets a value on a parameter in a composition, options specific to the parameter type can be configured. The parameter editor page represents the user interface for those options.
The parameter editor page you create will look like the following:

If you open the Home Page composition in Canvas and select the component Dragon Details, you will be able to see the changes you make to the code in the page in real time.
Get started
Open
apps/mesh-monsterpedia/pages/monster-list-parameter-editor.jsx
import React from "react";
export default function MonsterListParameterEditor() {
return (
<div>
<div>
[!!! LOADING INDICATOR !!!]
[!!! MONSTER SELECTOR COMPONENT !!!]
</div>
</div>
);
}
Loading indicator
Add the following import statement to the top of the file:
import React, {
useState,
} from 'react';
...Add the following code inside the function:
...
export default function MonsterListParameterEditor() {
const [loading, setLoading] = useState(true);
...
}About this codeThis enables you to mainstain state for the loading overlay so it can be activated and deactivated. The value is set when data is loaded from the external system.
Add the following import statement to the top of the file:
import React, {
useState,
} from 'react';
import { LoadingIndicator } from "@uniformdev/design-system";
...Replace
[!!! LOADING INDICATOR !!!]
with the following code:{loading && <LoadingIndicator />}
About this codeThis ensures the loading indicator component is only displayed when the component is in a loading state.
Load external data
Add the following code inside the function:
...
export default function MonsterListParameterEditor() {
const [loading, setLoading] = useState(true);
const [monsters, setMonsters] = useState([]);
...
}About this codeThe collection of monsters is retrieved from the external system. The process of reading this data is relatively expensive, so you only want to run it when necessary. This state variable lets you persist the list of monsters when the page reloads.
Add the following code inside the function:
...
export default function MonsterListParameterEditor() {
const [loading, setLoading] = useState(true);
const [monsters, setMonsters] = useState([]);
const [results, setResults] = useState([]);
...
}About this codeThe component that displays the list of monsters to the user displays the monsters in this state variable. That component requires objects have a shape that is different from the shape of the monsters returned from the external system.
Add the following code after the import statements:
function toResult(monster) {
const { index, name } = monster;
return { id: index, title: name };
}About this codeThis function implements the logic to convert an object from the external system (a "monster") into an object that can be displayed on the page (a "result").
Add the following import statement to the top of the file:
import React, {
useState,
} from 'react';
import { LoadingIndicator } from "@uniformdev/design-system";
import {
useMeshLocation,
} from "@uniformdev/mesh-sdk-react";
...Add the following import statement to the top of the file:
import React, {
useState,
} from 'react';
import { LoadingIndicator } from "@uniformdev/design-system";
import {
useMeshLocation,
} from "@uniformdev/mesh-sdk-react";
import { createClient } from "monsterpedia";
...Add the following import statement to the top of the file:
import React, {
useState,
useEffect,
} from 'react';
import { LoadingIndicator } from "@uniformdev/design-system";
import { createClient } from "monsterpedia";
...Add the following code after the state variables:
const {
metadata,
} = useMeshLocation();About this codeThis gets an object that provides access to metadata for the parameter. You can use this object to get the custom integration's base URL setting.
Add the following code after the state variables:
const {
metadata,
} = useMeshLocation();
const client = createClient(metadata?.settings?.baseUrl);About this codeThis uses the base URL to create a client object that can be used to read data from the extername system.
Add the following code after the state variables:
useEffect(() => {
async function getMonsters() {
const monsters = await client.getMonsters();
setMonsters(monsters);
const results = monsters.map(toResult);
setResults(results);
setLoading(false);
}
getMonsters();
}, []);About this codeThis code runs when the page is loaded. It controls retrieving and displaying the list of monsters. The code does not yet support using the filter setting you added in to the parameter config page.
EntrySearch component
While you can create any kind of user interface you want for your custom integration, Uniform provides a number of components that you can use. In addition to saving you development time, these components ensure your user interface matches the design of the Uniform dashboard.
Add the following import statement to the top of the file:
import React, {
useState,
} from 'react';
import { LoadingIndicator } from "@uniformdev/design-system";
import {
EntrySearch,
useMeshLocation,
} from "@uniformdev/mesh-sdk-react";
import { createClient } from "monsterpedia";
...Replace
[!!! MONSTER SELECTOR COMPONENT !!!]
with the following code:{!loading && (
<EntrySearch
logoIcon="/monster-badge.svg"
multiSelect={false}
results={results}
search={() => {}}
/>
)}About this codeThis adds the component that lets the user select a monster. Search functionality will be added later, but a value is required, so the empty function is specified for now.
Add select entry
The EntrySearch component lets the user control which item is selected. When an item is selected (or unselected), the component calls a function. You must implement this function in order for the selected (or unselected) value to get saved.
Make the following change to the code inside the function:
...
const {
value,
setValue,
metadata,
} = useMeshLocation();
const client = createClient(metadata?.settings?.baseUrl);
...About this codeThis enables you to maintain state for the page itself. The state value will be set when the user selects a monster from the EntrySearch component. This does not immediately write the value to permanent storage. That happens when the user saves the composition (i.e. clicks the save button).
Add the following code after the state variables:
const onSelect = (selected) => {
if (selected && selected.length == 1) {
setValue({ index: selected[0].id });
} else {
setValue("");
}
};About this codeThis is the event handler for when the user clicks the accept button in the EntrySearch component.
Add the following code inside the function:
...
export default function MonsterListParameterEditor() {
const { value, setValue, metadata } = useMeshLocation();
const [loading, setLoading] = useState(true);
const [monsters, setMonsters] = useState([]);
const [results, setResults] = useState([]);
const [selectedItems, setSelectedItems] = useState([]);
...
}About this codeThis enables you to maintain state for the EntrySearch to keep track of which item is selected.
In the effect hook with a dependency on
[]
, add the following code:useEffect(() => {
async function getMonsters() {
const monsters = await client.getMonsters();
setMonsters(monsters);
const results = monsters.map(toResult);
setResults(results);
if (value?.index) {
const selected = results.filter((result) => result.id == value.index);
setSelectedItems(selected);
}
setLoading(false);
}
getMonsters();
}, []);About this codeFor cases where the selected item was set prior to the page loading (e.g. from an earier session), the selected item state must be set.
Add the following to the EntrySearch component:
<EntrySearch
logoIcon="/monster-badge.svg"
multiSelect={false}
results={results}
search={() => {}}
selectedItems={selectedItems}
/>About this codeThis tells the EntrySearch component to use a state variable to determine which items are selected. When items are selected, the EntrySearch component renders itself differently.
Add the following code after the state variables:
useEffect(() => {
if (value?.index) {
const selected = results.filter((result) => result.id == value.index);
if (selected && selected.length > 0) {
setSelectedItems(selected);
return;
}
}
setSelectedItems();
}, [value]);About this codeThis code is an event handler that is triggered when the state variable
value
changes (which happens when theselect
event is triggered on the EntrySearch component). This event handler sets the state variable that the EntrySearch component uses to determine which entries are currently selected.Add the following to the EntrySearch component:
<EntrySearch
logoIcon="/monster-badge.svg"
multiSelect={false}
results={results}
search={() => {}}
selectedItems={selectedItems}
select={onSelect}
/>About this codeThis assigns an event handler to the
select
event on the component.
Add filtering
In the parameter configuration page you provided the user the ability to set a filter. This filter value must be incorporated into the call to the external API. This means filtering is applied before the items are displayed to the user.

In the effect hook with a dependency on
[]
, make the following change:useEffect(() => {
async function getMonsters() {
const filter = metadata?.parameterDefinition?.typeConfig?.filter;
const monsters = await client.getMonsters(filter);
setMonsters(monsters);
const results = monsters.map(toResult);
setResults(results);
if (value?.index) {
const selected = results.filter((result) => result.id == value.index);
setSelectedItems(selected);
}
setLoading(false);
}
getMonsters();
}, []);About this codeThis adds the filter value on the parameter definition to the call to the external system.
Add user search
The EntrySearch component provides a search field that lets the user filter the list of search results it displays. Since you already have an array of all available monsters, you can support user search without having to make another call to the external system.

Add the following code after the import statements:
function getSearchResults(filter, monsters) {
if (!monsters) {
return [];
}
if (!filter) {
return monsters.map(toResult);
}
const regex = new RegExp(filter, "i");
const filtered = monsters.filter((monster) => {
return monster.name.match(regex);
});
return filtered.map(toResult);
}About this codeThe filtered list of monsters is already available in the monsters state variable. This function applies the search text from the user to the list of monsters.
Add the following code inside the function:
...
export default function MonsterListParameterEditor() {
const { value, setValue, metadata } = useMeshLocation();
const [loading, setLoading] = useState(true);
const [monsters, setMonsters] = useState([]);
const [results, setResults] = useState([]);
const [selectedItems, setSelectedItems] = useState([]);
const [searchText, setSearchText] = useState();
...
}About this codeThis enables you to maintain state for the EntrySearch to keep track of the search text the use entered.
Add the following code after the state variables:
const onSearch = (text) => setSearchText(text);
About this codeThis is the event handler for when the user enters text in the search box in the EntrySearch component.
Add the following code after the state variables:
useEffect(() => {
const results = getSearchResults(searchText, monsters);
setResults(results);
}, [searchText]);About this codeThis updates the search results when the search text changes.
In the effect hook with a dependency on
[]
, make the following change:useEffect(() => {
async function getMonsters() {
const filter = metadata?.parameterDefinition?.typeConfig?.filter;
const monsters = await client.getMonsters(filter);
setMonsters(monsters);
const results = getSearchResults(searchText, monsters);
setResults(results);
if (value?.index) {
const selected = results.filter((result) => result.id == value.index);
setSelectedItems(selected);
}
setLoading(false);
}
getMonsters();
}, []);About this codeWhen the page is first loaded, the search text variable will be empty, but you still want to try to use the same search logic everywhere in the component.
Change the following on the EntrySearch component:
<EntrySearch
logoIcon="/monster-badge.svg"
multiSelect={false}
results={results}
search={onSearch}
selectedItems={selectedItems}
select={onSelect}
/>About this codeThis assigns an event handler to the
search
event on the component.
Add metadata
When an item is selected, the EntrySearch component displays
the value of the property title
from the selected item. If
the selected item has a property metadata
, it will also
display the properties of the object assigned to the metadata
property.

Add the following code after the state variables:
async function addMetadata(selected) {
for (let i = 0; i < selected.length; i++) {
const monster = await client.getMonster(selected[i].id);
if (monster) {
const { alignment, index, size, url } = monster;
selected[i].metadata = { alignment, index, size, url };
}
}
setSelectedItems(selected);
}About this codeThis function retrieves details about the selected item and set the property
metadata
on the selected item.In the effect hook with a dependency on
[]
, add the following code:useEffect(() => {
async function getMonsters() {
const filter = metadata?.parameterDefinition?.typeConfig?.filter;
const monsters = await client.getMonsters(filter);
setMonsters(monsters);
const results = getSearchResults(searchText, monsters);
setResults(results);
if (value?.index) {
const selected = results.filter((result) => result.id == value.index);
await addMetadata(selected);
setSelectedItems(selected);
}
setLoading(false);
}
getMonsters();
}, []);About this codeIn the case when the page is loaded for the first time and a monster has already been set on the parameter. You must retrieve metadata for the selected monster and then update the state variable for selected items in order for the EntrySearch component to refresh.
In the effect hook with a dependency on
[value]
, make the following change:useEffect(() => {
if (value?.index) {
const selected = results.filter((result) => result.id == value.index);
if (selected && selected.length > 0) {
addMetadata(selected).then(() => {
setSelectedItems(selected);
});
return;
}
}
setSelectedItems();
}, [value]);About this codeThe variable
value
changes when a monster is selected or unselected. This code ensures metadata is loaded when a monster is selected.
Create enhancer
When a Canvas editor selects a monster, the only value that is saves is the monster index. You can see this in Canvas when you use the View Source option.

An enhancer is the component that runs at build-time that takes that index and retrieves the relevant details from the external system. The enhancer packages those details up and Uniform makes them available to the front-end component via props.
As the custom integration developer, you provide one or more enhancers. In the Update demo app section, you will see how the front-end developer uses the enhancer.
When you create a custom integration, you provide enhancers to make it easier for other front-end developers to use your product reducing, if not eliminating, the need for them to have experience with the system you are integrating.
Open
packages/canvas-monsterpedia/index.js
Add the following code:
export const CANVAS_MONSTER_LIST_PARAMETER_TYPES = ["monster-list"];
About this codeThis defines an array of the different Canvas parameter types this enhancer supports. This value must match the parameter type you created when you registered your custom integration.
Add the following code:
export function createMonsterEnhancer(client) {
}About this codeThis a function that returns an enhancer. The front-end developer will use this function to get a reference to the enhancer.
Add the following code inside the function:
export function createMonsterEnhancer(client) {
return async ({ parameter }) => {
};
}About this codeThis is the enhancer itself, which is just a function that accepts an object with a property that represents the parameter that the enhancer may or may not support.
Add the following code inside the function:
export function createMonsterEnhancer(client) {
return async ({ parameter }) => {
const { type, value } = parameter;
};
}About this codeThis gets information from the parameter, specifically the type of the parameter and the value assigned to the parameter.
Add the following code inside the function:
export function createMonsterEnhancer(client) {
return async ({ parameter }) => {
const { type, value } = parameter;
if (type == CANVAS_MONSTER_LIST_PARAMETER_TYPES[0]) {
}
};
}About this codeUniform gives all enhancers the option to enhance a parameter. It is up to the enhancer to determine whether it wants to do anything. This logic ensures that the enhancer only affects parameters that are of a specific type.
Add the following code inside the function:
export function createMonsterEnhancer(client) {
return async ({ parameter }) => {
const { type, value } = parameter;
if (type == CANVAS_MONSTER_LIST_PARAMETER_TYPES[0]) {
const { index } = value;
}
};
}About this codeThe value on the parameter depends on the logic used to save the value. You implemented logic in the add select entry section that results in the value having a property
index
. This code gets the value of that property.Add the following code inside the function:
export function createMonsterEnhancer(client) {
return async ({ parameter }) => {
const { type, value } = parameter;
if (type == CANVAS_MONSTER_LIST_PARAMETER_TYPES[0]) {
const { index } = value;
const monster = await client.getMonster(index);
}
};
}About this codeThis code makes a call to the external system to retrieve the monster that matches the specified index.
Add the following code inside the function:
export function createMonsterEnhancer(client) {
return async ({ parameter }) => {
const { type, value } = parameter;
if (type == CANVAS_MONSTER_LIST_PARAMETER_TYPES[0]) {
const { index } = value;
const monster = await client.getMonster(index);
if (monster?.index == index) {
return monster;
}
}
};
}About this codeThis code confirms that the data returned from the external system matches what the code requested. If it does, the enhancer returns the monster. Uniform will then replace the parameter's value with the monster.
Add the following code inside the function:
export function createMonsterEnhancer(client) {
return async ({ parameter }) => {
const { type, value } = parameter;
if (type == CANVAS_MONSTER_LIST_PARAMETER_TYPES[0]) {
const { index } = value;
const monster = await client.getMonster(index);
if (monster?.index == index) {
return monster;
}
}
return value;
};
}About this codeThis code runs when the enhancer determines the parameter is not supported. It just returns the original value.
Update demo app
The demo app is already configured to use Canvas, but it needs to be updated to use the enhancer you created.
Add environment variable
During the build process, the demo app will use your custom enhancer
to make calls to the external system in order to retrieve details
about any monsters that are selected on Dragon Details components.
By default, the enhancer will use https://www.dnd5eapi.co/
.
However, if a user configured the base URL setting in the parameter config page, you must use that value. The value that is stored in Uniform and is not available to the demo app. This is for security reasons. You must provide the value yourself. An environment variable is the best way to do this.
If you did not set the base URL setting in the parameter config page, you can skip this step.
Open
apps/demo-monsterpedia/.env
Set the value for the variable MONSTERPEDIA_BASE_URL.
Add enhancer
Open
apps/demo-monsterpedia/pages/index.jsx
Add the following import statement to the top of the file:
import {
CanvasClient,
enhance,
EnhancerBuilder,
CANVAS_DRAFT_STATE,
CANVAS_PUBLISHED_STATE,
} from "@uniformdev/canvas";
...About this codeThis imports the standard Uniform components that enable you to run the enhancement process.
Add the following import statement to the top of the file:
import {
CanvasClient,
enhance,
EnhancerBuilder,
CANVAS_DRAFT_STATE,
CANVAS_PUBLISHED_STATE,
} from "@uniformdev/canvas";
import { UniformComposition } from "@uniformdev/canvas-react";
import {
CANVAS_MONSTER_LIST_PARAMETER_TYPES,
createMonsterEnhancer,
} from "canvas-monsterpedia";
...About this codeThis imports the custom enhancer you built in the create enhancer section.
Add the following import statement to the top of the file:
import {
CanvasClient,
enhance,
EnhancerBuilder,
CANVAS_DRAFT_STATE,
CANVAS_PUBLISHED_STATE,
} from "@uniformdev/canvas";
import { UniformComposition } from "@uniformdev/canvas-react";
import {
CANVAS_MONSTER_LIST_PARAMETER_TYPES,
createMonsterEnhancer,
} from "canvas-monsterpedia";
import { createClient } from "monsterpedia";
...About this codeThis imports the custom enhancer you built in the create enhancer section.
Add the following to the file:
function getEnhancers() {
const client = createClient(process.env.MONSTERPEDIA_BASE_URL);
const monsterEnhancer = createMonsterEnhancer(client);
return new EnhancerBuilder().parameterType(
CANVAS_MONSTER_LIST_PARAMETER_TYPES,
monsterEnhancer
);
}About this codeThis function creates an instance of your enhancer and adds an instruction that tells Uniform to apply this enhancer to any parameter that uses the custom parameter type you created when you registered your custom integration.
In the
getStaticProps
function, add the following:export async function getStaticProps({ preview }) {
const slug = "/";
const composition = await getComposition(slug, preview);
const enhancers = getEnhancers();
await enhance({ composition, enhancers });
return {
props: { composition },
};
}About this codeThis runs the enhancement process before the static props are returned. The
enhance()
function mutates thecomposition
object.
Confirm use cases work
You can use the instructions in the finished code section to confirm the use cases work.