26. Volto View Components: A Listing View for Talks

To be solved task in this part:

  • Create a view that shows a list of talks

In this part you will:

  • Create the view component

  • Register a react view component for folderish content types

  • Use an existing endpoint of Plone REST API to fetch content

  • Display the fetched data

Volto has has a default view for content type Document. The talk list should list all talks in this folderish page and also show information about the dates, the locations and the speakers. We will create an additonal view for the content type Document.

26.1. Register the view

Create a new file src/components/Views/TalkList.jsx.

As a first step, the file will hold a simple view component.

import React from 'react';

const TalkListView = (props) => {
  return <div>I am the TalkList component!</div>;
};

export default TalkListView;

As a convention we provide the view from src/components/index.js.

import TalkView from './Views/Talk';
import TalkListView from './Views/TalkList';

export { TalkView, TalkListView };

Now register the new component as a view for folderish types in src/config.js.

import { TalkListView, TalkView } from './components';

// All your imports required for the config here BEFORE this line
import '@plone/volto/config';

export default function applyConfig(config) {
  config.views = {
    ...config.views,
    contentTypesViews: {
      ...config.views.contentTypesViews,
      talk: TalkView,
    },
    layoutViews: {
      ...config.views.layoutViews,
      talklist_view: TalkListView,
    }
  };
  return config;
}

This extends the list of available views with the talklist_view.

To add a layout view you also have to add this new view in the ZMI of your Plone. Login to your Plone instance. Go to portal_types and select the Document-Type to add your new talklist_view to the Available view methods.

Add new View to content type Folder in the ZMI.

Add new View to content type Document in the ZMI.

Warning

This step is not in the final code for this chapter since it only changes the frontend, you need to do it manually for now. It will be added in the next chapter where you change the backend code.

The change would be in profiles/default/types/Document.xml:

 1<?xml version="1.0"?>
 2<object name="Document" meta_type="Dexterity FTI" i18n:domain="plone"
 3    xmlns:i18n="http://xml.zope.org/namespaces/i18n">
 4  <property name="filter_content_types" purge="false">False</property>
 5  <property name="view_methods" purge="false">
 6    <element value="talklist_view"/>
 7  </property>
 8  <property name="behaviors" purge="false">
 9    <element value="plone.constraintypes"/>
10  </property>
11</object>

From now on you can select the new view for a page:

../_images/talklistview_select.png

The view is available because plone.volto makes pages folderish. The layoutViews setting defines views for folderish content types.

Now we will improve this view step by step.

The following code displays title and description of the page. The data is provided via component props: props.content.

import React from 'react';
import { Container } from 'semantic-ui-react';

const TalkListView = ({content}) => {
  return (
    <Container id="view-wrapper talklist-view">
      <article id="content">
        <header>
          <h1 className="documentFirstHeading">{content.title}</h1>
          {content.description && (
            <p className="documentDescription">{content.description}</p>
          )}
        </header>
        <section id="content-core">
          <div>list of talks</div>
        </section>
      </article>
    </Container>
  )
};
export default TalkListView;

26.2. Using the search endpoint

To get a list of all talks, no matter where they are in our site, we will use the search endpoint of the Plone REST API. That is the equivalent of using a catalog search in Plone Classic. See Using the catalog.

 1import React from 'react';
 2import { Container, Segment } from 'semantic-ui-react';
 3import { searchContent } from '@plone/volto/actions';
 4import { useDispatch, useSelector } from 'react-redux';
 5
 6const TalkListView = ({ content }) => {
 7  const talks = useSelector(
 8    (state) => state.search.subrequests.conferencetalks?.items,
 9  );
10  const dispatch = useDispatch();
11
12  React.useEffect(() => {
13    dispatch(
14      searchContent(
15        '/',
16        {
17          portal_type: ['talk'],
18          fullobjects: true,
19        },
20        'conferencetalks',
21      ),
22    );
23  }, [dispatch]);
24
25  return (
26    <Container id="view-wrapper talklist-view">
27      <article id="content">
28        <header>
29          <h1 className="documentFirstHeading">{content.title}</h1>
30          {content.description && (
31            <p className="documentDescription">{content.description}</p>
32          )}
33        </header>
34        <section id="content-core">
35          {talks &&
36            talks.map((item) => (
37              <Segment padded clearing key={item.id}>
38                <h2>{item.title}</h2>
39                <p>{item.description}</p>
40              </Segment>
41            ))}
42        </section>
43      </article>
44    </Container>
45  );
46};
47
48export default TalkListView;

