Customize your Drupal 8 Paragraphs (or any entity really)

By Dave

Though at the time of writing this, Layout Builder seems to be next in line for easier content creation. The Paragraphs module, however is still widely used and allows a REALLY easy method of adding various content to a Drupal page.

If you want a way to manipulate the data within a paragraph before it's rendered, the 'lazy' method is to go straight to the template files. While this will get the job done, it locks you in to the theme layer for logic and will make reusability more difficult.

The method I will explain here might seem a little overkill since most of it could be done within a single hook_preprocess function. The idea here is to make it easier to add functionality later and can even be used beyond paragraphs, since out of the box, content is entity based.

If you haven't done much Drupal 8 development, this will also get you using OOP techniques as opposed to strictly functional.

As with any method found on the web, this isn't meant to be the final way to do this. It can be cleaned up, optimized or torn completely apart depending on what you're trying to accomplish.

In this example, we will be displaying the number of characters that have been input in the text field. Yes yes, I know it's lame but the idea is to show you where to plug in the really cool stuff you want to do.

What you need to start:

Helpful to have:

Here we go!

  • Let's start by adding a new paragraph bundle called String Count
  • Add a field titled 'Text' (field_text)
    • Don't worry about the display output right now.
  • If you haven't already been using paragraph on a content type, go ahead an add the Paragraphs field to your basic page content type.
  • Add a page, with a paragraph attached. By default it will look like this.
Screenshot of new paragraph

Now we get into some code! Woot!

First we will create a 'service' in our custom module. Let's assume our custom module is called 'Hello World' (hello_world)

Remember, for modules, the root file names will follow the same naming as the module machine name. In other words, if my module is called hello_world then I will have hello_world.module hello_world.info etc.

  • Create a hello_world.services.yml file.

The yaml files are finicky about syntax, so if you see errors, that may be the culprit.

  • In hello_world.services.yml
services:
  hello_world.paragraphs_core_helper:
    class: Drupal\hello_world\Controller\Services\ParagraphsCoreHelper

As you'll figure out from the class above, you'll create a file called ParagraphsCoreHelper.php in the /src/Controller/Services directory within your custom module

  • In ParagraphsCoreHelper.php Let's start out with this
<?php

namespace Drupal\hello_world\Controller\Services;

use Drupal\paragraphs\Entity\Paragraph;

class ParagraphsCoreHelper {

  /**
   * {@inheritdoc}
   */
  public function getStringCount(Paragraph $entity) {

    if ($entity->bundle() != 'string_count') {
      return false;
    }
    
    ksm($entity->bundle());
    
    $data = array();

    return $data;

  }

}

Explained: This is a class with a method called 'getStringCount'. You'll see how it gets called and used next. The idea of this file is to include all the 'helpers' for any paragraph that needs them. Why? Because it's just a way to keep your code organized.

  • To make Drupal use this, we are going to use a hook_preprocess function.
  • I won't go into hook naming, but for this tutorial you can probably figure out the usage
  • It's really just saying, "Hey Drupal before you render this, lemme give you some more things. Cool?"

In your .module file add this...

function hello_world_preprocess_paragraph__string_count(&$variables) {
    // This loads the service we created above
    $paragraph_helper = \Drupal::service('hello_world.paragraphs_core_helper');
    
    // This puts the paragraph entity in a single variable.
    $paragraph_entity = $variables['paragraph'];
    
    // This reaches out and uses that class within the service to get some cool data
    $data = $paragraph_helper->getStringCount($paragraph_entity);
    
    // This puts what is returned into an array item for this entity (entity being the paragraph)
    $variables['data'] = $data;
}

If you use the devel and devel_kint module as recommended, you can put a simple ksm() function as a sanity check that your code made it this far. We'll do more with that entity object later.

  • If you don't use devel, it's up to you if you want to put anything there to verify (dump(), etc.)
  • Save your files and clear your cache.

 

kint screenshot

 

Now that we know our helper class is working... let's put together a little template.

Wait what? I thought this was without templates??

This template will live in your module, not the theme. So if and when you change your theme, your logic stays intact.

  • Back in your .module file we'll add a hook_theme (or add to it if it already exists)
