How to use UI-extensions to improve everyday authoring experience

Building products that serve everybody is hard. This is especially true in the field of content management. Different content is best handled with different interfaces.

At Contentful we build a very flexible product which can be used for almost everything. Product feature always have to serve a broad audience. To help with specific needs we offer UI-extensions which help to tailor the authoring experience to exactly to your needs.

In this blog post, I want to show you how I extend the Contentful interface with my own custom UI elements to make my life as an editor and content creator a little easier.

Quick fixes to improve editors experience

The way it works is that thanks to Contentful's UI-Extensions feature you can upload HTML files to Contentful which will be displayed in an iframe inside of the web application.

In the context of this iframe, you then can use the UI-Extensions SDK to communicate with the outer web application. The SDK offers all the functionality to build rich interfaces.

And to make your UI components look similar to the other components (and to not annoy people with missing visual consistency) you can include a stylesheet providing you some 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>

These HTML files then build in combination with a configuration extension.json file a Contentful UI extension.

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 intragtions with third party services are easily done and you can tweak the web interface like you need it.

UI-Extensions are "just" HTML files – you can basically build whatever you like with them.

And that's exactly what I do regularly…

A custom language select

In my job, I’m attending a few events and I’m listing these events on my own personal website. The underlying event content model is fairly simple. It includes fields for a date, a title but also a city and a depending country.

These fields are the base for an event listing on my site which includes the given country flag followed by the city.

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

The flags use Unicode/Emoji flag symbols. These symbols are a combination of two letters representing the given country.

My problem was that I don’t always know the country code of the country I’m going to and the one step to google the code of a country annoyed me quickly. That wasn't a good experience.

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

So let's quickly have a look at the UI-extension code it takes to realize something like that 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 a simple text value does the job. To make this UI-extension available for simple text fields I have to define Symbol in the fieldTypes property. You find a list of all the field types in the 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 functionality of the code above is pretty straight forward. The key part is the custom select HTML element which I found online somewhere. It maps between the values of country codes and human readable country name.

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

Example of the country select

So, if you need a country select in your projects you can head over to the GitHub repository and will find installation instructions there.

Editable tabular data

Once at our Berlin User Meetup, a woman came to me and we chatted about the way she uses Contentful. She explained me some workarounds they did to work with tabular data. Her company was publishing articles with statistics included and this meant that they copied a lot of tables around.

According to her description the tables we're not really complex but the field types provided by Contentful simply didn't 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 looks now quite similar to the first example but there is the key difference that this UI-Extension should be available for the field types of Object. Object means that I can store any valid JSON object in Contentful.

When you think of that, this means, that you really have all the freedom to build interfaces you need. You can define the data structure and you can define how this data structure should be edited. That's really cool!

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 for the future because it might be that I want to add more things to this field later.

The table itself is a two-dimensional Array which includes the table header as the first entry.

The editor interface then should display this data in an editable table and here's the needed code for that:

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>

So what is going on in there:

  • it initializes the UI-Extension
  • it gets the current field value from the Contentful web app
    • if there is no data stored yet, it creates the data structure
  • it creates the DOM table structure (inserting tr and td into the table element
  • it adds a global event listener to save new values on blur

A cool thing I really enjoy is, that UI-Extensions are encapsulated and you don't have to worry about clashes 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 all!

Example of editable tabular data

Looking at this example I hope you see the power of this! You can basically build whatever you like with a UI-Extension.

If you want to play around with this example yourself you can head over to the GitHub project.

Quick selects for common references

The reference feature is one of the Contentful's features I like the most because it allows me to build whole content trees that I can fetch in a single request.

For me, the provided reference editor works perfectly fine but for e.g. people that set a lot of similar references I totally see that the interface could be improved to let authors set references easier and quicker. Dave Olsen had the same thought and asked me if it's possible to build different reference editor that offers a quick select. It sure is.

Taking his project as an example: it deals with an educational system and in his content model he wanted to quick reference to a certain college institution. Let's have a look at what is needed to build a quick select for references:

  • fetch all entries of a certain 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

The "quick reference" example is a bit more complex so let's go through it step by step.

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"
}

As we're dealing with references here this UI-Extension should only be available for reference fields so the fieldTypes only includes "Entry".

Fetch all entries of a certain/valid type

I could now hardcode all the content types I want to show the entries of but there is also a way to make it more flexible. When setting up a reference field it's possible to define validations to only allow certain content types.

Reference field validations

With the information of 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 is executed with an extension object. This object also holds information on the current active validations (extension.field.validations).

There can be several included validation types like e.g. the required validation. Validations for a particular content type include the linkContentType property, so that I can check for this property to figure out the validation type and concat all the allowed content types later on.

Next thing is to fetch the entries for these content types. As the content types are stored in an Array already 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.

To make a request the 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), []);
  });

Render the UI

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

Next step is to render the UI for these items. This step can be solved similar to the other examples. Firstly it's important to know if there is a field reference value stored already. If that's the case 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('');

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 changes its value this value is sent to the outer web app and 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 one click. The example you see below shows a way to quickly select authors for the blog content model that's included in the interactive getting started guide.

And just because the this UI-Extension is built in a general way it is also able to 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 save your content creators some time

Productivity in a digital world highly depends on the tools you use. The Contentful web interface simply can't address all your needs but with UI-Extensions, we give you a perfect way to customize the Contentful web app to fit your and your editors needs.

If an editing workflow is too complicated or could be done in one step instead of two, go and improve the interface… and believe me, people will thank you for that.

Blog posts in your inbox

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