We make use of the useSelector and useDispatch hooks from the React Redux library. They are used to subscribe our component to the store changes (useSelector) and for issuing Redux actions (useDispatch).

The Redux action searchContent will do the search for us: It fetches the data and stores the result in the global app store. The component gets access to the search results via the subscription to the store.

The search itself will be defined in the React.useEffect(() => {})- section of the code and will contain all parameters for the search. In case of the talks listing view, we search for all objects of type talk with portal_type:['talk'] and force to fetch all information with fullobjects: true.

We see now a list of all talks.

simple talk list view

Next step is to enrich the view with details about the individual talks. We can take code from the talk view.

 1import React from 'react';
 2import { Link } from 'react-router-dom';
 3import { Container, Image, Label, Segment } from 'semantic-ui-react';
 4import { searchContent } from '@plone/volto/actions';
 5import { useDispatch, useSelector } from 'react-redux';
 6import { flattenToAppURL } from '@plone/volto/helpers';
 7
 8const TalkListView = ({ content }) => {
 9  const talks = useSelector(
10    (state) => state.search.subrequests.conferencetalks?.items,
11  );
12  const dispatch = useDispatch();
13
14  React.useEffect(() => {
15    dispatch(
16      searchContent(
17        '/',
18        {
19          portal_type: ['talk'],
20          fullobjects: true,
21        },
22        'conferencetalks',
23      ),
24    );
25  }, [dispatch]);
26
27  const color_mapping = {
28    Beginner: 'green',
29    Advanced: 'yellow',
30    Professional: 'red',
31  };
32
33  return (
34    <Container id="view-wrapper talklist-view">
35      <article id="content">
36        <header>
37          <h1 className="documentFirstHeading">{content.title}</h1>
38          {content.description && (
39            <p className="documentDescription">{content.description}</p>
40          )}
41        </header>
42        <section id="content-core">
43          {talks &&
44            talks.map((item) => (
45              <Segment padded clearing key={item.id}>
46                <h2>
47                  <Link to={item['@id']} title={item['@type']}>
48                    {item.type_of_talk?.title || item.type_of_talk?.token}:{' '}
49                    {item.title}
50                  </Link>
51                </h2>
52                <div>
53                  {item.audience.map((item) => {
54                    let audience = item.title || item.token;
55                    let color = color_mapping[audience] || 'green';
56                    return (
57                      <Label key={audience} color={color} tag>
58                        {audience}
59                      </Label>
60                    );
61                  })}
62                </div>
63                {item.image && (
64                  <Image
65                    src={flattenToAppURL(item.image.scales.preview.download)}
66                    size="small"
67                    floated="right"
68                    alt={content.image_caption}
69                    avatar
70                  />
71                )}
72                <p>{item.description}</p>
73              </Segment>
74            ))}
75        </section>
76      </article>
77    </Container>
78  );
79};
80
81export default TalkListView;
simple talk list view

26.3. Search options

  • The default representation for search results is a summary that contains only the most basic information like title, review state, type, path and description.

  • With the option fullobjects all available field values are present in the fetched data.

  • Another option is metadata_fields, which allows to get more attributes (selection of Plone catalog metadata columns) than the default search without a performance expensive fetch via option fullobjects.

Possible sort criteria are indices of the Plone catalog.

