Create a Contentful App that Contains Reference Content Types to Media Assets
How many times have you heard content editors complain about having to create a whole new content entry just to use that entry in one page? This slows down the content entry process. This is where having custom Contentful Apps shines.
Apps are a great tool to enhance the content entry experience. Unfortunately, a lot of the example Content App tutorials that I found only contained fields that were simple text types. We want something more complicated. Perhaps a App that contains a collection of reference types. Let’s create that! Let’s create an App that allows you to add and remove a collection of icons. We will make sure you can add and remove icons in one seamless enjoyable user experience. This will eliminate the need for content editors to 1) create one off content entries they will only used as a reference content type in one other model and 2) to remember font-awesome codes such as pen-to-square.
First, we need to create the app. The create-contentful-app CLI makes it easy. You can read more about those steps and the CLI in the contentful documentation.
npx create-contentful-app icon-with-description
When prompted with “Do you want to start with a blank template or use one of our examples?”, select Template. I will be picking JavaScript as our template. You can choose TypeScript if you wish.
Once we have the application created, we need to create and register our app in our Contentful workspace. We do that by creating an AppDefinition. The link posted above shows how to do that via the CLI. Instead of that, let’s create our AppDefinition in our Contentful workspace.
From the main menu, let’s select Apps -> Custom Apps. From the top right, select Manage App Definitions. That will take us to where our Custom App Definitions live. Next, select Create App. Give your app a name. I will call ours Icon with Descriptions. Make sure Frontend is not Hosted by Contentful and is running on http://localhost:3000. The entry field should be JSON Object. Click Save.
Next, back in our Contentful space, let’s make sure the App is visible. Under Apps -> Custom Apps, you should see our newly created app. If not, make sure your app is properly created and registered. Once verified, let’s use our Custom App in a Content Model. From our main menu, select Content Model. If you haven’t already created a model, create one. Select + Add Field. Call it whatever you want. Under Appearance, select your newly created App. Select Confirm.
Let’s now try creating content of our newly created Content Model. From the main menu, select Content, then + Add Entry. Take a look at our Field entry for our custom app. You should see an error. That is okay. That is because Contentful Apps are single-page applications running in iFrames. Let’s fix that.
Let’s go back to our command line. Go to the directory where our app was created. Then run:
npm start
Hopefully you saw the message “Compiled Successfully!” That worked! Go back to your Contentful workspace. Edit your model you just created. Look at the field where you use your custom app. You should see something like below:
Congrats! You successfully created your first app. It’s bare bones and not very useful. Let’s change that.
Let’s open our app in our favorite IDE (Visual Studio Code for me). Open the file src/locations/Field.jsx (or .tsx if you chose TypeScript). This is a React Functional Component. To see that this is indeed working, change the text within the Paragraph component. Now look at it back in our Contentful workspace. The text should have been updated. Great!
Let’s get our field value. we will use the SDK to do that. The SDK provides you with the ability to access and manipulate data within Contentful from your app. You can learn more from the Contentful SDK documentation.
Let’s add the code to get our field value. Let’s also use a state hook to store our variable. Don’t forget to import the useState hook from react. Since what we will be creating is a collection of rows, let’s make our state an array.
const fieldValue = sdk.field.getValue();
const [rows, setRows] = useState(fieldValue ? fieldValue : []);
Our rows will consist of an icon, heading and description. I have pasted below the jsx that our react functional component will return. It uses a simple f36 Table component to display our rows and their values.
<Table>
<TableBody>
{rows.map((row, index) => {
return <TableRow key={`term${index}`}>
{
row.map((item, c) => (
<TableCell key={`col${index}${c}`}>
<div>
<Asset style={{"width": "90px", "height": "90px"}} src={item.Icon}></Asset>
<Button
variant="positive"
style={{"marginBottom" : "5px"}}
icon="Plus"
buttonType="naked"
onClick={(e) => handleImageUpload(index, c, 'Icon')}>
Select Icon
</Button>
</div>
<div>
<TextInput
style={{"marginBottom" : "5px"}}
value={item.Heading}
placeholder="Heading"
data-index={`${index},${c}`}
onChange={(e) => onChanged(e, 'Heading')}>
</TextInput>
</div>
<div>
<TextInput
style={{"marginBottom" : "5px"}}
value={item.Description}
placeholder="Enter description"
data-index={`${index},${c}`}
onChange={(e) => onChanged(e, 'Description')}>
</TextInput>
</div>
<div>
<Button
variant="negative"
style={{"marginBottom" : "5px"}}
icon="Delete"
buttonType="naked"
data-index={`${index},${c}`}
onClick={() => onDeleteButtonClicked(index, c)}>
Delete Row
</Button>
</div>
</TableCell>
))
}
</TableRow>
})}
</TableBody>
</Table>
<div style={{marginTop: '10px', marginBottom: '10px'}}>
<Button
variant='primary'
icon="Plus"
buttonType="naked"
onClick={onAddRowButtonClicked}>
Add Row
</Button>
</div>
Let’s see how we can add functionality to allow content editors to select an icon image that already exists in our media library. Once those images already exist, let’s look at the code for handleImageUpload.
We can use dialogs in our SDK. Dialogs allows us to open a UI. Let’s use the selectSingleAsset function to select a media asset. SelectSingleAsset will prompt a UI then returns a promise. Here is our code.
const handleImageUpload = async (index, c, key) => {
sdk.dialogs.selectSingleAsset({}).then((asset) => {
sdk.space.getAsset(asset.sys.id).then((assetObj) => {
const rowIndex = index;
const colIndex = c;
const updatedRows = [...rows];
const fileName = assetObj.fields.file['en-US'].url;
updatedRows[rowIndex][colIndex][key] = fileName;
setRows(updatedRows);
})
});
}
Make sure to use a react useEffect hook to update our field value whenever our rows state has changed.
useEffect(() => {
const submitRows = [...rows];
submitRows.forEach((r, c) => {
submitRows[c] = r.filter(i => !!i.Icon || !!i.Heading || !!i.Text);
});
sdk.field.setValue(submitRows.filter(r => r.length));
}, [rows, sdk.field]);
Here is how it should look in our Contentful editor:
The complete code can be found on my gitHub:
https://github.com/kirkmcpherson/contentful-app-icon-with-descriptions