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.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 to NewsItemView 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 or content.text.data

  • You can inspect all data that content holds using the React Developer Tools for Firefox or Chrome:

    ../_images/volto_react_devtools.png

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:

A News Item with publishing date.

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:

The customized Listing Block.

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.