Theme colour switcher

Truncated expanding text field with "show more" link in Drupal 8 & Twig

I've been working on a website with the requirement to truncate certain text fields—display the first several words of the text, with a "show more" link which on click, expands the rest of the text. I had to dig a bit into the Twig documentation to figure it out, but it's easily done with Twig filters and a bit of JavaScript.

(Note that this method will strip any HTML in the text fields. It's on my to-do list to someday figure out how to do it while preserving tags).

In this case, we're working with a field called "synopsis" and we're in a node template (e.g. node--content-type.html.twig).

First, we need to filter the text field output in Twig. We set a variable which is equal to the field content, with several filters:

{% set synopsis = content.field_synopsis|render|striptags|trim|split(' ') %}

  • First, the render filter converts the field content from an array into a string, allowing the other filters to be applied.
  • "Striptags" removes any HTML from the field content, as it can cause problems if tags aren't formatted properly or get cut.
  • "Trim" removes any extra whitespace at the beginning and end of the field. This is necessary for the next filter to work properly.
  • "Split" is the really crucial filter. The field content created by "render" is an undifferentiated string of characters. Split breaks it into chunks based on the delimitor character that you pass it, in this case, a space. The filter looks for the delimitor in the content and breaks it into separate strings with each new string beginning after each delimitor. Delimiting by spaces, then, effectively breaks the output into words.

Now that we have our "synopsis" variable with the correctly formatted field content, we can use it in our Twig template. We need to display synopsis in two different chunks: first, however many words we want in our teaser (which will display on page load) and second, the remainder of the text, which will display when the user clicks "show more". We also need to add an ellipsis (…) at the end of the first chunk of text, and the element to click to show the rest.

We want to wrap this all in an if statement which will only do this processing if our field is more than 50 words. (Since we've divided the field into words, the length is counted by words rather than characters.) If it's shorter, the whole field will display normally:

{% if synopsis|length > 50 %}
  {# the rest of our code will go here #}
{% else %}
  {{ content.field_synopsis }}
{% endif %}

Everything we do from this point on will be between the {% if %} and {% else %} tags; you'll find it all together further on.

First, we're going to create a variable for the text teaser, with an ellipsis character appended wrapped in a span element for easy hiding later:

{% set teaser = synopsis|slice(0, 40)|join(' ') ~ '<span class="summary-ellipsis">&hellip;</span>' %}

This variable takes our previously-created synopsis variable and applies two more filters: slice, which creates a chunk of the synopsis output. The arguments in parentheses are two: the starting point, in this case 0 or the first word; the second, the endpoint, in this case 40. I chose 40 because I wanted at least 10 words of difference between the teaser and full text, as it doesn't make sense for users to click "show more" to see only a few words.

Secondly, the "join" filter takes this slice of 40 separate strings, and puts them back together as one string with the delimiter character that you choose (in this case, a space) between them.

Thirdly, we append the string which contains the ellipsis and its wrapping span.

We then create a variable for the remainder text:

{% set remainder = synopsis|slice(40)|join(' ') %}

The slice filter in this case starts at word 40, where the teaser slice left off. I'm not sure why it's 40 and not 41, but if you set it to 41, it will skip a word.

Since we've passed no second argument, the slice will go to the end of the output, i.e., the last word.

We then print the teaser in our template:

{{ teaser|raw }}

The "raw" filter is needed because otherwise the HTML in our ellipsis element will be output as characters rather than encoded HTML.

Next, we add the button which users will click to show the full summary. For accessibility purposes, we use a button rather than a link because it's performing an action, not navigating to a different page.

<button aria-expanded="false" class="show-more">{% trans %}(Show full summary){% endtrans %}</button>

The aria-expanded attribute indicates to a screen reader user that something will be revealed on pressing the button. We wrap the button text in {% trans %} tags so it can be translated through Drupal's interface.

We then print the remainder text, wrapped in a span so we can target it to hide/show. We also put the "hidden" attribute on it so the text does not display:

<span class="summary-remainder" hidden>{{ remainder|raw }}</span>

The full output, wrapped in a div with a class so we can easily target it with JavaScript:

<div class="read-more">
{% set synopsis = fields.field_synopsis.content|render|striptags|trim|split(' ') %}

{% if synopsis|length > 30 %}

  {% set teaser = synopsis|slice(0, 20)|join(' ') ~ '<span class="summary-ellipsis">&hellip;</span>' %}

  {% set remainder = synopsis|slice(20)|join(' ') %}

  <p>
{{ teaser|raw }}
<button aria-expanded="false" class="show-more">{% trans %}(Show full summary){% endtrans %}</button>
<span class="summary-remainder">{{ remainder|raw }}</span>
  </p>
{% else %}
  {{ fields.field_synopsis.content }}
{% endif %}
</div>

Css

.read-more .summary-remainder {
  display: none;
}

.read-more.expanded .summary-ellipsis {
  display: none;
}

.read-more.expanded .summary-remainder {
  display: inline;
}

JavaScript

The JS required to make this work is minimal. First we get every div with the class "read-more", we add a "click" event listener to it, and if the click is on the button we add the "expanded" class.

const teasers = document.querySelectorAll('.read-more');

function showMore(e) {
    if (e.target.matches('button.show-more')) {
        e.currentTarget.classList.add('expanded');
    }
}

teasers.forEach(function(teaser) {
    teaser.addEventListener('click', showMore);
});

"Show less" link

What if you want a "show less" button to collapse the remainder text after displaying it?

You could add a "show less" button at the end of the remainder text, and toggle its display on the click of the "show more", and vice-versa. Or you could toggle the text and aria-expanded attribute of the show more button and its function based on its display state. There are a few different ways to accomplish this and if anyone has questions about how to do it let me know in the comments!

Tags:

Add new comment

Plain text

  • No HTML tags allowed.
  • Lines and paragraphs break automatically.