/**
 * Implements hook_theme().
 *
 * To add the template definition.
 **/
function hello_world_theme($existing, $type, $theme, $path) {

  return [    
    'paragraph__string_count' => [
        'template' => 'paragraph--string-count',
        'base hook' => 'paragraph',
        'variables' => array('data' => null),
    ], 
  ];

}
  • Basically this says "Hey Drupal, for this paragraph go ahead and use this template... and by the way, my 'data' variable is NULL if nothing is there"

  • Back in the module directory, create a directory for your template files. call it '/templates'
  • Add a file called paragraph--string-count.html.twig
  • This file will simply overwrite the default paragraphs template file. You can always go grab the original file content from the paragraphs module /templates folder
  • I'm leaving out the comments to make this all easier to read.

 

{%
  set classes = [
    'paragraph',
    'paragraph--type--' ~ paragraph.bundle|clean_class,
    view_mode ? 'paragraph--view-mode--' ~ view_mode|clean_class,
    not paragraph.isPublished() ? 'paragraph--unpublished'
  ]
%}
{% block paragraph %}
  <div{{ attributes.addClass(classes) }}>
    {% block content %}
      {{ kint('Woot') }}
      {{ content }}
    {% endblock %}
  </div>
{% endblock paragraph %}
  • This is the default output for a paragraph. The output from all the fields, etc. coming from the {{ content }} variable.
  • How do I know the template is working? Put something in the template file that you know will show up as it's rendered. I personally like {{ kint() }} since I use that to get the correct info from my variables.
Output of kint function

 

Almost there!

BUG REPORT!

If you use kint() within your template file and you get the white screen of death, check this link out. (Or just use dump())


Time for the $data variable!

Moving on...

Back in your template file add in some markup with the data.text variable. You'll see what it does coming up.

{%
  set classes = [
    'paragraph',
    'paragraph--type--' ~ paragraph.bundle|clean_class,
    view_mode ? 'paragraph--view-mode--' ~ view_mode|clean_class,
    not paragraph.isPublished() ? 'paragraph--unpublished'
  ]
%}
{% block paragraph %}
  <div{{ attributes.addClass(classes) }}>
    {% block content %}
      {{ content }}
      <h3>The string above has {{ data.text }} characters</h3>
    {% endblock %}
  </div>
{% endblock paragraph %}
  • If you clear your cache, you will see the new text but that variable will still be empty.
  • Let's go put some good stuff in that variable!

  • Back in the ParagraphsCoreHelper.php file, you will be getting the field value of that text field, doing some php-ish and Drupal things, then returning that value back into the $data['text'] array item.
namespace Drupal\hello_world\Controller\Services;

use Drupal\paragraphs\Entity\Paragraph;

class ParagraphsCoreHelper {

  /**
   * {@inheritdoc}
   */
  public function getStringCount(Paragraph $entity) {

    if ($entity->bundle() != 'string_count') {
      return false;
    }
    $data = [];
    
    // Make sure the field exists
    $hasField = $entity->hasField('field_text');

    if ($hasField === FALSE) {
        return false;
    }
    
    $value = $entity->get('field_text')->getString();
        
    $data['text'] = strlen($value);

    return $data;

  }

}

Explained: We check to make sure the entity has the field... because why not. Any time we can throw in some empty checks, the less 'bugs' we'll have to hear about down the road. We then get the string using some built in Drupal methods. 'getString()', etc. Using a handy little php function to give you the string length, we put that in $data['text'].

Go ahead and clear your cache, and check out your page.

Final Result

 

Now, if that worked you should have a really basic idea on how to create a very extensible system. This doesn't have to apply to just paragraphs either. The hook_preprocess function will allow you to alter any entity before it is well, processed. 

The benefit of having a service is to keep your base module code clean. You can even call that same service from elsewhere (like another module).

As you get more comfortable with OOP and what Drupal has behind the scenes, you can make this sort of thing even more efficient and useful. Take a look at caching as well.

And lastly, keeping all of this within your module is great because:

  1. Logic within the template file is restrictive
  2. All the logic of your output should happen way before it even touches the site theme.