Improve user experience with Paragraphs on Drupal 8

An old typewriter

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.

 

Commentaires

Soumis par Stephan (non vérifié) le 20/10/2016 à 13:14 - Permalien

Salut,
i saw you bullitlist-view and i think it will be very useful, for displaying a list of sponsors or so. Did you now a tutorial about the topic, how i can realize that with paragraphs? Or can you share the code, so i can extract it for myself?
Thank you :)
Stephan

You can find some tutorials about building nested paragraphs (for example : https://www.webwash.net/how-to-create-powerful-container-paragraphs-in-…). I will try to contribute this package, but it's need refactoring because it use some features of the front theme (based on bootstrap, with the fontawesome font-icon). The key here is to build a paragraph (let's name it paragraphs_pictos) which contains one entity reference revision field which reference an another paragraph type (let's name it paragraphs_picto). And then you can add the fields you need inside this last paragraph type (title, text, icon, link) for your list's item. You can too add fields on the parent paragraph type for the master title, the description above the bullet list, and a select field which permit you to determine the number of items by row. The remainder to do is some customization inside the twig templates and the css rules. This could be the subject of an another blog post :-)

Soumis par Stephan (non vérifié) le 20/10/2016 à 18:33 - Permalien

...you fabrice, now i have a starting point and i am looking forward to more blogposts :)

Stephan

Soumis par Kappaluppa (non vérifié) le 30/10/2018 à 18:43 - Permalien

We ran into the same thing on a D7 project. Our solution was to add an extra field "Paragraph name" to the paragraph. In the paragraph display there is a view for Paragraph Editor Preview. Display that field name only there. You could also add another field, if needed, for a description (if you wanted to display more info), or anything else. Then set the paragraphs to preview as you described.

Ajouter un commentaire