Custom Drupal blocks the right way

Overview

Creating custom blocks in Drupal 7 seems like a pretty straightforward process in Drupal 7. You first implement hook_block_info() to tell Drupal about your block, then you implement hook_block_view() to set the contents of the block for the user. The API pages on these functions explain how to do this, and developers who spend a small bit of time working with these functions can probably get their block working.

However, there are actually a some caveats that we at Jaypan have come across, leading us to come up with a number of our own best practices for creating custom blocks in Drupal 7.

Some of the issues we have come across are:

  • drupal_add_js() and drupal_add_css() don't work for cached blocks
  • Adding a #prefix and #suffix to blocks wraps the whole block instead of the content of the given render array
  • Individual blocks are hard to find in the full page render array
  • Altering the output of a block when creating a new theme for a site changes the output on the current theme

As such, we've put together this overview on creating blocks the way we do at Jaypan, to deal with the issues outlined above.

Issue 1: drupal_add_js() and drupal_add_css() don't work for cached blocks

This is the first issue we dealt with that was confusing. Coming from Drupal 6, where CSS and JS were always added using drupal_add_js() and drupal_add_css(), we were using these functions when creating our blocks, as follows:

  1. function mymodule_block_view($delta = '') {
  2. if ($delta == 'myblock') {
  3. $block = [
  4. 'subject' => t('Block title'),
  5. 'content' => [
  6. '#markup' => t('These are the block contents'),
  7. ],
  8. ];
  9.  
  10. $path = drupal_get_path('module', 'mymodule');
  11. drupal_add_js($path . '/js/my_block.js');
  12. drupal_add_css($path . '/css/my_block.css');
  13.  
  14. return $block;
  15. }
  16. }

The above code works when block caching is turned off, but when block caching is turned on, all of a sudden your CSS and JS stop working.

The solution is to use the #attached property of render arrays to attach the CSS and JS to your blocks as follows:

  1. function mymodule_block_view($delta = '') {
  2. if ($delta == 'myblock') {
  3. $path = drupal_get_path('module', 'mymodule');
  4.  
  5. $block = [
  6. 'subject' => t('Block title'),
  7. 'content' => [
  8. '#markup' => t('These are the block contents'),
  9. '#attached' => [
  10. 'css' => [
  11. [
  12. 'type' => 'file',
  13. 'data' => $path . '/css/my_block.css',
  14. ],
  15. ],
  16. 'js' => [
  17. [
  18. 'type' => 'file',
  19. 'data' => $path . '/js/my_block.js',
  20. ],
  21. ],
  22. ],
  23. ],
  24. ];
  25.  
  26. return $block;
  27. }
  28. }

The above code will attach the CSS and JS even when block caching is turned on. Make sure you add #attached to the 'content' attribute of the block array, and not to the root of the block array, or it will not work.

Issue 2: Adding a #prefix and #suffix to blocks wraps the whole block instead of the content of the given render array

Drupal render arrays allow for adding prefixes and suffixes to the contents of the render array. These wrap the given contents in the prefix and suffix, allowing for HTML tags to be kept separate from content. For example:

  1. $some_array = [
  2. '#prefix' => '<p>',
  3. '#suffix' => '</p>,
  4. '#markup' => t('This is the content'),
  5. ];

 

When rendered, the above content will look like this:

<p>This is the content</p>

As you can see, the content is wrapped in the #prefix and #suffix.

The problem is that blocks do not react in this manner. Take the following block definition for example:

  1. function mymodule_block_view($delta = '') {
  2. if ($delta == 'myblock') {
  3. $block = [
  4. 'subject' => '',
  5. 'content' => [
  6. '#prefix' => '<p>',
  7. '#suffix' => '</p>',
  8. '#markup' => t('These are the block contents'),
  9. ],
  10. ];
  11.  
  12. return $block;
  13. }
  14. }

You would expect that this would be rendered something like the following:

  1. <div class="block-wrapper">
  2. <p>These are the block contents</p>
  3. </div>

Unfortunately, you'd be wrong. When setting a #prefix and #suffix on a block, the entire block gets wrapped, rather than just the content. So the rendered HTML instead ends up as follows:

  1. <p>
  2. <div class="block-wrapper">These are the block contents</div>
  3. </p>

This results in invalid HTML, as well as sometimes breaking the expected layout since the margin/padding of p tags will be applied to the block.

