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.
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.
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.
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
andtd
into thetable
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!
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.
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!
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.