Skip to main content

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.

  1. Enter the following commands:

    git clone https://github.com/uniformdev/examples
    cd examples
    cd examples/docs/custom-integrations/basic/nextjs/no-uniform
  2. Enter the following command:

    yarn
    caution

    For this tutorial you must use yarn to install the dependencies instead of npm.

  3. Set the variables in apps\demo-monsterpedia\.env to match the settings that apply to your Uniform project.

  4. Enter the following command:

    yarn canvas:basic:push
  5. In a new browser window, open Uniform and publish the composition Home Page.

  6. Return to the CLI.

  7. Enter the following command:

    yarn dev:basic
    About this step

    This command starts the following web apps:

    • Port 4030 - Configuration app
    • Port 4040 - Demo app
  8. 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.

tip

For more information about the registration process and the values involved, see the register custom integration guide.

  1. In Uniform, open team settings.

  2. Navigate to the Custom Integrations tab.

  3. Click ADD INTEGRATION.

    add-team-integration
  4. Enter the following integration settings within the drawer.

  5. For the field Name, enter the following value:

    Monsterpedia
  6. For the Category, dropdown, select Content.

  7. For the field Badge Icon Url, enter the following value:

    http://localhost:4030/monster-badge.svg
  8. For the field Logo Icon Url, enter the following value:

    http://localhost:4030/monster-logo.svg
  9. 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"
    }
    }
    }
  10. Click Save.

    About this step

    Your custom integration will appear in the list of custom integrations.

    team-integration-added

Add integration to project

  1. In Uniform, open your project.

  2. Navigate to the Integrations tab.

  3. Scroll to the section Browse Integrations.

    browse-integrations
  4. Click Monsterpedia.

    add-integration-to-project
  5. Click Add to project.

  6. The settings page is displayed.

    settings-page-initial

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:

TypeNameDescription
ComponentDragon Details
  • Has no parameters defined
ComponentLanding Page
  • Has a text parameter that stores the page name
  • Is a composition component
  • Has a slot named body
  • Body slot allows the component Dragon Details
CompositionHome Page
  • Is based on the component Landing Page
  • Slug is /
  • Parameter for page name is set to Welcome to the site!

Add parameter

  1. In Uniform, navigate to Canvas > Component Library > Dragon Details.

    config-canvas-select-component
  2. Scroll to the Parameters section.

    config-canvas-parameters-section
  3. Click the red (+) button.

  4. Enter the following values:

    • Parameter Name: Monster
    • Public ID: monster
    • Type: Monster List
    config-canvas-add-parameter-initial
    About this step

    The filter input field is not displayed because it comes from the parameter config page, which you haven't implemented yet.

  5. Click OK.

  6. 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.

  1. In the CLI, enter the following command, which adds a package with Uniform Mesh SDK.

    cd apps/mesh-monsterpedia
    yarn add @uniformdev/mesh-sdk-react
    caution

    This 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.

  2. 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 {
    useInitializeUniformMeshSdk,
    UniformMeshSdkContextProvider,
    } from "@uniformdev/mesh-sdk-react";

    import "../styles/globals.css";

    function MyApp({ Component, pageProps }) {
    return (
    <Component {...pageProps} />
    );
    }

    export default MyApp;
  3. 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 {
    useInitializeUniformMeshSdk,
    UniformMeshSdkContextProvider,
    } from "@uniformdev/mesh-sdk-react";

    import "../styles/globals.css";

    function MyApp({ Component, pageProps }) {
    const { initializing, error } = useInitializeUniformMeshSdk();
    if (error) {
    throw error;
    }
    return initializing ? null : (
    <UniformMeshSdkContextProvider>
    <Component {...pageProps} />
    </UniformMeshSdkContextProvider>
    );
    }

    export default MyApp;
    About this code

    If initialization fails, Uniform provides details in the error object. Since this isn't a thrown error, you need to throw it in order for it to be surfaced to the Canvas user. While there probably isn't anything the Canvas user can do about the error, throwing the error ensures that the failure is noticed.

    This code prevents the application from being rendered until the initialization process is finished. It also adds the Mesh SDK context provider.

  4. 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:

edit-settings-page