The solution is to always sub array to the 'content' element of the block, and put the contents of the block into this sub-array. By adding this additional layer, the #prefix and #suffix are correctly wrapped around the content. Example:

  1. function mymodule_block_view($delta = '') {
  2. if ($delta == 'myblock') {
  3. $block = [
  4. 'subject' => '',
  5. 'content' => [
  6. // Arbitrary key. I usually use DELTA_block
  7. 'my_block_block' => [
  8. '#prefix' => '<p>',
  9. '#suffix' => '</p>',
  10. '#markup' => t('These are the block contents'),
  11. ],
  12. ],
  13. };
  14.  
  15. return $block;
  16. }
  17. }

When this block is rendered, the HTML will correctly be:

  1. <div class="block-wrapper">
  2. <p>These are the block contents</p>
  3. </div>

 

Issue 3: Individual blocks are hard to find in the full page render array

This issue isn't such a big deal, but sometimes you will want to make a specific change to a block at the last step of the page build process, before rending the block, in hook_page_alter(). When creating a block without the sub array shown issue #2 above, there is no way to identify a given block in the $build array. By adding the sub array in issue #2 above, the block can be identified from the key used for the sub array.

Issue 4: Altering the output of a block when creating a new theme for a site changes the output on the current theme

The last issue is one that isn't found until you are creating a new theme for an existing site. When creating a new theme, generally you will want to re-use blocks for the new theme, while maintaining the existing blocks for the current theme. When using render arrays to add contents to a block, this can become an issue when the contents need to be re-ordered.

To get an understanding of this, let's look at the following example:

  1. function mymodule_block_view($delta = '') {
  2. if ($delta == 'myblock') {
  3. $account = menu_get_object('user');
  4. if ($account) {
  5. $username = get_some_username($account);
  6. $followers = get_user_followers($account);
  7. $age = get_user_age($account);
  8. $location = get_user_location($location);
  9.  
  10. $block = [
  11. 'subject' => '',
  12. 'content' => [
  13. 'my_block_block' => [
  14. 'username' => [
  15. '#prefix' => '<p>',
  16. '#suffix' => '</p>',
  17. '#markup' => t('The user is named @name', ['@name' => $username]),
  18. ],
  19. 'followers' => [
  20. '#prefix' => '<p>',
  21. '#suffix' => '</p>',
  22. '#markup' => t('The user has !count followers', [
  23. '!count' => count($followers)
  24. ]),
  25. ],
  26. 'age' => [
  27. '#prefix' => '<p>',
  28. '#suffix' => '</p>',
  29. '#markup' => t('The user is !age years old', ['!age' => $age]),
  30. ],
  31. 'location' => [
  32. '#prefix' => '<p>',
  33. '#suffix' => '</p>',
  34. '#markup' => t('The user is in @location', ['@location' => $location]),
  35. ],
  36. ],
  37. '#attached' => [
  38. 'css' => [
  39. [
  40. 'type' => 'file',
  41. 'data' => $path . '/css/my_block.css',
  42. ],
  43. ],
  44. 'js' => [
  45. [
  46. 'type' => 'file',
  47. 'data' => $path . '/js/my_block.js',
  48. ],
  49. ],
  50. ],
  51. ],
  52. ];
  53.  
  54. return $block;
  55. }
  56. }
  57. }

It all looks good, like it should work without any troubles. And it will. But the problem arises when you want to re-skin your site, and all of a sudden, you don't need 'location' anymore, and 'followers' needs to come before everything else. Also, the CSS and JS have changed entirely, so you don't want to re-use them. The only solution with the above method of building blocks is to create a new block altogether. But there a better solution:

  1. In your module, create a theme function for the block
  2. Use this theme function for the sub-array of the content element of the block
  3. Add a default theme function to your module
  4. Theme the block in template.php of your theme
  5. Add CSS and JS in template.php of your theme

1) In your module, create a theme function for the block

The first thing is to register a theme function for your block in hook_theme() in your module. We usually name the theme MODULENAME_DELTA_block. Ex:

  1. function mymodule_theme() {
  2. $themes['mymodule_myblock_block'] =[
  3. 'variables' => [
  4. 'username' => FALSE,
  5. 'age' => FALSE,
  6. 'location' => FALSE,
  7. 'followers' => FALSE,
  8. ],
  9. ];
  10.  
  11. return $themes;
  12. }

The above registers a new theme with the key 'mymodule_myblock_block'. The various values that we will use in the block are set as the variables that will be passed to the theme function.

2) Use this theme function for the sub-array of the content element of the block

