Announcing the Repeater app — and how I used the App Framework and Forma 36 to build it

If you’ve ever wished you could repeat a certain pattern in Contentful — maybe a list of ingredients, items or products — but found that the built-in fields don’t quite do what you’re looking for, then the Repeater app is your answer. On a live stream I did with Stefan Judis, we worked on building a prototype of an app that helped with this. Now it’s become a reality.

Today, I am launching the Repeater app on the Contentful App Marketplace. Repeating content should be easy, but the reality is that everyone’s use case is a little different.

I’ll show how I built the app using the App Framework, Forma 36 and a little bit of custom code, and give you the tools to extend it. If you already have a good understanding of building apps, you can check out the Repeater app repo here, fork it, and go to town!

GIF showcasing how to use the repeater ap

Getting started

To start, we will use the create-contentful-app CLI tool.This is available free to developers, and helps get apps up and running quickly. The create-contentful-app CLI tool creates a React app project with our design library: Forma 36 and our open-source field editors for an easy way to get to writing business logic fast.

We get all this by running the commands below:

sh
npx @contentful/create-contentful-app init repeater-app
cd repeater-app
npm start

An App Framework app is a front-end single page application that runs in an iframe. At this point our app is running on localhost:3000. But it won’t be accessible until we create the AppDefinition and select the locations where we want it to show up. In other words, the locations where the iframe will be loaded in the Contentful interface.

Creating the AppDefinition

Simply put, an AppDefinition is the entity that represents an app in Contentful. You can think of it as a sort of blueprint for how your app will function.

To get started, you must be a part of a Contentful organization with an admin or developer account. If you prefer to develop in a space or environment which isn’t production-facing, you can create a Contentful Community edition. Or you can develop an app in your primary organization.

To create the AppDefinition, head to your organization settings and click on Apps in the top menu bar. Once on the AppDefinition page, click the button to create a new app.

  • First, we are going to name our app: Repeater

  • Next, we will make its app URL http://localhost:3000. This is where our app is currently running. Then, we are going to select the field location and pick the JSON field and click the confirm button to save this AppDefinition.

  • Last, we will create a new instance parameter called valueName, which will be short text.

Here’s what your AppDefinition should look like:

A screenshot of what the app defition should look like

Using a JSON field with the Repeater app

Let’s now head over to a space where we want to see our app show up. In the top menu of our space or environment, click Apps, then Manage apps. From here we can find our newly created Repeater app and install it into our space.

Because we are building an app that will take over the appearance of the JSON field, we must also have a content type that makes use of a JSON field. You can create a content type to do this. When creating the JSON field, we need to configure it by editing the settings of the field and selecting appearance. Choose our Repeater app as its appearance.

Screenshot of the JSON field created for the repeater app

Coding the app for custom functionality

Once the app is assigned to a field, we can head over to our entries section and find an entry to see how the app is displayed.

Coding the app for custome functionality

As we can see, our app is running but the functionality now needs to be built. Inside the project code, I’m specifically going to work on the src/components/Field.tsx React component. This maps to the location of the field inside of a Contentful entry. To start, let’s import some components and tools:

js
import React, { useEffect, useState } from 'react';
import {
    Button,
    EditorToolbarButton,
    Table,
    TableBody,
    TableRow,
    TableCell,
    TextField,
} from '@contentful/forma-36-react-components';
import tokens from '@contentful/forma-36-tokens';
import { FieldExtensionSDK } from '@contentful/app-sdk';
import { v4 as uuid } from 'uuid';

interface FieldProps {
    sdk: FieldExtensionSDK;
}

As a note, I am using uuid, a unique ID-generating library, as a way to tag items in our app with an ID.

Because we are using typescript, I’m also going to create a type for our Repeater Item:

js
/** An Item which represents an list item of the repeater app */
interface Item {
    id: string;
    key: string;
    value: string;
}

I will also start to replace the default functionality inside of the Field component:

js
/** A simple utility function to create a 'blank' item
 * @returns A blank `Item` with a uuid
*/
function createItem(): Item {
    return {
        id: uuid(),
        key: '',
        value: '',
    };
}

/** The Field component is the Repeater App which shows up 
 * in the Contentful field.
 * 
 * The Field expects and uses a `Contentful JSON field`
 */
