Customizing Volto Components
Contents
13. Customizing Volto Components#
In this part you will:
Find out how news items are displayed
Customize existing components
Topics covered:
Customize existing views with Component Shadowing
Content Type Views
Listing Views
Blocks
13.1. Component shadowing#
We use a technique called component shadowing to override an existing Volto component with our local custom version, without having to modify Volto's source code at all.
You have to place the replacing component in the same original folder path inside the src/customizations
folder.
Every time you add a file to the customizations folder or to the theme folder, you must restart Volto for changes to take effect. From that point on, the hot reloading should kick in and reload the page automatically.
Note
Component shadowing is very much like the good old Plone technique called "JBOT" ("just a bunch of templates").
You can customize any module in Volto, including actions and reducers, not only components.
13.2. The Logo#
You can use this approach to change the Logo.
Create your own logo or download the logo from https://www.starzel.de/plone-tutorial/Logo.svg/@@download and add it to your Volto package (frontend
) using this path and name: src/customizations/components/theme/Logo/Logo.svg
.
After a restart of Volto (ctrl + c and yarn start) your page should look like this:

13.4. The News Item View#
We want to show the date a News Item is published. This way visitors can see at a glance if they are looking at current or old news.
This information is not shown by default. So you will need to customize the way that is used to render a News Item.
The Volto component to render a News Item is in omelette/src/components/theme/View/NewsItemView.jsx
(remember chapter Plone 6 Basics?).
/**
* NewsItemView view component.
* @module components/theme/View/NewsItemView
*/
import React from "react";
import PropTypes from "prop-types";
import { Container, Image } from "semantic-ui-react";
import { flattenToAppURL, flattenHTMLToAppURL } from "@plone/volto/helpers";
/**
* NewsItemView view component class.
* @function NewsItemView
* @params {object} content Content object.
* @returns {string} Markup of the component.
*/
const NewsItemView = ({ content }) => (
<Container className="view-wrapper">
{content.title && (
<h1 className="documentFirstHeading">
{content.title}
{content.subtitle && ` - ${content.subtitle}`}
</h1>
)}
{content.description && (
<p className="documentDescription">{content.description}</p>
)}
{content.image && (
<Image
className="documentImage"
alt={content.title}
title={content.title}
src={
content.image["content-type"] === "image/svg+xml"
? flattenToAppURL(content.image.download)
: flattenToAppURL(content.image.scales.mini.download)
}
floated="right"
/>
)}
{content.text && (
<div
dangerouslySetInnerHTML={{
__html: flattenHTMLToAppURL(content.text.data),
}}
/>
)}
</Container>
);
/**
* Property types.
* @property {Object} propTypes Property types.
* @static
*/
NewsItemView.propTypes = {
content: PropTypes.shape({
title: PropTypes.string,
description: PropTypes.string,
text: PropTypes.shape({
data: PropTypes.string,
}),
}).isRequired,
};
export default NewsItemView;
Note
content
is passed toNewsItemView
and represents the content item as it is serialized by the REST API.The view displays various attributes of the News Item using
content.title
,content.description
orcontent.text.data
You can inspect all data that
content
holds using the React Developer Tools for Firefox or Chrome:
Copy that file into src/customizations/components/theme/View/NewsItemView.jsx
.
After restarting Volto the new file is used when displaying a News Item. To make sure your file is used add a small change before or after the text. If it shows up you're good to go.
In you own projects you shoud always do a commit of the unchanged file and another commit after you changed the file. This way you will have a commit in your git-history with the change you made. You will thank yourself later for that clean diff!
To display the date add the following before the text:
<p>{content.created}</p>
This will render something like 2020-10-19T10:51:21
.
Not very user friendly.
Let's use one of many helpers available in React.
Import the library moment at the top of the file and use it to format the date in a readable format.
/**
* NewsItemView view component.
* @module components/theme/View/NewsItemView
*/
import React from 'react';
import PropTypes from 'prop-types';
import { Container, Image } from 'semantic-ui-react';
import moment from 'moment';
import { flattenToAppURL, flattenHTMLToAppURL } from '@plone/volto/helpers';
/**
* NewsItemView view component.
* @function NewsItemView
* @params {object} content Content object.
* @returns {string} Markup of the component.
*/
const NewsItemView = ({ content }) => (
<Container className="view-wrapper">
{content.title && (
<h1 className="documentFirstHeading">
{content.title}
{content.subtitle && ` - ${content.subtitle}`}
</h1>
)}
{content.description && (
<p className="documentDescription">{content.description}</p>
)}
{content.image && (
<Image
className="documentImage"
alt={content.title}
title={content.title}
src={
content.image['content-type'] === 'image/svg+xml'
? flattenToAppURL(content.image.download)
: flattenToAppURL(content.image.scales.mini.download)
}
floated="right"
/>
)}
<p>{moment(content.created).format('ll')}</p>
{content.text && (
<div
dangerouslySetInnerHTML={{
__html: flattenHTMLToAppURL(content.text.data),
}}
/>
)}
</Container>
);
/**
* Property types.
* @property {Object} propTypes Property types.
* @static
*/
NewsItemView.propTypes = {
content: PropTypes.shape({
title: PropTypes.string,
description: PropTypes.string,
text: PropTypes.shape({
data: PropTypes.string,
}),
}).isRequired,
};
export default NewsItemView;
The result should look like this:

