Creating UI extensions with Contentful

Did you know that there’s an easy way to extend the Contentful UI? This blog post will show you how simple it is to create custom UI extensions.

Building a product that everyone likes is hard. This is especially true in the world of content management—and that’s mainly because different content types require different types of interfaces to look their best.

While we work hard to provide you with the best UI possible, we don’t always know what your use cases are. This is where UI extensions come into play.

This blog post will show you will show you how to customize the Contentful web application with custom UI elements using the UI extensions SDK. This will allow you to build interfaces that editors want to use and that are adapted to your organization's needs.

How do UI extensions work?

Custom UI extensions work thanks to the Contentful's UI extensions feature. This feature enables you to upload HTML files to Contentful which will be displayed in an iframe inside of the web application.

You can then use the UI extensions SDK within the iframe to communicate with the surrounding web application. The SDK has all of the functionality you need to build rich interfaces.

And to make your custom UI components blend in correctly, so that you don’t annoy people with visual inconsistencies, you can include a stylesheet that gives you the Contentful base styling.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8"/>
    <title>UI-Extension Country Select</title>
    <!-- load Contentful stylesheet to have some base styles -->
    <link rel="stylesheet" href="https://contentful.github.io/ui-extensions-sdk/cf-extension.css">
    <!-- load Contentful extensions SDK -->
    <script src="https://contentful.github.io/ui-extensions-sdk/cf-extension-api.js"></script>
  </head>
  <body>
    <!-- your UI extension code -->
  </body>
</html>

Then you have to define a file called extension.json which includes the configuration for your UI extension inside of the Contentful web app.

1
2
3
4
5
6
{
  "name": "Contentful UI extension",
  "id": "cf-contentful-ui-extension",
  "fieldTypes": ["Object"],
  "srcdoc": "./index.html"
}

Things like querying other APIs for autocompletions or other integrations with third-party services is painless. You can also tweak the web interface as needed.

UI-Extensions are plain HTML files—you can build whatever you want with them. And that's what I regularly do.

A custom language select

Part of my job is attending events, and I list the events I attend on my personal website. The underlying event content model is simple: it includes fields for a date, title, city, and country.

These fields make up the base for the event listing on my site, which is also configured to include the given country’s flag.

List of events including city, country flag, date, and title

The flags are emojis and they require a country code to render. My problem was that I didn’t always know the country code of where I was going to—plus having to google it every time annoyed me. It wasn’t a good user experience.

All I needed was a simple select field that maps a readable country to the given country code. This is a perfect use case for a simple UI extension tailored to my use case.

So let's have a look at the UI extension code it takes to create what we want—starting with the configuration file:

1
2
3
4
5
6
7
// extension.json
{
  "name": "Contentful UI extension country select",
  "id": "cf-contentful-ui-country-select",
  "fieldTypes": ["Symbol"],
  "srcdoc": "./index.html"
}

I only want to store a letter combination, so a text value does the job. To make this UI extension available for text fields, I have to define Symbol in the fieldTypes property. You can find a list of all the field types in our documentation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!doctype html>
<html lang="en">
  <head><!-- ... --></head>
  <body>
    <div id="content">
      <select id="countrySelect" class="cf-form-input">
        <option value="AF">Afghanistan</option>
        <option value="AX">Åland Islands</option>
        <!--      . . .     -->
        <!-- more countries -->
        <!--      . . .     -->
      </select>
    </div>
    <script type="text/javascript">
      // initialize the contentful extension
      // using the UI extensions SDK
      window.contentfulExtension.init( extension => {
        const select = document.getElementById('countrySelect');
        const value = extension.field.getValue();
        select.value = value;
        
        // send changes to the Contentful web app
        select.addEventListener( 'change', event => {
          extension.field.setValue( event.target.value );
        } );
      } );
    </script>
  </body>
</html>

The key part of the code above is the custom select HTML element, which I found somewhere online. It maps the country codes to country names.

You just need to sprinkle some JavaScript on top of it to send and save the changes to the application.

Example of the country select

If you need a country select feature for your next project, head over to the GitHub repository and you will find the installation instructions there.

Editable tabular data