const Field = (props: FieldProps) => {
    const { valueName = 'Value' } = props.sdk.parameters.instance as any;
    const [items, setItems] = useState<Item[]>([]);

In the Field component itself, we have two main things going on.

  1. We are going to read the value of our instance parameter that we created during the AppDefinition process. This value will be a display name for our list which I will point out again later on to clarify.

  2. Items are going to be stored in an array of items. These items will be created by the createItem function and return an object with the shape of Item as outlined by our typescript interface.

Next, let’s add functionality for when the app loads.

js
   useEffect(() => {
        // This ensures our app has enough space to render
        props.sdk.window.startAutoResizer();

        // Every time we change the value on the field, we update internal state
        props.sdk.field.onValueChanged((value: Item[]) => {
            if (Array.isArray(value)) {
                setItems(value);
            }
        });
    });

We have two main things happening.

  1. When the app loads, ensure the app shows up correctly in the UI by calling the startAutoResizer function, which is provided on the SDK and automatically resizes the app if necessary.

  2. Using the SDK’s field.onValueChanged, we can listen for changes to the field’s underlying value and set our React state to reflect the data that is stored in Contentful.

Now, let’s add a few utility functions.

js
    /** Adds another item to the list */
    const addNewItem = () => {
        props.sdk.field.setValue([...items, createItem()]);
    };

    /** Creates an `onChange` handler for an item based on its `property`
     * @returns A function which takes an `onChange` event 
    */
    const createOnChangeHandler = (item: Item, property: 'key' | 'value') => (
        e: React.ChangeEvent<HTMLInputElement>
    ) => {
        const itemList = items.concat();
        const index = itemList.findIndex((i) => i.id === item.id);

        itemList.splice(index, 1, { ...item, [property]: e.target.value });

        props.sdk.field.setValue(itemList);
    };

    /** Deletes an item from the list */
    const deleteItem = (item: Item) => {
        props.sdk.field.setValue(items.filter((i) => i.id !== item.id));
    };

These functions will provide our UI with the handlers necessary to take care of any user interaction. Let’s make this component return proper JSX, which will give us the ability to see our app in action.

js
   return (
        <div>
            <Table>
                <TableBody>
                    {items.map((item) => (
                        <TableRow key={item.id}>
                            <TableCell>
                                <TextField
                                    id="key"
                                    name="key"
                                    labelText="Item Name"
                                    value={item.key}
                                    onChange={createOnChangeHandler(item, 'key')}
                                />
                            </TableCell>
                            <TableCell>
                                <TextField
                                    id="value"
                                    name="value"
                                    labelText={valueName}
                                    value={item.value}
                                    onChange={createOnChangeHandler(item, 'value')}
                                />
                            </TableCell>
                            <TableCell align="right">
                                <EditorToolbarButton
                                    label="delete"
                                    icon="Delete"
                                    onClick={() => deleteItem(item)}
                                />
                            </TableCell>
                        </TableRow>
                    ))}
                </TableBody>
            </Table>
            <Button
                buttonType="naked"
                onClick={addNewItem}
                icon="PlusCircle"
                style={{ marginTop: tokens.spacingS }}
            >
                Add Item
            </Button>
        </div>
    );
};

export default Field;

I’m using Contentful’s Forma 36 Table and Button components to build out the UI and create a cohesive experience with the other built-in fields. I like this approach because it ensures the UI doesn’t seem disjointed and users who are already familiar with Contentful will already feel comfortable using these UI elements.

While our app does take advantage of hot reloading, I always like to refresh the page to ensure all our work has taken effect. After refreshing, we should see the app in action. If you are having trouble seeing this app work or you think something may have missed a step while following the above directions, don’t worry.You can view the full source to see how it works.

Going beyond

While I think many people may benefit from the general Repeater app which is free to install on our marketplace, you can always fork the repo to start your own customization. One cool idea that was proposed and implemented by a member of our community, Martin Schön, is a Repeater app using reference fields instead of JSON. The Reference Matrix Field App is open source and also available for further tinkering. But many options are available. Stitching together UI components from Forma 36 and making use of the different app locations could unlock more ways for you to create a powerful repeater app.

That said, it is always interesting to see the different ways developers have come together and built their own components for the UI/UX they envision for their users. Many developers make use of our Slack Community where help and ideas are easily shared. I’m active on the channel and always happy to help explore ideas or guide other developers through the app-creation process. If you’d like to join our community, you can take advantage of some of the cool things we are doing and discussing over there.

About the author

Don't miss the latest

Get updates in your inbox
Discover new insights from the Contentful developer community each month.
add-circle arrow-right remove style-two-pin-marker subtract-circle