Hướng dẫn thêm CSS Classes vào Blocks ở Drupal 8

The concept of Modular CSS has been around for years. The basic idea is tackling your CSS code in a systematic way through class and file naming conventions. The main goal being improved workflow through organization, readability and reusability. Some of the more recent modular CSS architectures are Object Oriented CSS (OOCSS) and Scalable and Modular Architecture for CSS (SMACSS). I tend to prefer SMACCS as Jonathan Snook has presented it as a set of guidelines rather than rules. He leaves it open for individual style and interpretation as every developer and every project is different. If you want to see a great example of Modular CSS in action, go blow your mind looking at the Twitter Bootstrap Components Library. The creators have done an excellent job abstracting the components in a way that makes them easy to understand and flexible via classes and subclasses.

>> Đưa region block vào trong node templates của Drupal 8

>> Hướng dẫn cài đặt Drupal VM PHP 7.0.x (configurable) trên Windows

The idea of Modular CSS really appeals to me, especially having worked on large complex sites with multiple front-end developers collaborating together. While Drupal is perfect for these large scale projects, adding your own CSS classes to a Drupal theme ( essential when working with modular CSS ) is often unintuitive, inconsistent and tiresome at best. To remedy this, I've been working on a systematic approach to adding classes to themes that has so far proved extremely useful. In this post we're going to see how we can easily manage classes on one of Drupal's most used components, Blocks, using a preprocess function.

Preprocess Functions

Preprocess ( and Process ) Functions are the key to taking control of your classes in Drupal. Preprocess functions are called before every theme hook is sent to a theme function and template file (.tpl). In my mind preprocess functions have 2 uses:

  1. Altering and adding variables that are available to theme functions and .tpl's. This is how we'll be adding our classes.
  2. Sending these variables to an alternate theme function or .tpl via theme hook suggestions.

I prefer managing my classes via preprocess functions over template files because I find it way more flexible and concise. I no longer need duplicate template files just to add or remove a single class.

Adding Classes to Blocks

As is often the case with Drupal, there are a few options out there for adding classes to blocks, including Skinr, Fusion Accelerator and Block Class. All of these offer the power of adding classes through the admin UI which may be right for you and your site administrators. This hasn't been a requirement for most sites I've worked on. Your mileage may vary. I prefer to define my classes directly in the theme where I'm already writing my CSS, everything is already in code and there is no need to export settings from the database. So let's see how we can do this using default core functionality.

Looking at the core block.tpl.php file, by default there are 3 elements to which we can add classes during preprocess: the block container itself ($classes), the block title ($title_attributes) and block content ($content_attributes). As of this writing, in order to preprocess the block content classes, you must remove the hard coded class attribute from the template yourself or apply a core patch. Below is what block.tpl.php looks like after removing the hard coded content class.

<div id="<?php print $block_html_id; ?>" class="<?php print $classes; ?>"<?php print $attributes; ?>>
 
  <?php print render($title_prefix); ?>
  <?php if ($block->subject): ?>
    <h2<?php print $title_attributes; ?>><?php print $block->subject ?></h2>
  <?php endif;?>
  <?php print render($title_suffix); ?>
 
  <div <?php print $content_attributes; ?>>
    <?php print $content ?>
  </div>
</div>

Now let's create our preprocess function and start adding some classes. Below is an example of our entire function.

<?php
 
/**
 * Implements hook_preprocess_block()
 */
 
function mytheme_preprocess_block(&$vars) {
  /* Set shortcut variables. Hooray for less typing! */
  $block_id = $vars['block']->module . '-' . $vars['block']->delta;
  $classes = &$vars['classes_array'];
  $title_classes = &$vars['title_attributes_array']['class'];
  $content_classes = &$vars['content_attributes_array']['class'];
 
  /* Add global classes to all blocks */
  $title_classes[] = 'block-title';
  $content_classes[] = 'block-content';
 
  /* Uncomment the line below to see variables you can use to target a field */
  #print $block_id . '<br/>';
 
  /* Add classes based on the block delta. */
  switch ($block_id) {
    /* System Navigation block */
    case 'system-navigation':
      $classes[] = 'block-rounded';
      $title_classes[] = 'block-fancy-title';
      $content_classes[] = 'block-fancy-content';
      break;
    /* Main Menu block */
    case 'system-main-menu':
    /* User Login block */
    case 'user-login':
      $title_classes[] = 'element-invisible';
      break;
  }
}

This may look like a lot but it's actually pretty simple. Let's break it down. First off, we create some shortcut variables to save us some typing down the road and make our code more legible.

The first variable we create is $block_idwhich is just a string combining the name of the module creating the block and the block's delta— separated by a dash. This gives us an easy way to target individual blocks when we start adding classes. One thing to note, some blocks, including core custom blocks, use an auto-incremented integer for their delta value. This can present issues if you are moving blocks from dev to staging to production and you're not careful. I recommend using the boxes module to help mitigate this.

The next three variables, $classes, $title_classesand $content_classesare references to the arrays that will eventually be rendered into class attributes. To add classes to an element, we just need to add them to its corresponding array.

After we have our shortcuts set, we can started adding classes to our blocks. We first start by adding some global classes, .block-titleand .block-content, to all block titles and block content containers respectively. You may or may not find this helpful. If you're interested in stripping out all the classes that core and other modules have added to your blocks up to this point, this would be the place to do it. To remove those classes, just reset your variable to an empty array like so $classes = array();

Next, we can start adding our classes on a per block basis. I like to keep a simple print statement handy that prints out the $block_idfor all blocks on the page. When styling a new block, I just uncomment this line and copy the id from the browser into my code editor. To target individual blocks, we create a simple switch statement based on the $block_id. For each block we need to style, we write a case and add the corresponding classes there.