A while ago at one of our Berlin User Meetups, a woman came up to me and we started chatting about how she uses Contentful. She talked about a few workarounds that her and her colleagues have to do when they work with tabular data. Because her company publishes articles that include statistics, this meant that they had to copy a lot of tables back and forth.

She said that the tables themselves were not complex—however, the field types provided by Contentful just did not fit their needs.

Field options in Contentful

She didn't know about UI extensions, though. So, how could you implement an editable table in Contentful? Let's have a look.

First thing is to set up a different configuration file:

1
2
3
4
5
6
7
// extension.json
{
  "name": "Contentful UI extension table example",
  "id": "cf-contentful-ui-table-example",
  "fieldTypes": ["Object"],
  "srcdoc": "./index.html"
}

This file is fairly similar to the first example, but there is a crucial difference. That difference is that the UI extension should be available for the field types of Object. Here Object means that I can store any valid JSON object in Contentful.

When you think about it, this implies that you have the freedom to build the interfaces you need. You can define the data structure, and you can also define how this data structure should be edited. Cool, right?

1
2
3
4
5
6
7
8
{
  tableData: [
    ['FOO', 'BAR', 'BAZ'],
    ['1', '2', '3'],
    ['4', '5', '6'],
    ['7', '8', '8']
  ]
}

In this case, I decided to go with an object that includes a tableData property. This way it stays extensible and future-proof since we might want to add more items to this field later on.

The table itself is a two-dimensional array that includes the table header as the first entry.

The editor interface should then display this data in an editable table. And the code to do that looks like so:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<!doctype html>
<html lang="en">
  <head><!-- ... --></head>
  <body>
    <table></table>
    <script>
      /**
       * setup initial table data structure
       * used when there is no data
       * stored in Contentful yet
       */
      function getInitialData({rows, header}) {
        const data = [
          header
        ];
        const columns = data[0].length;

        for (let i = 0; i <= rows; i++) {
          data.push(new Array(data[0].length));
        }

        return data;
      }
      
      /**
       * Iterate over the data and
       * create table struture for
       * the provided element
       */
      function createDOMTable(elem, tableData) {
        for (let i = 0; i < tableData.length; i++) {
          let row = elem.insertRow();

          for (let j = 0; j < tableData[0].length; j++) {
            row.insertCell().innerHTML = `<input data-row="${i}" data-column="${j}" value="${tableData[i][j] || ''}"/>`;
          }
        }
      }
      
      // initialize the Contentful extension
      // using the UI extensions SDK
      window.contentfulExtension.init(extension => {
        let value = extension.field.getValue();

        // if there is no value saved yet
        // set up the data object and save it
        if (!value) {
          value = {
            tableData : getInitialData({
              rows: 3,
              header: ['FOO', 'BAR', 'BAZ']
            })
          };
          extension.field.setValue(value);
        }
  
        createDOMTable(value.tableData);

        // add event listener to save value on blur
        window.addEventListener('blur', e => {
          value.tableData[e.target.dataset.row][e.target.dataset.column] = e.target.value;
          extension.field.setValue(value);
        }, true);
      });
    </script>
  </body>
</html>

Here’s what is going on:

  • The UI extension gets initialized.
  • The current field value from the Contentful web app is retrieved.
  • If there is no data stored yet, the data structure will be created.
  • The DOM table structure is built (by inserting tr and td into the table element).
  • A global event listener is added to save new values on blur.

Another cool aspect is that the UI extensions are encapsulated. That guarantees that you don't have to worry about clashing with other scripts or functionality. You provide the markup, you define the functionality with JavaScript and you don't have to deal with additional complexity. That's it!

Example of editable tabular data

If you want to play around with this example yourself, you can find it on GitHub.

Quick selects for common references

The reference feature is my personal favorite because it allows me to build entire content trees and fetch them with a single request.

For me, the provided reference editor gets the job done. But for other use cases I understand that the interface can be improved—particularly to give authors a more natural and quicker solution for setting references.

Dave Olsen had the same thought. He asked me if it is possible to build a different reference editor that offers a quick select. The answer: It sure is.

