36. The Sponsors Component

In the previous chapter Dexterity Types III: Sponsors you created the sponsor content type. Now let's learn how to display content of this type.

To be solved task in this part:

  • Advert to sponsors on all pages, sorted by level

In this part you will:

  • Display data from fetched content

Topics covered:

  • Create React component

  • Use React action of Volto to fetch data from Plone via REST API

  • Style component with Semantic UI

Sponsors component

For sponsors we will stay with the default view as we will only display the sponsors in the footer and do not modify their own pages. The default-view of Volto does not show any of the custom fields you added to the sponsors. Using what you learned in Volto View Component: A Default View for a "Talk" you should be able to write a view for sponsors if you wanted to.

36.1. A React component

React components let you split the UI into independent, reusable pieces, and think about each piece in isolation.

  • You can write a view component for the current context - like the TalkView.

  • You can write a view component that can be selected as view for a set of content objects like the TalkListView.

  • You can also write components that are visible on all content objects. In classic Plone we use viewlets for that.

  • Volto comes with several components like header, footer, sidebar. In fact everything of the UI is build of nested components.

  • Inspect existing components with the React Developer Tools.

36.2. The Sponsors Component

We will now see how to achieve in Volto frontend the equivalent to the Plone viewlet of chapter Dexterity Types III: Sponsors.

Steps to take

  • Copy and customize Footer component.

  • Create component to fetch data from backend and to display the fetched data.

36.2.2. Getting the sponsors data

With our Sponsors component in place we can take the next step and explore Volto some more to figure out how it does data fetching.

As the data is in the backend, we need to find a way to address it. Volto provides various predefined actions to communicate with the backend (fetching data, creating content, editing content, etc.). A Redux action communicates with the backend and has a common pattern: It addresses the backend via REST API and updates the global app store according to the response of the backend. A component calls an action and has hereupon access to the global app store (shortened: store) with the fetched data.

For more information which actions are already provided by Volto have a look at frontend/omelette/src/actions.

Our component will use the action searchContent to fetch data of all sponsors. It takes as arguments the path where to search, the information what to search and an argument with which key the data should be stored in the store. Remember: the result is stored in the global app store.

So if we call the action searchContent to fetch data of sponsors, that means data of the instances of content type sponsor, then we can access this data from the store.

The Hook useEffect lets you perform side effects in function components. We use it to fetch the sponsors data from the backend.

 1const dispatch = useDispatch();
 2
 3React.useEffect(() => {
 4  dispatch(
 5    searchContent(
 6      '/',
 7      {
 8        portal_type: ['sponsor'],
 9        review_state: 'published',
10        fullobjects: true,
11      },
12      'sponsors',
13    ),
14  );
15}, [dispatch]);

36.2.3. Connection of component and store

Let's connect the store to our component. The Selector Hook useSelector allows a function component to connect to the store.

It's worth exploring the store of our app with the Redux Dev Tools which are additional Dev Tools to React Dev Tools. There you can see what is stored in state.search.subrequests.sponsors. And you can walk through time and watch how the store is changing.

1const sponsors = useSelector((state) =>
2  groupedSponsorsByLevel(state.search.subrequests.sponsors?.items),
3);

With these both: dispatching the action and a connection to the state in place, the component can call the predefined action searchContent and has access to the fetched data via its constant sponsors.

The next step is advanced and can be skipped on a first reading. As by now we fetch the sponsors data on mounting event of the component. The mounting is done once on the first visit of a page of our app. What if a new sponsor is added or a sponsor is published? We want to achieve a re-rendering of the component on changed sponsorship. To subscribe to these changes in sponsorship, we extend our already defined connection.

 1const content = useSelector((state) => state.workflow.transition);
 2
 3React.useEffect(() => {
 4  dispatch(
 5    searchContent(
 6      '/',
 7      {
 8        portal_type: ['sponsor'],
 9        review_state: 'published',
10        fullobjects: true,
11      },
12      'sponsors',
13    ),
14  );
15}, [dispatch, content]);

Listening to this subscription the component fetches the data from the store if a workflow state changes.

36.2.4. Presentation of the prepared data

