Skip to main content

Component based design with Paragraphs and Field formatters

A common problem that I’ve faced, particularly in the last few years, is how to deliver the complex, component driven design that clients want while also giving content authors full flexibility with those components without creating an un-maintainable, or brittle product.

by adam.bramley /

I’m sure you’ve been there, clients want maximum control over content curation and page layout without compromising on the authoring experience. There’s usually many components that look similar but may display a different combination of fields, or at a different column width. And each of those components can display on different page layouts, and in different regions.

On a recent project we used a combination of ParagraphsClassy Paragraphs, and custom field formatters to achieve just that.

The problem

Simplifying what I’ve said above, how can we deliver a design like this:

Without locking down the order of components, number of components, or type of components on that page? How do we allow content authors to curate the page in its entirety? And let them change it at a whim?

The solution

Our solution combined backend and frontend techniques to deliver a rich content experience that was easily maintainable and gave full control to the authors. No hardcoded templates or layouts for a content type. No custom fields for each element on the page. Just fully reusable components.

The backend

Each content type had a Entity Reference Revisions field called Page body that was shared across all content types. This field was used as the heart of our page creation. We could then select which components were available on each content type to ensure authors couldn’t go too crazy!

Each paragraph type was used to provide one or more components for the design. This could be a simple WYSIWYG text area, a list of Featured content, or a promo card. All paragraphs had the Layout style field which is provided by the Paragraphs Layouts module (for more information read Rikki’s blog on the module) which is used to govern how many columns the particular component takes up.

We then extended this idea further with a Display field for components that were similar but contained different combinations of fields. For example, a list of content references showing just a link to the referenced page, or a list of content references display the title link and summary text pulled from a field on the content itself.

Display field was used in custom field formatters to determine which fields (or sometimes view modes) should be displayed on the parent page. This helped to alleviate the number of paragraph types we needed by being able to combine similar displays and functionality into a single bundle. Example formatter settings and view functions in our ContactCardFormatter:

  /**
   * {@inheritdoc}
   */
  public function settingsForm(array $form, FormStateInterface $form_state) {
    $elements['view_mode_normal'] = [
      '#type' => 'select',
      '#options' => $this->entityDisplayRepository->getViewModeOptions($this->getFieldSetting('target_type')),
      '#title' => $this->t('View mode normal'),
      '#default_value' => $this->getSetting('view_mode_normal'),
      '#required' => TRUE,
    ];
    $elements['view_mode_extended'] = [
      '#type' => 'select',
      '#options' => $this->entityDisplayRepository->getViewModeOptions($this->getFieldSetting('target_type')),
      '#title' => $this->t('View mode extended'),
      '#default_value' => $this->getSetting('view_mode_extended'),
      '#required' => TRUE,
    ];

    return $elements;
  }

  /**
   * {@inheritdoc}
   */
  public function settingsSummary() {
    $summary = [];

    $view_modes = $this->entityDisplayRepository->getViewModeOptions($this->getFieldSetting('target_type'));
    $view_mode = $this->getSetting('view_mode_normal');
    $summary[] = $this->t('Normal display rendered as @mode', ['@mode' => isset($view_modes[$view_mode]) ? $view_modes[$view_mode] : $view_mode]);
    $view_mode = $this->getSetting('view_mode_extended');
    $summary[] = $this->t('Extended display rendered as @mode', ['@mode' => isset($view_modes[$view_mode]) ? $view_modes[$view_mode] : $view_mode]);

    return $summary;
  }

  /**
   * {@inheritdoc}
   */
  public function viewElements(FieldItemListInterface $items, $langcode) {
    $elements = [];
    $host_entity = $items->getParent()->getValue();
    $display = $host_entity->get('field_display')->value;
    $view_mode = $display == 'normal' ? $this->getSetting('view_mode_normal') : $this->getSetting('view_mode_extended');

    foreach ($this->getEntitiesToView($items, $langcode) as $delta => $entity) {
      $elements[$delta] = $this->viewBuilder->view($entity, $view_mode);
    }

    return $elements;
  }

A piece of content could then chain these various combinations of components, layout styles, and display settings to create rich and dynamic pages.

Each relevant content type also contained common fields used to pull through information about that piece of content, whether it be summary text, an image, or a date. This meant that the same paragraphs could target multiple content types for even greater flexibility. For example, the Featured content component used the node’s Title, Syndicated Summary (a plain text field limited to 120 characters), and Published date fields across all content types to render each display. And the Hero card component used the Syndicated image (a media reference to an Image bundle), and Syndicated summary fields for its displays.

The frontend

Mostly described in Rikki’s blog on paragraph layouts, we rely heavily on Flexbox, though it was very tempting to start playing with CSS Grids, which would also work well with this approach.

The biggest hurdle is theming your components in a flexible way so they work at a range of widths. Using the grid class inside your component to tailor it to each width is an easy win, but flexbox is again the real hero. This particular design had every single component aligned on a vertical rhythm, so no matter what two components you put next to each other their bottom borders or background colours had to align. Because the grid column class is on a parent element to the design component, the grid columns all had the same height but the inner component didn’t know about it, resulting in something like this:

But the fact that the grid column heights matched was enough for a bit more flexbox magic. Adding `display: flex` and ‘flex-direction: column` to our grid column classes tells the inner component to fill the height of its parent. Giving us perfectly aligned components every time.

The https://www.transport.nsw.gov.au site was launched in early July and heavily utilises these concepts across the majority of its content types, including the homepage and the top level landing pages. The solution was developed in an iterative approach, with formatters and view modes coming last which really married everything together. It’s also worth mentioning we used the Display suite module for page layouts and implemented various custom DS layouts and field plugins which again helped a lot.

Component based design doesn’t have to be a headache for content editors or developers, it just takes some forward planning when designing the content model and front end approach.