The next step is to set this theme for our content in our block, instead of building the render array we previously built. We will then pass the same values we generated for our block to the theme function:

  1. function mymodule_block_view($delta = '') {
  2. if($delta == 'myblock') {
  3. $account = menu_get_object('user');
  4. if($account) {
  5. $username = get_some_username($account);
  6. $followers = get_user_followers($account);
  7. $age = get_user_age($account);
  8. $location = get_user_location($location);
  9.  
  10. $block = [
  11. 'subject' => '',
  12. 'content' =>[
  13. 'my_block_block' =>[
  14. '#theme' => 'mymodule_myblock_block',
  15. '#username' => $username,
  16. '#age' => $age,
  17. '#followers' => $followers,
  18. '#location' => $location,
  19. ],
  20. ],
  21. ];
  22.  
  23. return $block;
  24. }
  25. }
  26. }

The above block declaration uses the theme we declared in step 1, passing the values we will use for our block.

3) Add a default theme function to your module

Even though we will be theming the block in our theme, we still add a theme to the module. This is so that when we start building our new theme in the future, we have a reminder about what needs to be done in order to display the contents of our block:

  1. function theme_mymodule_myblock_block($variables) {
  2. return '<p>' . t('Need to overrided theme_mymodule_myblock_block() in template.php') . '</p>';
  3. }

 

The above message will be shown in any theme that enables the block, but has not overridden the theme in template.php. It's basically a reminder for the future.

4) Theme the block in template.php of your theme

Now we actually theme the contents of our block. We will output the same content as we did in our original block declaration, but will do it in the theme function:

  1. function THEMENAME_mymodule_myblock_block($variables) {
  2. $username = $variables['username'];
  3. $age = $variables['age'];
  4. $location = $variables['location'];
  5. $followers = $variables['followers'];
  6.  
  7. $output = '<p>' . t('The user is named @name', array('@name' => $username)) . '</p>';
  8. $output .= '<p>' . t('The user has !count followers', array('!count' => count($followers))) . '</p>';
  9. $output .= '<p>' . t('The user is !age years old', array('!age' => $age)) . '</p>';
  10. $output .= '<p>' . t('The user is in @location', array('@location' => $location)) . '</p>';
  11.  
  12. return $output;
  13. }

Now we have outputted our content in the exact same form as it was originally outputted, however because we have done this in our theme, when we build a new theme we can simply implement this theme override in our new theme, and have the output as different as we may want. For example we could do this:

  1. function NEWTHEMENAME_mymodule_myblock_block($variables) {
  2. $username = $variables['username'];
  3. $age = $variables['age'];
  4. $location = $variables['location'];
  5. $followers = $variables['followers'];
  6.  
  7. $output = '<p>' . t('The user @name is !age years old, lives in @location, and has !count followers', [
  8. '@name' => $username,
  9. '!age' => $age,
  10. '@location' => $location,
  11. '!count' => count($followers
  12. ])) . '</p>';
  13.  
  14. return $output;
  15. }

This can be done without altering the output of the original block, meaning that we can have entirely different output for the same block in multiple themes, without having to build a new block to be able to do this.

5) Add CSS and JS in template.php of your theme

The last step in building our block to be alterable between themes is to add our CSS and JS in the theme, rather than in the module. This will allow us to use separate CSS and JS for each theme, without using any overrides.

We can do this by implementing hook_block_view_MODULE_DELTA_alter() as follows:

  1. function THEMENAME_block_view_mymodule_myblock_block_alter(&$block, $data) {
  2. // We only want to attach our files if $block has been returned in our module.
  3. if ($data) {
  4. $path = drupal_get_path('theme', 'THEMENAME');
  5. $block['content']['#attached'] = [
  6. 'js' => [
  7. [
  8. 'type' => 'file',
  9. 'data' => $path . '/js/my_block.js',
  10. ],
  11. ],
  12. 'css' => [
  13. [
  14. 'type' => 'file',
  15. 'data' => $path . '/css/my_block.css',
  16. ],
  17. ],
  18. ];
  19. }
  20. }

The above will attach the CSS and JS to our block in our theme, allowing different CSS and JS to be used in a different theme, and the files are attached in a manner that will be cached, as described in Issue #1 of this writeup.

Summary

Blocks are an extremely powerful part of the Drupal environment, and creating custom blocks can be a great way to add custom functionality to a Drupal site. However, there can be some issues that are difficult to figure out and overcome when doing so. Hopefully the issues addressed in this write up can make your block experience a little easier.