Upgrade Your Drupal Skills

We trained 1,000+ Drupal Developers over the last decade.

See Advanced Courses NAH, I know Enough

Improve user experience with Paragraphs on Drupal 8

The Paragraphs module is a very good alternative to a WYSIWYG editor for those who wants to allow Drupal users ans editors to make arrangements of complex pages, combining text, images, video, audio, quote, statement blocks, or any other advanced component.

Rather than let the user to struggle somehow with the text editor to make advanced pages, but never able to reach the level made possible with Paragraphs, we can offer him to compose his page with structured content’s components, each of these components being responsible for rendering the content, according to the settings selected, in a layout keeper under control.

Cite one example among dozens of others (possibility are endless). Rather than offering a simple bulleted list from the text editor, we can create a component that can generate a more refined bulleted list : each item in the bulleted list could for example have a pictogram, a title, a brief description and a possible link, and the content publisher could simply select the number of elements he wishes per row. For example a report of this kind.

Une liste à puces mise en forme avec paragraphs

Offer these components enables an inexperienced user to create complex page layouts, with the only constraint to focus on its content, and only its content.

The different possibles form mode availables for paragraph components

We have several options to display, inside the content edit form, the components created with Paragraphs. we can show them in :

  • Open mode: the edit form of the Paragraph component is open by default
  • Closed mode: the edit form of the Paragraph component is closed by default
  • Preview mode: the paragraph component is displayed as rendered on the front office

Paramètres d'affichage du formulaire d'un composant paragraph

I tend to prefer to retain the default closed mode, on content edit form, to improve editor’s experience. Because if the page consists of many components (and it’s the purpose of Paragraphs module), in open mode the content’s edit form tends to scare the user as the number of form may be important, and also makes it very difficult reorganization of the different components (order change), while the pre-visualization method involves either to integrate theses components inside the administration theme or to opt for using default theme when editing content.

The disadvantage of using the closed mode for editing components

The use of closed mode provides an overview of the different components used on the page (the content), and to rearrange them easily with simple drag / drop. Modification of the various components available is done by uncollapse / collapse them on demand.

Formulaire d'édition d'un contenu composé de paragraphs

With this editing mode, the content’s components are listed and have for title the paragraph’s type used. This can be a major drawback if the content uses many components of the same type, the publisher does not have immediate cues to distinguish which content relates to each component.

Modify the label of paragraph components

We can overcome this issue by creating a small module, which will be responsible for changing the label of each component by retrieving the contents of certain fields of our paragraphs.

The general idea is to alter the content edit form, detect if content contains entity reference revision fields (used by paragraphs), and if so, to recover for each paragraph the value of a field (eg a field whose machine name contains the word title), then change the label used in the edit form for each paragraph with this value.

Let's go to practice and PHP snippet. We will implement hook_form_alter().


/**
 * Implements hook_form_alter().
 */
function MYMODULE_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  $form_object = $form_state->getFormObject();

  // Paragraphs are only set on ContentEntityForm object.
  if (!$form_object instanceof ContentEntityForm) {
    return;
  }

  /** @var \Drupal\Core\Entity\FieldableEntityInterface $entity */
  $entity = $form_object->getEntity();
  // We check that the entity fetched is fieldable.
  if (!$entity instanceof FieldableEntityInterface) {
    return;
  }

  // Check if an entity reference revision field is attached to the entity.
  $field_definitions = $entity->getFieldDefinitions();
  /** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */
  foreach ($field_definitions as $field_name => $field_definition) {
    if ($field_definition instanceof FieldConfigInterface && $field_definition->getType() == 'entity_reference_revisions') {
      // Fetch the paragrahs entities referenced.
      $entities_referenced = $entity->{$field_name}->referencedEntities();
      /** @var \Drupal\Core\Entity\FieldableEntityInterface $entity_referenced */
      foreach ($entities_referenced as $key => $entity_referenced) {
        
        $fields = $entity_referenced->getFieldDefinitions();
        $title = '';
        $text = '';
        
        foreach ($fields as $name => $field) {
          if ($field instanceof FieldConfigInterface && $field->getType() == 'string') {
            if (strpos($name, 'title') !== FALSE) {
              $title = $entity_referenced->{$name}->value;
            }
            // Fallback to text string if no title field found.
            elseif (strpos($name, 'text') !== FALSE) {
              $text = $entity_referenced->{$name}->value;
            }
          }
        }
        // Fallback to $text if $title is empty.
        $title = $title ? $title : $text;
        // Override paragraph label only if a title has been found.
        if ($title) {
          $title = (strlen($title) > 50) ? substr($title, 0, 50) . ' (...)' : $title;
          $form[$field_name]['widget'][$key]['top']['paragraph_type_title']['info']['#markup'] = '<strong>' . $title . '</strong>';
        }
      }

    }
  }

}