Now another issue appears. There are various dates associated with any content object:
The date the item is created:
content.created
The date the item is last modified
content.modified
The date the item is published
content.effective
In fact you most likely want to show the date when the item was published. But while the item is not yet published that value is not yet set and you will get a error. So we'll add some simple logic to use the effective-date if it exists and the creation-date as a fallback.
<p className="discreet">
{moment(content.effective || content.created).format('ll')}
</p>
13.5. The Summary View#
The listing of News Items in http://localhost:3000/news does not show any dates as well.
Customize the Summary View component that exists in omelette/src/components/theme/View/SummaryView.jsx
.
Copy that file to src/customizations/components/theme/View/SummaryView.jsx
and add the following after the description:
<p className="discreet">
{moment(item.effective || item.created).format('ll')}
</p>
Don't forget to add the import of moment: import moment from 'moment';
at the top.
Note how the component iterates over the variable items
of content
with {content.items.map((item) => (...)}
. Here item
is the item in the Folder or Collection where this component is used.
13.6. The Listing Block#
When you edited the frontpage in Default content types you may have added a Listing block to the frontpage. If not do so now.
You will see that the listing block does not display the date as well.
Copy omelette/src/components/manage/Blocks/Listing/DefaultTemplate.jsx
to src/customizations/components/manage/Blocks/Listing/DefaultTemplate.jsx
and add the dates as you did with the Summary View.
1import React from 'react';
2import PropTypes from 'prop-types';
3import { ConditionalLink } from '@plone/volto/components';
4import { flattenToAppURL } from '@plone/volto/helpers';
5import moment from 'moment';
6
7import { isInternalURL } from '@plone/volto/helpers/Url/Url';
8
9const DefaultTemplate = ({ items, linkMore, isEditMode }) => {
10 let link = null;
11 let href = linkMore?.href || '';
12
13 if (isInternalURL(href)) {
14 link = (
15 <ConditionalLink to={flattenToAppURL(href)} condition={!isEditMode}>
16 {linkMore?.title || href}
17 </ConditionalLink>
18 );
19 } else if (href) {
20 link = <a href={href}>{linkMore?.title || href}</a>;
21 }
22
23 return (
24 <>
25 <div className="items">
26 {items.map((item) => (
27 <div className="listing-item" key={item['@id']}>
28 <ConditionalLink item={item} condition={!isEditMode}>
29 <div className="listing-body">
30 <h4>{item.title ? item.title : item.id}</h4>
31 <p>
32 {moment(item.effective || item.created).format('ll')}
33 </p>
34 <p>{item.description}</p>
35 </div>
36 </ConditionalLink>
37 </div>
38 ))}
39 </div>
40
41 {link && <div className="footer">{link}</div>}
42 </>
43 );
44};
45DefaultTemplate.propTypes = {
46 items: PropTypes.arrayOf(PropTypes.any).isRequired,
47 linkMore: PropTypes.any,
48 isEditMode: PropTypes.bool,
49};
50export default DefaultTemplate;
The result should look like this:

13.7. Localization#
The result is fine if you have an english-speaking website but for other locales you want to configure moment
to use your locale.
You could set it by hand with moment.locale('fr');
(for french) but the code for this application should work with any language.
NewsItemView
contains no code but directly returns the container.
You need to make a small change to allow setting the locale here.
Wrap the Container with {}
and return the container.
Put the locale-setting before it.
1/**
2 * NewsItemView view component.
3 * @module components/theme/View/NewsItemView
4 */
5
6import React from 'react';
7import PropTypes from 'prop-types';
8import { Container, Image } from 'semantic-ui-react';
9import moment from 'moment';
10import { useIntl } from 'react-intl';
11
12import { flattenToAppURL, flattenHTMLToAppURL } from '@plone/volto/helpers';
13
14/**
15 * NewsItemView view component.
16 * @function NewsItemView
17 * @params {object} content Content object.
18 * @returns {string} Markup of the component.
19 */
20
21const NewsItemView = ({ content }) => {
22 const intl = useIntl();
23 moment.locale(intl.locale);
24
25 return (
26 <Container className="view-wrapper">
27 {content.title && (
28 <h1 className="documentFirstHeading">
29 {content.title}
30 {content.subtitle && ` - ${content.subtitle}`}
31 </h1>
32 )}
33 {content.description && (
34 <p className="documentDescription">{content.description}</p>
35 )}
36 {content.image && (
37 <Image
38 className="documentImage"
39 alt={content.title}
40 title={content.title}
41 src={
42 content.image['content-type'] === 'image/svg+xml'
43 ? flattenToAppURL(content.image.download)
44 : flattenToAppURL(content.image.scales.mini.download)
45 }
46 floated="right"
47 />
48 )}
49 <p className="discreet">
50 {moment(content.effective || content.created).format('ll')}
51 </p>
52 {content.text && (
53 <div
54 dangerouslySetInnerHTML={{
55 __html: flattenHTMLToAppURL(content.text.data),
56 }}
57 />
58 )}
59 </Container>
60 );
61};
62
63/**
64 * Property types.
65 * @property {Object} propTypes Property types.
66 * @static
67 */
68NewsItemView.propTypes = {
69 content: PropTypes.shape({
70 title: PropTypes.string,
71 description: PropTypes.string,
72 text: PropTypes.shape({
73 data: PropTypes.string,
74 }),
75 }).isRequired,
76};
77
78export default NewsItemView;
You can now do the same changes for the Summary View and the Listing Block. The Listing Block alread has some code in it so you would not need to wrap it in {}
and add the return ()
statement.
13.8. Summary#
Component shadowing allows you to modify, extend and customize views in Volto.
It is a powerful feature for making changes without the need for complex configuration or maintaining a fork of the code.
You need to restart Volto when you add a new override.