23. Volto View Components: A Default View for “Talk”

To be solved task in this part:

  • Create a view to display talks in a nice way

In this part you will:

  • Register a react view component for talks

  • Write the component

Topics covered:

  • Views

  • Displaying data stored in fields of content types

  • React Basics

In Volto the default visualization for your new content type “talk” only shows the title, description and the image.

This paragraph might be rendered in a custom way.

Note

In Plone the default view iterates over all fields in your schema and displays the stored data. In Volto this feature is not implemented yet.

Since we want to show the data we need to write a custom view for talks that is used in Volto.

In the folder frontend you need to add a new file src/components/Views/Talk.jsx (create the folder Views first.)

As a first step the file will hold a placeholder only:

import React from 'react';

const TalkView = props => {
  return <div>I'm the TalkView component!</div>;
};

export default TalkView;

Also add a convenience-import of the new component to src/components/index.js:

import TalkView from './Views/Talk';

export { TalkView };

This is is a common practice and allows us to import the new view as import { TalkView } from './components'; instead of import { TalkView } from './components/Views/Talk';.

Now register the new component as default view for talks in src/config.js.

import { TalkView } from './components';

[...]

config.views = {
  ...config.views,
  contentTypesViews: {
    ...config.views.contentTypesViews,
    talk: TalkView,
  },
};
  • This extends the Volto default setting config.views.contentTypesViews with the key/value pair talk: TalkView.

  • It uses the spread syntax.

When Volto is running (with yarn start) it picks up these changes and displays the placeholder in place of the previously used default-view.

Now we will improve this view step by step. First we reuse the component DefaultView.jsx in our custom view:

import React from 'react';
import { DefaultView } from '@plone/volto/components';

const TalkView = props => {
  return <DefaultView {...props} />;
};
export default TalkView;

We will now add the content from the field details after the DefaultView.

import React from 'react';
import { DefaultView } from '@plone/volto/components';

const TalkView = props => {
  const { content } = props;
  return (
    <>
      <DefaultView {...props} />
      <div dangerouslySetInnerHTML={{ __html: content.details.data }} />
    </>
  );
};
export default TalkView;

The result is not really beautiful because the text sticks to the left border of the page. You need to wrap it in a Container to get the same styling as the content of DefaultView:

import React from 'react';
import { DefaultView } from '@plone/volto/components';
import { Container } from 'semantic-ui-react';

const TalkView = props => {
  const { content } = props;
  return (
    <>
      <DefaultView {...props} />
      <Container>
        <div dangerouslySetInnerHTML={{ __html: content.details.data }} />
      </Container>
    </>
  );
};
export default TalkView;
  • Container is a component from Semantic UI React and needs to be imported before it is used.

We now decide to display the type of talk in the title (E.g. “Keynote: The Future of Plone”). This means we cannot use DefaultView anymore since that displays the title like this: <h1 className="documentFirstHeading">{content.title}</h1>. Instead we display the title and description ourselves.

This has multiple benefits:

  • All content can now be wrapped in the same Container which cleans up the html.

  • We can control where the speaker portrait is displayed. We can now move all information on the speaker into a separate box. The speaker portrait is picked up by the DefaultView because the fields name is image (same as the image from the behavior plone.leadimage).

With this changes we do discard the title-tag in the HTML head though. This will change the name occuring in the browser tab or browser head to the current site url. To use the content title instead, you’ll have to import the Helmet component, which allows to overwrite all meta tags for the HTML head like the page-title.

import React from 'react';
import { Container } from 'semantic-ui-react';
import { Helmet } from '@plone/volto/helpers';

const TalkView = props => {
  const { content } = props;
  return (
    <Container id="page-talk">
      <Helmet title={content.title} />
      <h1 className="documentFirstHeading">
        <span class="type_of_talk">{content.type_of_talk.title}: </span>
        {content.title}
      </h1>
      {content.description && (
        <p className="documentDescription">{content.description}</p>
      )}
      <div dangerouslySetInnerHTML={{ __html: content.details.data }} />
    </Container>
  );
};
export default TalkView;
  • content.type_of_talk is the json of the value from the choice field type_of_talk: {token: "training", title: "Training"}. To display it we use the title.

  • The && in {content.description && (<p>...</p>)} makes sure that this paragraph is only rendered if the talk actually has a description.

Next we add a block with info on the speaker:

import React from 'react';
import { Container, Header, Icon, Segment } from 'semantic-ui-react';

const TalkView = props => {
  const { content } = props;
  return (
    <Container id="page-talk">
      <h1 className="documentFirstHeading">
        <span class="type_of_talk">{content.type_of_talk.title} </span>
        {content.title}
      </h1>
      {content.description && (
        <p className="documentDescription">{content.description}</p>
      )}
      <div dangerouslySetInnerHTML={{ __html: content.details.data }} />
      <Segment clearing>
        {content.speaker && <Header dividing>{content.speaker}</Header>}
        <p>{content.company || content.website}</p>
        <a href={`mailto:${content.email}`}>
          <Icon name="mail" />
          {content.email}
        </a>
        {content.speaker_biography && (
          <div
            dangerouslySetInnerHTML={{
              __html: content.speaker_biography.data,
            }}
          />
        )}
      </Segment>
    </Container>
  );
};
export default TalkView;
  • We use the component Segment for the box.

  • We use the component Icon to display the mail icon.

  • {`mailto:${content.email}`} is a template literal