See also

  • Plone REST API documentation <https://plonerestapi.readthedocs.io/en/latest/searching.html>_

  • Plone documentation about searching and indexing <https://docs.plone.org/develop/plone/searching_and_indexing/query.html>_

26.4. Exercises

26.4.1. Exercise 1

Modify the criteria in the search to sort the talks in the order of their modification date.

Solution

 1React.useEffect(() => {
 2  dispatch(
 3    searchContent('/', {
 4      portal_type: ['talk'],
 5      sort_on: 'modified',
 6      fullobjects: true,
 7    },
 8    'conferencetalks',
 9  ),
10  );
11}, [dispatch]);

26.4.2. Exercise 2

Change TalkListView to show the keynote speakers (name, biography and foto) and with a link to their keynote. Remember that you cannot search for a specific value in type_of_talk yet so you'll have to filter the results.

For bonus points create and register it as a separate view Keynotes

Solution

Write the view:

 1import React from 'react';
 2import { Container, Segment, Image } from 'semantic-ui-react';
 3import { Helmet } from '@plone/volto/helpers';
 4import { Link } from 'react-router-dom';
 5import { flattenToAppURL } from '@plone/volto/helpers';
 6import { searchContent } from '@plone/volto/actions';
 7import { useDispatch, useSelector } from 'react-redux';
 8
 9const TalkListView = ({ content }) => {
10    const talks = useSelector(
11      (state) => state.search.subrequests.conferencetalks?.items,
12    );
13  const dispatch = useDispatch();
14
15  React.useEffect(() => {
16    dispatch(
17      searchContent('/', {
18        portal_type: ['talk'],
19        review_state: 'published',
20        fullobjects: true,
21      },
22      'conferencetalks',
23    ),
24    );
25  }, [dispatch]);
26
27  return (
28    <Container className="view-wrapper">
29      <Helmet title={content.title} />
30      <article id="content">
31        <header>
32          <h1 className="documentFirstHeading">Our Keynote Speakers</h1>
33        </header>
34        <section id="content-core">
35          {talks &&
36            talks.map(
37              item =>
38                item.type_of_talk.title === 'Keynote' && (
39                  <Segment padded>
40                    <h2>{item.speaker}</h2>
41                    {item.image && (
42                      <Image
43                        src={flattenToAppURL(
44                          item.image.scales.preview.download,
45                        )}
46                        size="medium"
47                        centered
48                        alt={item.speaker}
49                      />
50                    )}
51                    {item.speaker_biography && (
52                      <div
53                        dangerouslySetInnerHTML={{
54                          __html: item.speaker_biography.data,
55                        }}
56                      />
57                    )}
58                    <h3>
59                      Keynote:{' '}
60                      <Link to={item['@id']} title={item['@type']}>
61                        {item.title}
62                      </Link>
63                    </h3>
64                  </Segment>
65                ),
66            )}
67        </section>
68      </article>
69    </Container>
70  );
71};
72export default TalkListView;

Note

  • The query uses review_state: 'published'

  • Filtering is done using item.type_of_talk.title === 'Keynote' && (... during the iteration.

To regster it move the code to new frontend/src/components/Views/Keynotes.jsx and rename it to KeynotesView:

const KeynotesView = props => {
  [...]
}

export default KeynotesView;

Export it in frontend/src/components/index.js:

import TalkView from './Views/Talk';
import TalkListView from './Views/TalkList';
import KeynotesView from './Views/Keynotes';

export { TalkView, TalkListView, KeynotesView };

Register the component as layout view for folderish types in frontend/src/config.js.

import { TalkListView, TalkView, KeynotesView } from './components';

[...]

config.views = {
  ...config.views,
  layoutViews: {
    ...config.views.layoutViews,
    talklist_view: TalkListView,
    keynotes_view: KeynotesView,
  },
  contentTypesViews: {
    ...config.views.contentTypesViews,
    talk: TalkView,
  },
};