Profile picture for user admin
Daniel Sipos
13 May 2019

I’ve been working on a Drupal 7 to 8 migration of content and I encountered in the source body fields a bunch of tables which had a class that styled them in a certain way. One of the requirements was clearly to port the style but improve it using the Bootstrap tables styles and responsiveness. What to do, what to do.

In the source file I was encountering something like this:

<table class="some-class">...</table>

Which you can argue is not that bad, it only has one class on it and an external style does the job. But obviously it would be better if the stored data didn’t even have that class. So then in the migration I could just kill all the table classes from the body field and apply those stylings externally (all the tables inside the body field). This is a first good step. But what about Bootstrap?

I needed something like this instead to pick up Bootstrap styles:

<table class="table table-sm table-striped table-hover">...</table>

So to make the tables show up with Bootstrap styles I’d have to step on my earlier point of not storing the table classes in the body field storage. Even if I could somehow alter the CKEditor plugin to apply the classes from the widget. And not to mention that if I wanted responsive tables, I’d have to wrap the table element with a <div class="table-responsive">...</div>. So even more crap to store. No.

Then it dawned on me: why not just store the clean table elements and then, upon rendering, apply the Bootstrap classes, as well as wrap them into the necessary div? After replying Hold my beer to my self-challenging alter ego, I went and I did. So I came up with this little number (will explain after):

<?php

namespace Drupal\my_module\Plugin\Filter;

use Drupal\Component\Utility\Html;
use Drupal\filter\FilterProcessResult;
use Drupal\filter\Plugin\FilterBase;

/**
 * Makes the tables in the content show up using Bootstrap styling.
 *
 * @Filter(
 *   id = "table_style_filter",
 *   title = @Translation("Table styles"),
 *   description = @Translation("Adds the necessary markup to tables to render
 *   them via Bootstrap styling."),
 *   type = \Drupal\filter\Plugin\FilterInterface::TYPE_MARKUP_LANGUAGE,
 * )
 */
class TableStyleFilter extends FilterBase {

  /**
   * {@inheritdoc}
   */
  public function process($text, $langcode) {
    $dom = Html::load($text);


    $elements = $dom->getElementsByTagName('table');
    if ($elements->length === 0) {
      return new FilterProcessResult(Html::serialize($dom));
    }

    /** @var \DOMElement $element */
    foreach ($elements as $element) {
      $classes = explode(' ', $element->getAttribute('class'));
      $bootstrap_classes = [
        'table',
        'table-sm',
        'table-striped',
        'table-hover'
      ];

      foreach ($bootstrap_classes as $class) {
        $classes[] = $class;
      }

      $new_element = clone $element;
      $new_element->setAttribute('class', join(' ', array_unique($classes)));

      $wrapper = $dom->createElement('div');
      $wrapper->setAttribute('class', 'table-responsive');
      $wrapper->appendChild($new_element);
      $element->parentNode->replaceChild($wrapper, $element);
    }

    return new FilterProcessResult(Html::serialize($dom));
  }

}

So what do we have here? Well, it’s a Filter plugin that you can add to your text format and which processes the text before it’s rendered. And obviously gets cached after.

In the plugin annotation I used the type \Drupal\filter\Plugin\FilterInterface::TYPE_MARKUP_LANGUAGE because this doesn’t seem to be skipped by core anywhere and its purpose is to generate HTML. Then I implement the process() method to achieve my goal. And I do this quite easily with the following steps:

  1. Find all the table DOM elements and return early if none are found
  2. Loop through all the table DOM elements, clone them and apply the classes to the clone
  3. Create a wrapper div DOM element with the Bootstrap responsive class and append the table element clone to it
  4. Replace the initial table DOM element with the new wrapper
  5. Profit

The return of the method needs to be a FilterProcessResult object that contains the HTML in the same format as the method receives it in. So I serialize the DOM object back into an HTML string and use that.

And that’s it. After clearing the cache you can add this to a text format and all the tables found in the content rendered using that format will be Bootstrap ready. And tables are just an example. Imagine all the possibilities you have to turn simple HTML tags into the markup required by your corner frontend framework. All the while keeping your data clean and not pissing off the developer that will have to migrate that content somewhere else or render it in some other place differently.

Profile picture for user admin

Daniel Sipos

CEO @ Web Omelette

Danny founded WEBOMELETTE in 2012 as a passion project, mostly writing about Drupal problems he faced day to day, as well as about new technologies and things that he thought other developers would find useful. Now he now manages a team of developers and designers, delivering quality products that make businesses successful.

Contact us

Add new comment