Some more context: he’s dealing with an educational system and in his content model, he wants to create a quick reference to a particular college institution. Here’s what is needed to build just that:

  • Fetch all the entries of a specific content type.
  • Render radio boxes for the fetched entries.
  • Check the one that represents the currently saved value.
  • Save a new value in case another radio box is checked.

Let’s break this down step by step:

1. Define the configuration

1
2
3
4
5
6
{
  "name": "Contentful UI reference radio select",
  "id": "cf-contentful-ui-ref-radio-select",
  "fieldTypes": ["Entry"],
  "srcdoc": "./index.html"
}

Note that the fieldTypes only includes "Entry" because this UI extension should only be available for reference fields.

2. Fetch all the entries of a certain/valid type

I could hard code all of the content types that I want to show entries for, but there’s a more dynamic way to do it. When setting up a reference field, it's possible to define validations to only allow certain content types.

Reference field validations

With the information about the allowed content types — I don't have to hardcode anything.

1
2
3
4
5
6
7
8
9
10
11
12
13
window.contentfulExtension.init(extension => {
  // get an array including allowed content types
  const allowedContentTypes = extension.field.validations.reduce(
    (acc, val) => {
      // content type validation check
      if (val.linkContentType) {
        acc = acc.concat( val.linkContentType )
      }
      return acc;
    }, 
    []
  );
});

The init function of a UI-Extension executes with an extension object. This object also holds information about the current active validations (extension.field.validations).

There can be several included validation types like for example the required validation. Validations for a particular content type include the linkContentType property, so that I can check for this property in order to figure out the validation type, and then concatenate all of the allowed content types later on.

The next thing is to fetch the entries for these content types. As the content types are already stored in an Array, you can map the content types to actual request promises and wrap them with a Promise.all to retrieve entries of several content types at once but how do you make requests in a UI-Extensions. It turns out that the handed in extension object includes a space property which provides a getEntries function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
window.contentfulExtension.init(extension => {
  // get an array including allowed content types
  
  const allowedContentTypes = ... ;

  // fetch entries for allowed content types
  Promise.all(
    allowedContentTypes.map(
      type => extension.space.getEntries({ 
        content_type: type
      }).then(resp => resp.items)
    )
  ).then(([...items]) => {
    // concat the arrays of different entries
    items = items.reduce((acc, items) => acc.concat(items), []);
  });

3. Render the UI

After that, all the entries are available in the items array.

The next step is to render the UI for these items. Firstly: it's important to know if there is already a field reference value stored. If that's the case then one of the radios has to have an active state.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// get current field value
// and extract reference entry ID
let currentValue = extension.field.getValue();
let currentId = currentValue && currentValue.sys.id;

// render radio button list
list.innerHTML = items.map(item => `
  <li>
    <label>
      <input name="entry" type="radio" value="${item.sys.id}" ${ item.sys.id === currentId ? 'checked' : '' }>
      ${item.fields.title['en-US']}
    </label>
  </li>
`).join('');

4. React to changes and save new references

The last thing is to react to changes and save the new values. To do that I queried all the radio elements and attached change event listeners to them.

If one of them changes its value, this value is sent to the surrounding web app and then saved using the extension.field.setValue method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[...document.querySelectorAll('input[type=radio]')].forEach( elem => {
  elem.addEventListener('change', event => {
    // save new set value
    extension.field.setValue({
      sys: {
        id: event.target.value,
        linkType: "Entry",
        type: "Link"
      }
    })
    .catch( error => {
      alert('Error: ' + JSON.strinigy(error));
    });
  });
});

With this extension, I now can quickly select references with just one click. The example you see below shows a way to swiftly select authors for the blog content model that is included in the interactive getting started guide.

And since this UI-Extension is built in a general way it will work with any content model. Maybe you want to play around with it too?

Refrence field radio buttons in action

Editor experience matters—be a good colleague and help your content creators save time

Productivity in the digital world depends on the tools you use. The Contentful web interface simply can't address all your needs out of the box—but by using UI-Extensions you can customize the Contentful web app suite to fit you and your editors needs perfectly.

Because if the editorial workflow is too complicated, or could be done in a single step instead of two, go ahead and improve the interface—people will thank you for that.

Blog posts in your inbox

Subscribe to receive most important updates. We send emails once a month.