Next we add the image:

import React from 'react';
import { Container, Header, Icon, Image, Segment } from 'semantic-ui-react';
import { flattenToAppURL } from '@plone/volto/helpers';

const TalkView = props => {
  const { content } = props;
  return (
    <Container id="page-talk">
      <h1 className="documentFirstHeading">
        <span class="type_of_talk">{content.type_of_talk.title} </span>
        {content.title}
      </h1>
      {content.description && (
        <p className="documentDescription">{content.description}</p>
      )}
      <div dangerouslySetInnerHTML={{ __html: content.details.data }} />
      <Segment clearing>
        {content.speaker && <Header dividing>{content.speaker}</Header>}
        <p>{content.company || content.website}</p>
        <a href={`mailto:${content.email}`}>
          <Icon name="mail" />
          {content.email}
        </a>
        <Image
          src={flattenToAppURL(content.image.scales.preview.download)}
          size="small"
          floated="right"
          alt={content.image_caption}
          avatar
        />
        {content.speaker_biography && (
          <div
            dangerouslySetInnerHTML={{
              __html: content.speaker_biography.data,
            }}
          />
        )}
      </Segment>
    </Container>
  );
};
export default TalkView;

Next we add the audience:

import React from 'react';
import { Container, Header, Icon, Image, Label, Segment } from 'semantic-ui-react';
import { flattenToAppURL } from '@plone/volto/helpers';

const TalkView = props => {
  const { content } = props;
  const color_mapping = {
    Beginner: 'green',
    Advanced: 'yellow',
    Professional: 'purple',
  };

  return (
    <Container id="page-talk">
      <h1 className="documentFirstHeading">
        {content.type_of_talk.title || content.type_of_talk.token}:{' '}
        {content.title}
      </h1>
      {content.description && (
        <p className="documentDescription">{content.description}</p>
      )}
      {content.audience?.map((item) => {
        let audience = item.title || item.token;
        let color = color_mapping[audience] || 'green';
        return (
          <Label key={audience} color={color}>
            {audience}
          </Label>
        );
      })}
      <div dangerouslySetInnerHTML={{ __html: content.details.data }} />
      <Segment clearing>
        {content.speaker && <Header dividing>{content.speaker}</Header>}
        <p>{content.company || content.website}</p>
        <a href={`mailto:${content.email}`}>
          <Icon name="mail" />
          {content.email}
        </a>
        <Image
          src={flattenToAppURL(content.image.scales.preview.download)}
          size="small"
          floated="right"
          alt={content.image_caption}
          avatar
        />
        {content.speaker_biography && (
          <div
            dangerouslySetInnerHTML={{
              __html: content.speaker_biography.data,
            }}
          />
        )}
      </Segment>
    </Container>
  );
};
export default TalkView;
  • With {content.audience?.map(item => {...})} we iterate over the individual values of the field audience if that exists.

  • ?. is optional chaining

  • map is used to iterate over the array audience using a Arrow-function (=>) in which item is one item in audience.

  • The item is a object like {'title': 'Advanced', 'token': 'Advanced'}. We need to use item.title || item.token in case title is null which happens in simple fields as ours.

  • We map the values that are available in the field to colors and use blue as a fallback.

As a last step we show the last few fields website and company, github and twitter:

import React from 'react';
import { flattenToAppURL } from '@plone/volto/helpers';
import { Container, Image, Icon, Label, Segment } from 'semantic-ui-react';

const TalkView = props => {
  const { content } = props;
  const color_mapping = {
    Beginner: 'green',
    Advanced: 'yellow',
    Professional: 'purple',
  };

  return (
    <Container id="page-talk">
      <h1 className="documentFirstHeading">
        {content.type_of_talk.title || content.type_of_talk.token}:{' '}
        {content.title}
      </h1>
      {content.description && (
        <p className="documentDescription">{content.description}</p>
      )}
      {content.audience?.map((item) => {
        let audience = item.title || item.token;
        let color = color_mapping[audience] || 'green';
        return (
          <Label key={audience} color={color}>
            {audience}
          </Label>
        );
      })}
      {content.details && (
        <div dangerouslySetInnerHTML={{ __html: content.details.data }} />
      )}
      <Segment clearing>
        {content.speaker && <Header dividing>{content.speaker}</Header>}
        {content.website ? (
          <p>
            <a href={content.website}>
              {content.company || content.website}
            </a>
          </p>
        ) : (
          <p>{content.company}</p>
        )}
        {content.email && (
          <p>
            Email: <a href={`mailto:${content.email}`}>{content.email}</a>
          </p>
        )}
        {content.twitter && (
          <p>
            Twitter:{' '}
            <a href={`https://twitter.com/${content.twitter}`}>
              {content.twitter.startsWith('@')
                ? content.twitter
                : '@' + content.twitter}
            </a>
          </p>
        )}
        {content.github && (
          <p>
            Github:{' '}
            <a href={`https://github.com/${content.github}`}>
              {content.github}
            </a>
          </p>
        )}
        {content.image && (
          <Image
            src={flattenToAppURL(content.image.scales.preview.download)}
            size="small"
            floated="right"
            alt={content.image_caption}
            avatar
          />
        )}
        {content.speaker_biography && (
          <div
            dangerouslySetInnerHTML={{
              __html: content.speaker_biography.data,
            }}
          />
        )}
      </Segment>
    </Container>
  );
};
export default TalkView;