With the data fetched and accessible in the component constant sponsors we can now render the sponsors data. As we have already prepared a dictionary by sponsor level of the list of sponsors, groupedSponsorsByLevel, we can now show a nested list.

 1<List>
 2  {keys(sponsors).map((level) => {
 3    return (
 4      <List.Item key={level} className={'sponsorlevel ' + level}>
 5        <h3>{level.toUpperCase()}</h3>
 6        <List horizontal>
 7          {sponsors[level].map((item) => (
 8            <List.Item key={item['@id']} className="sponsor">
 9              {item.logo ? (
10                <Image
11                  className="logo"
12                  as="a"
13                  href={item.url}
14                  target="_blank"
15                  src={flattenToAppURL(item.logo.scales.preview.download)}
16                  size="small"
17                  alt={item.title}
18                  title={item.title}
19                />
20              ) : (
21                <a href={item['@id']}>{item.title}</a>
22              )}
23            </List.Item>
24          ))}
25        </List>
26      </List.Item>
27    );
28  })}
29</List>

Complete code of the Sponsors component

 1import React from 'react';
 2import { useDispatch, useSelector } from 'react-redux';
 3import { Segment, List, Image } from 'semantic-ui-react';
 4import { keys, isEmpty } from 'lodash';
 5
 6import { flattenToAppURL } from '@plone/volto/helpers';
 7import { searchContent } from '@plone/volto/actions';
 8
 9const groupedSponsorsByLevel = (array = []) =>
10  array.reduce((obj, item) => {
11    let token = item.level?.token || 'bronze';
12    obj[token] ? obj[token].push(item) : (obj[token] = [item]);
13    return obj;
14  }, {});
15
16const Sponsors = () => {
17  const dispatch = useDispatch();
18  const sponsors = useSelector((state) =>
19    groupedSponsorsByLevel(state.search.subrequests.sponsors?.items),
20  );
21
22  const content = useSelector((state) => state.workflow.transition);
23
24  React.useEffect(() => {
25    dispatch(
26      searchContent(
27        '/',
28        {
29          portal_type: ['sponsor'],
30          review_state: 'published',
31          fullobjects: true,
32        },
33        'sponsors',
34      ),
35    );
36  }, [dispatch, content]);
37
38  return !isEmpty(sponsors) ? (
39    <Segment basic textAlign="center" className="sponsors" inverted>
40      <div className="sponsorheader">
41        <h3 className="subheadline">SPONSORS</h3>
42      </div>
43      <List>
44        {keys(sponsors).map((level) => {
45          return (
46            <List.Item key={level} className={'sponsorlevel ' + level}>
47              <h3>{level.toUpperCase()}</h3>
48              <List horizontal>
49                {sponsors[level].map((item) => (
50                  <List.Item key={item['@id']} className="sponsor">
51                    {item.logo ? (
52                      <Image
53                        className="logo"
54                        src={flattenToAppURL(item.logo.scales.preview.download)}
55                        size="small"
56                        alt={item.title}
57                        title={item.title}
58                      />
59                    ) : (
60                      <span>{item.title}</span>
61                    )}
62                  </List.Item>
63                ))}
64              </List>
65            </List.Item>
66          );
67        })}
68      </List>
69    </Segment>
70  ) : (
71    <></>
72  );
73};
74
75export default Sponsors;

We group the sponsors by sponsorship level.

An Object sponsors using the sponsorship level as key helps to build rows with sponsors by sponsorship level.

The Semantic UI compontent Image is used to display the logo. It cares about the markup of an html image node with all necessary attributes in place.

We also benefit from Semantic UI component List to build our list of sponsors. The styling can be customized but these predefined components help simplifying the code and achieve an app wide harmonic style.

See also

Chapter Semantic UI

See the new footer. A restart is not necessary as we didn't add a new file. The browser updates automagically by configuration.

Sponsors component

36.3. Exercise

Modify the component to display a sponsor logo as a link to the sponsors website. The address is set in sponsor field "url". See the documentation of Semantic UI React.

Solution

<Image
  className="logo"
  as="a"
  href={item.url}
  target="_blank"
  src={flattenToAppURL(item.logo.scales.preview.download)}
  size="small"
  alt={item.title}
  title={item.title}
/>

The Semantic Image component is now rendered with a wrapping anchor tag.

<a
  target="_blank"
  title="Gold Sponsor Violetta Systems"
  class="ui small image logo"
  href="https://www.nzz.ch">
    <img
      src="/sponsors/violetta-systems/@@images/d1db77a4-448d-4df3-af5a-bc944c182094.png"
      alt="Violetta Systems">
</a>

36.4. Summary

You know how to fetch data from backend. With the data you are able to create a component displayed at any place in the website.