In the first case we are adding three classes to the System Navigation block, one to the block itself, one to its title element and one to its content wrapper. In the second instance we are actually grouping two cases together so we can add the same classes to multiple blocks. Here we are adding .element-invisible, Drupal's built in class for hiding content in an accessible manner, to the titles of the Main Menu and User Login Blocks.

As we add blocks, we add more switch statements. That's all we really need to do to take over our block classes. I've found this alone can be a tremendous boost to productivity while styling a Drupal site. You can get pretty far with blocks alone. However, with modifications to core theme functions and templates, similar to what we did with block.tpl.php above, this technique can be adapted to manage classes on nodes, fields, menus and most other themeable output.

I've submitted a session proposal to DrupalCon Munich on this topic called Stay Classy Drupal - Theming with Modular CSS. If you're interested in learning more about taking control of your classes or have run into trouble trying to do so, leave a comment to let the organizers know you're interested in the session.

As I've stated before, I'm a big fan of Modular CSS which requires the ability to easily manage classes on your markup. This was often a struggle in previous versions of Drupal. However, Drupal 8 makes this significantly easier to manage thanks to a number of improvements to the front-end developer experience (DX). In this post we'll look at how two of these DX improvements, the Twig template language and hook_theme_suggestions_HOOK_alter, and how they make adding classes to blocks much easier to manage.

Twig allows us to easily open up a template file and add our classes where we need them. There are two main approaches to adding classes to a template file. The first is simple: open the file, add the class directly to the tag, save the file and move on with your life.

block.html.twig

<div class="block block--fancy">
  {{ title_prefix }}
  {% if label %}
    <h2  class="block__title block__title--fancy">{{ label }}</h2>
  {% endif %}
  {{ title_suffix }}
  {% block content %}
    {{ content }}
  {% endblock %}
</div>

This works in a lot of cases, but may not be flexible enough. The second approach utilizes the new attributes object – the successor to Drupal 7's attributes array. The attribute object encapsulates all the attributes for a given tag. It also includes a number of methods which enable you to add, remove and alter those attributes before printing. For now we'll just focus on the attributes.addClass() method. You can learn more about available methods in the official Drupal 8 documentation.

block.html.twig

{%
  set classes = [
    'block',
    'block--fancy'
  ]
%}
 
{%
  set title_classes = [
    'block__title',
    'block__title--fancy'
  ]
%}
 
<div{{ attributes.addClass(classes) }}>
  {{ title_prefix }}
  {% if label %}
    <h2{{ title_attributes.addClass(title_classes) }}>{{ label }}</h2>
  {% endif %}
  {{ title_suffix }}
  {% block content %}
    {{ content }}
  {% endblock %}
</div>

Alternatively, we can add our class directly to the class attribute with the existing classes from the attribute.class then print the remaining attributes. To prevent the class attribute from printing twice, we exclude it using the without Twig filter. Either way works.

block.html.twig

<div class="block--fancy {{ attributes.class }}"{{attributes|without('class') }}>
  {{ title_prefix }}
  {% if label %}
    <h2  class="block--fancy {{ title_attributes.class }}" {{title_attributes|without('class') }}>{{ label }}</h2>
  {% endif %}
  {{ title_suffix }}
  {% block content %}
    {{ content }}
  {% endblock %}
</div>

In any case, all our blocks on the site now look fancy as hell (assuming we've styled .block--fancy as such)

Template Suggestions

The above examples work. In reality if all our blocks look fancy, no blocks will look fancy. We need to apply this class only to our special blocks that truly deserve to be fancy. This introduces my second favorite DX improvement to Drupal 8 – hook_theme_suggestions_HOOK_alter.

If you wanted to make a custom template available for use to a certain block In Drupal 7, you had to do so in a preprocess function. Altering theme hook suggestions (the list of possible templates) in the Drupal 8 is delegated to its very own hook. The concept is pretty straight forward. Before Drupal renders an element, it looks at an array of possible template file names (a.k.a. suggestions) one-by-one. For each template file, it looks in the file system to see if that file exists in our theme, its base theme or core themes. Once it finds a match, it stops looking and renders the element using the matching template.

We'll use this hook to add our new template file to the list of suggestions. In the case of blocks, the function we'll define is hook_theme_suggestions_block_alter. It takes two arguments, the first is the array of suggestions which are passed by reference (by prefixing the parameter with a & so we can alter them directly. The second is the variables from our element that we can use to determine which templates we want to include.

Lets assume we renamed one of our templates above to block--fancy.html.twig and saved it to our theme. We then add the following function to my_theme.theme where "my_theme" is the name of our theme.

my_theme.theme

<?php
 
/**
 * Implements hook_theme_suggestions_HOOK_alter() for block templates.
 */
function my_theme_theme_suggestions_block_alter(array &$suggestions, array $variables) {
  $block_id = $variables['elements']['#id'];
 
  /* Uncomment the line below to see variables you can use to target a block */
  // print $block_id . '<br/>';
 
  /* Add classes based on the block id. */
  switch ($block_id) {
    /* Account Menu block */
    case 'account_menu':
      $suggestions[] = 'block__fancy';
      break;
  }
}

Now the account menu block on our site will use block--fancy.html.twig as we can see from the output of twig debug

This is just one example of the improvements in D8 theming. I'm really excited for the clarity that the new Twig templates bring to Drupal 8 and the simplicity of managing template suggestions through hook_theme_suggestions_HOOK_alter.