Get started

  1. 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.

  1. Add the following import statement to the top of the file:

    import React from 'react';
    import {
    Heading,
    } from '@uniformdev/design-system';
    ...
  2. Replace [!!! HEADING COMPONENT !!!] with the following code:

    <Heading>Monsterpedia settings</Heading>
  3. The page heading is styled to match the Uniform dashboard theme.

    settings-page-heading

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.

  1. Add the following import statements to the top of the file:

    import React, { useState } from 'react';
    import {
    Heading,
    Input,
    } from "@uniformdev/design-system";
    ...
  2. Add the following code inside the function:

    ...
    export default function Settings() {
    const [baseUrl, setBaseUrl] = useState();
    ...
    }
    About this code

    This 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.

  3. Replace [!!! URL COMPONENT !!!] with the following code:

    <Input
    caption="Specify the base URL for the D&amp;D 5th Edition API."
    id="baseUrl"
    label="Base URL (optional)"
    type="text"
    onChange={(e) => setBaseUrl(e?.target?.value)}
    />
  4. 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";
    ...
  5. Make the following change to the input component:

    <Input
    caption="Specify the base URL for the D&amp;D 5th Edition API."
    id="baseUrl"
    label="Base URL (optional)"
    placeholder={DEFAULT_BASE_MONSTER_URL}
    type="text"
    onChange={(e) => setBaseUrl(e?.target?.value)}
    />
  6. The base URL field appears with the placeholder text.

    settings-page-base-url

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.

  1. 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";
    ...
  2. Add the following code inside the function:

    ...
    export default function Settings() {
    const [baseUrl, setBaseUrl] = useState();
    const [message, setMessage] = useState();
    ...
    ...
    About this code

    This 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.

  3. Replace [!!! CALLOUT COMPONENT !!!] with the following code:

    {message ? (
    <Callout title={message.title} type={message.type}>
    {message.text}
    </Callout>
    ) : null}
    About this code

    This 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.

  1. 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";
    ...
  2. 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 code

    This 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.

  3. 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.

  1. 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";
    ...
  2. 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";
    ...
  3. 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;
    }
    }
    ...
  4. 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 code

    This 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.

  5. Replace [!!! TEST BUTTON COMPONENT !!!] with the following code:

    <Button buttonType="primary" className="mr-4" onClick={onTest}>
    Test
    </Button>
  6. The test button appears.

    settings-page-test-button
  7. Click the test button to check the connection to the external system.

    settings-page-test-button-success
  8. Enter a value for the base URL that will not work and click the test button to see an error message.

    settings-page-test-button-error

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/"
}
  1. 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 { useUniformMeshLocation } from "@uniformdev/mesh-sdk-react";
    import {
    createClient,
    DEFAULT_BASE_MONSTER_URL,
    } from "monsterpedia";
    ...
  2. Add the following code before the state variables:

    ...
    export default function Settings() {
    const { value, setValue } = useUniformMeshLocation();
    const [baseUrl, setBaseUrl] = useState();
    const [message, setMessage] = useState();
    const [isWorking, setIsWorking] = useState(false);
    ...
    About this code

    This 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.

  3. 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 code

    This 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.

  4. Set a default value for the baseUrl variable:

    ...
    export default function Settings() {
    const { value, setValue } = useUniformMeshLocation();
    const [baseUrl, setBaseUrl] = useState(value?.baseUrl);
    const [message, setMessage] = useState();
    const [isWorking, setIsWorking] = useState(false);
    ...
    About this code

    The state variable used to store the base URL should be set to the currently stored value when the page loads.

  5. Replace [!!! SAVE BUTTON COMPONENT !!!] with the following code:

    <Button buttonType="secondary" className="mr-4" onClick={onSave}>
    Save
    </Button>
  6. The save button appears.

    settings-page-save-button

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:

configure-parameter-page

Get started

  1. 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

tip

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.

  1. Add the following import statement to the top of the file:

    import React from "react";
    import { Input } from "@uniformdev/design-system";
    ...
  2. Replace [!!! FILTER INPUT FIELD COMPONENT !!!] with the following code:

    <Input
    caption="This filtering is very basic. Only enter one word &amp; no wildcards."
    id="url"
    label="Filter"
    placeholder="Enter a value to include monsters with a matching name"
    type="text"
    value=""
    onChange=""
    />
  3. Add the following import statement to the top of the file:

    import React from "react";
    import { Input } from "@uniformdev/design-system";
    import { useUniformMeshLocation } from "@uniformdev/mesh-sdk-react";
  4. Add the following code inside the function:

    ...
    export default function MonsterListParameterConfig() {
    const { value, setValue } = useUniformMeshLocation();
    ...
    }
  5. Add the following code inside the function:

    ...
    export default function MonsterListParameterConfig() {
    const { value, setValue } = useUniformMeshLocation();
    async function onChangeFilter(e) {
    await setValue({ filter: e.target.value });
    }
    ...
    }
  6. Make the following change to the filter input field component:

    <Input
    caption="This filtering is very basic. Only enter one word &amp; no wildcards."
    id="url"
    label="Filter"
    placeholder="Enter a value to include monsters with a matching name"
    type="text"
    value={value?.filter}
    onChange=""
    />
  7. Make the following change to the filter input field component:

    <Input
    caption="This filtering is very basic. Only enter one word &amp; 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.

  1. In Uniform, navigate to Canvas > Component Library > Dragon Details.

  2. Click the parameter Monster.

  3. For the field Filter, enter the following value:

    dragon
    config-canvas-edit-parameter-dragon
  4. Click OK.

  5. 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:

edit-parameter-page
tip

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

  1. 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

  1. Add the following import statement to the top of the file:

    import React, { 
    useState,
    } from 'react';
    ...
  2. Add the following code inside the function:

    ...
    export default function MonsterListParameterEditor() {
    const [loading, setLoading] = useState(true);
    ...
    }
    About this code

    This 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.

  3. Add the following import statement to the top of the file:

    import React, { 
    useState,
    } from 'react';
    import { LoadingIndicator } from "@uniformdev/design-system";
    ...
  4. Replace [!!! LOADING INDICATOR !!!] with the following code:

    {loading && <LoadingIndicator />}
    About this code

    This ensures the loading indicator component is only displayed when the component is in a loading state.

Load external data

  1. Add the following code inside the function:

    ...
    export default function MonsterListParameterEditor() {
    const [loading, setLoading] = useState(true);
    const [monsters, setMonsters] = useState([]);
    ...
    }
    About this code

    The 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.

  2. 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 code

    The 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.

  3. Add the following code after the import statements:

    function toResult(monster) {
    const { index, name } = monster;
    return { id: index, title: name };
    }
    About this code

    This 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").

  4. Add the following import statement to the top of the file:

    import React, { 
    useState,
    } from 'react';
    import { LoadingIndicator } from "@uniformdev/design-system";
    import {
    useUniformMeshLocation,
    } from "@uniformdev/mesh-sdk-react";
    ...
  5. Add the following import statement to the top of the file:

    import React, { 
    useState,
    } from 'react';
    import { LoadingIndicator } from "@uniformdev/design-system";
    import {
    useUniformMeshLocation,
    } from "@uniformdev/mesh-sdk-react";
    import { createClient } from "monsterpedia";
    ...
  6. 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";
    ...
  7. Add the following code after the state variables:

    const { 
    metadata,
    } = useUniformMeshLocation();
    About this code

    This 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.

  8. Add the following code after the state variables:

    const { 
    metadata,
    } = useUniformMeshLocation();
    const client = createClient(metadata?.settings?.baseUrl);
    About this code

    This uses the base URL to create a client object that can be used to read data from the extername system.

  9. 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 code

    This 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.

  1. Add the following import statement to the top of the file:

    import React, { 
    useState,
    } from 'react';
    import { LoadingIndicator } from "@uniformdev/design-system";
    import {
    EntrySearch,
    useUniformMeshLocation,
    } from "@uniformdev/mesh-sdk-react";
    import { createClient } from "monsterpedia";
    ...
  2. Replace [!!! MONSTER SELECTOR COMPONENT !!!] with the following code:

    {!loading && (
    <EntrySearch
    logoIcon="/monster-badge.svg"
    multiSelect={false}
    results={results}
    search={() => {}}
    />
    )}
    About this code

    This 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.

  1. Make the following change to the code inside the function:

    ...
    const {
    value,
    setValue,
    metadata,
    } = useUniformMeshLocation();
    const client = createClient(metadata?.settings?.baseUrl);
    ...
    About this code

    This 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).

  2. 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 code

    This is the event handler for when the user clicks the accept button in the EntrySearch component.

  3. Add the following code inside the function:

    ...
    export default function MonsterListParameterEditor() {
    const { value, setValue, metadata } = useUniformMeshLocation();
    const [loading, setLoading] = useState(true);
    const [monsters, setMonsters] = useState([]);
    const [results, setResults] = useState([]);
    const [selectedItems, setSelectedItems] = useState([]);
    ...
    }
    About this code

    This enables you to maintain state for the EntrySearch to keep track of which item is selected.

  4. 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 code

    For 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.

  5. Add the following to the EntrySearch component:

    <EntrySearch
    logoIcon="/monster-badge.svg"
    multiSelect={false}
    results={results}
    search={() => {}}
    selectedItems={selectedItems}
    />
    About this code

    This 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.

  6. 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 code

    This code is an event handler that is triggered when the state variable value changes (which happens when the select 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.

  7. Add the following to the EntrySearch component:

    <EntrySearch
    logoIcon="/monster-badge.svg"
    multiSelect={false}
    results={results}
    search={() => {}}
    selectedItems={selectedItems}
    select={onSelect}
    />
    About this code

    This 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.

edit-parameter-filtering
On the left are the results with no filtering assigned to the component's parameter. On the right are the results with a filter assigned.
  1. 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 code

    This adds the filter value on the parameter definition to the call to the external system.

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.

edit-parameter-search
Search box that enables users to search the available items.
  1. 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 code

    The 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.

  2. Add the following code inside the function:

    ...
    export default function MonsterListParameterEditor() {
    const { value, setValue, metadata } = useUniformMeshLocation();
    const [loading, setLoading] = useState(true);
    const [monsters, setMonsters] = useState([]);
    const [results, setResults] = useState([]);
    const [selectedItems, setSelectedItems] = useState([]);
    const [searchText, setSearchText] = useState();
    ...
    }
    About this code

    This enables you to maintain state for the EntrySearch to keep track of the search text the use entered.

  3. Add the following code after the state variables:

    const onSearch = (text) => setSearchText(text);
    About this code

    This is the event handler for when the user enters text in the search box in the EntrySearch component.

  4. Add the following code after the state variables:

    useEffect(() => {
    const results = getSearchResults(searchText, monsters);
    setResults(results);
    }, [searchText]);
    About this code

    This updates the search results when the search text changes.

  5. 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 code

    When 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.

  6. Change the following on the EntrySearch component:

    <EntrySearch
    logoIcon="/monster-badge.svg"
    multiSelect={false}
    results={results}
    search={onSearch}
    selectedItems={selectedItems}
    select={onSelect}
    />
    About this code

    This 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.

edit-parameter-metadata
  1. 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 code

    This function retrieves details about the selected item and set the property metadata on the selected item.

  2. 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 code

    In 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.

  3. 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 code

    The 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.

canvas-view-source-monster-selected

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.

tip

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.

  1. Open packages/canvas-monsterpedia/index.js

  2. Add the following code:

    export const CANVAS_MONSTER_LIST_PARAMETER_TYPES = ["monster-list"];
    About this code

    This 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.

  3. Add the following code:

    export function createMonsterEnhancer(client) {
    }
    About this code

    This a function that returns an enhancer. The front-end developer will use this function to get a reference to the enhancer.

  4. Add the following code inside the function:

    export function createMonsterEnhancer(client) {
    return async ({ parameter }) => {
    };
    }
    About this code

    This 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.

  5. Add the following code inside the function:

    export function createMonsterEnhancer(client) {
    return async ({ parameter }) => {
    const { type, value } = parameter;
    };
    }
    About this code

    This gets information from the parameter, specifically the type of the parameter and the value assigned to the parameter.

  6. 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 code

    Uniform 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.

  7. 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 code

    The 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.

  8. 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 code

    This code makes a call to the external system to retrieve the monster that matches the specified index.

  9. 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 code

    This 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.

  10. 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 code

    This 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.

tip

If you did not set the base URL setting in the parameter config page, you can skip this step.

  1. Open apps/demo-monsterpedia/.env

  2. Set the value for the variable MONSTERPEDIA_BASE_URL.

Add enhancer

  1. Open apps/demo-monsterpedia/pages/index.jsx

  2. 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 code

    This imports the standard Uniform components that enable you to run the enhancement process.

  3. 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 { Composition } from "@uniformdev/canvas-react";
    import {
    CANVAS_MONSTER_LIST_PARAMETER_TYPES,
    createMonsterEnhancer,
    } from "canvas-monsterpedia";
    ...
    About this code

    This imports the custom enhancer you built in the create enhancer section.

  4. 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 { Composition } from "@uniformdev/canvas-react";
    import {
    CANVAS_MONSTER_LIST_PARAMETER_TYPES,
    createMonsterEnhancer,
    } from "canvas-monsterpedia";
    import { createClient } from "monsterpedia";
    ...
    About this code

    This imports the custom enhancer you built in the create enhancer section.

  5. 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 code

    This 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.

  6. 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 code

    This runs the enhancement process before the static props are returned. The enhance() function mutates the composition object.

Confirm use cases work

You can use the instructions in the finished code section to confirm the use cases work.