Let us review in more detail what we do in this alteration.

First we check that we are on a content entity form, and the entity that we are currently editing is fieldable.

$form_object = $form_state->getFormObject();

// Paragraphs are only set on ContentEntityForm object.
if (!$form_object instanceof ContentEntityForm) {
  return;
}

/** @var \Drupal\Core\Entity\FieldableEntityInterface $entity */
$entity = $form_object->getEntity();
// We check that the entity fetched is fieldable.
if (!$entity instanceof FieldableEntityInterface) {
  return;
}

We check then all the entity’s fields (a node, a content block, or any other content entity) and only treat the entity_reference_revisions  field type that correspond to the field implemented and used by Paragraphs module.

// Check if an entity reference revision field is attached to the entity.
$field_definitions = $entity->getFieldDefinitions();
/** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */
foreach ($field_definitions as $field_name => $field_definition) {
  if ($field_definition instanceof FieldConfigInterface && $field_definition->getType() == 'entity_reference_revisions') {
    // Fetch the paragrahs entities referenced.
    $entities_referenced = $entity->{$field_name}->referencedEntities();
    /** @var \Drupal\Core\Entity\FieldableEntityInterface $entity_referenced */
    foreach ($entities_referenced as $key => $entity_referenced) {

      // Stuff.

    }

  }
}

For each detected Paragraph entities we will then retrieve the value of a field. In our example, we test first if this is a text field type (string), then test whether its machine name contains the word title, or the word text which will serve as fallback if no field containing title in its machine name is found.

$fields = $entity_referenced->getFieldDefinitions();
$title = '';
$text = '';
foreach ($fields as $name => $field) {
  if ($field instanceof FieldConfigInterface && $field->getType() == 'string') {
    if (strpos($name, 'title') !== FALSE) {
      $title = $entity_referenced->{$name}->value;
    }
    // Fallback to text string if no title field found.
    elseif (strpos($name, 'text') !== FALSE) {
      $text = $entity_referenced->{$name}->value;
    }
  }
}

This example is of course to adapt according to your own context. We could, for example, precisely target a specific field based on the type of paragraph detected. For example :

$bundle = $entity_referenced->bundle();
$title = '';
$text = '';
switch ($bundle) {
  case 'paragraph_imagetext':
    $title = $entity_referenced->field_paragraph_imagetext_title->value;
    break;
  case 'other_paragraph_type':
    $title = $entity_referenced->another_field->value;
    break;
  default:
    break;
}

Finally, we replace the label used by the paragraph type, if we have got a value for our new label.

// Fallback to $text if $title is empty.
$title = $title ? $title : $text;
// Override paragraph label only if a title has been found.
if ($title) {
  $title = (strlen($title) > 50) ? substr($title, 0, 50) . ' (...)' : $title;
  $form[$field_name]['widget'][$key]['top']['paragraph_type_title']['info']['#markup'] = '<strong>' . $title . '</strong>';
}

A happy user

The result then allows us to offer content publishers and editors a compact and readable edition form where he can immediately identify what content refers to a paragraph type.

Formulaire amélioré d'édition d'un contenu composé de paragraphes

This tiny alteration applied on the content edit form, and specifically on the default paragraph labels, makes it immediately more readable and understandable. It translates technical information, more oriented site builder, in a user-content information, giving him a better understanding and better comfort.

I wonder how this feature could be implemented using a contributed module (or inside the Paragraphs module itself ?), the biggest difficulty living here in the capacity of a Entity Reference Revisions field to target an infinite Paragraphs type, themselves containing a possible infinity fields. If you have an idea I'm interested?

You need a freelance Drupal ? Feel free to contact me.

Author: 
Original Post: 

About Drupal Sun

Drupal Sun is an Evolving Web project. It allows you to:

  • Do full-text search on all the articles in Drupal Planet (thanks to Apache Solr)
  • Facet based on tags, author, or feed
  • Flip through articles quickly (with j/k or arrow keys) to find what you're interested in
  • View the entire article text inline, or in the context of the site where it was created

See the blog post at Evolving Web

Evolving Web