Programmatically creating custom block types for Panels

Fri, 03/03/2017 - 00:37

The most potent Drupal module for creating very complex layouts is definitely Panels. It integrates with a lot of other parts of your Drupal site, so you can show views, nodes, webforms, blocks, and pretty much anything else you can think of. Page builder is very easy to use and allows administrators visual representation of complex pages.

One of the limitations I faced multiple times is lack of any customization of the generic "Custom content" block. However, this is easily achieved with a custom module. In this article, I will show you how to provide a custom CTools content type in your custom module and use it with Panels.

The code in this article will work only for Drupal 7. Article for D8 will come later.

So, when would you need a custom CTools content type? Basically any time when you wish that the "Custom content" provided by Panels had more fields so you can manipulate better the output. With a custom content type you can define any number of configurable fields, preprocess those values and render the output.

Before starting, it is good to explain why do we actually need to integrate with CTools instead of Panels. The reason for this is that Panels builds a lot of its functionality on top of CTools and custom content types (which are actually blocks you can add to a panel) are provided by CTools.

First thing to do is to implement the hook_ctools_plugin_directory() and provide the directory in our custom module where we will store our content types. I recommend making this as self as explanatory as possible and storing the new content type in /plugins/ctools/content_types. Also, to avoid name collision with other modules, prefix everything with the machine name of your module.

/**
 * Implement hook_ctools_plugin_directory().
 */
function YOUR_MODULE_ctools_plugin_directory($owner, $plugin_type) {
  if ($owner == 'ctools' && $plugin_type == 'content_types') {
    return 'plugins/ctools/content_types';
  }
}

Next, we will create the plugins/ctools/content_types directories in your custom module. Each content type should be in a separate file. In our case, we will name the content type "Teaser block with image", so the file name I chose is YOUR_MODULE_pane_teaser_block_with_image.inc. This file should contain plugin information and certain hooks that will be used for configuration form, storing the settings and rendering.

Plugin declaration provides basic information to CTools, such as name and description, and lists callbacks that should be used for various aspects of your custom content type. The code should be placed at the top of your plugin file, outside of any function or class.

/**
 * Plugin declaration.
 */
$plugin = array(
  'title' => t('Custom block'),
  'description' => t('Displays a custom block with customizable fields.'),
  // This is the callback to the administrative form where we will show the
  // configuration form.
  'edit form' => 'YOUR_MODULE_pane_teaser_block_with_image_edit_form',
  // This is the callback used for rendering the block itself. Here you can
  // include any preprocessing logic you need.
  'render callback' => 'YOUR_MODULE_pane_teaser_block_with_image_render',
  // Default setting values are provided here.
  'defaults' => array(
    'textfield' => NULL,
    'checkboxes' => NULL,
    'select' => t('See More'),
    'content' => array('filter' => '', 'value' => ''),
  ),
  // This means that the block is available in all contexts - nodes, blocks, etc.
  'all contexts' => TRUE,
);

After this, we will create the edit form for administrators. This uses Drupal's Form API, so all elements are standardized. The only difference is that block settings will not be fetched from variable storage (using variable_get(), like you would do with regular blocks and admin settings forms) but instead you would get everything from the $conf variable passed to your form. Here's an example:

/**
 * Administrative form callback.
 */
function YOUR_MODULE_pane_teaser_block_with_image_edit_form($form, &$form_state) {
  $conf = $form_state['conf'];

  // You can start adding the fields to the $form variable using Drupal Form API.
  //
  // Here are some example fields:
  $form['textfield'] = array(
    '#type' => 'textfield',
    '#title' => t('Text field'),
    '#default_value' => $conf['textfield'],
    '#required' => TRUE,
  );
  $form['select'] = array(
    '#type' => 'select',
    '#title' => t('Select box'),
    '#default_value' => $conf['select'],
    '#required' => TRUE,
    '#options' => array(
      'option1' => t('Option @number', array('@number' => 1)),
      'option2' => t('Option @number', array('@number' => 2)),
      'option3' => t('Option @number', array('@number' => 3)),
      'option4' => t('Option @number', array('@number' => 4)),
      'option5' => t('Option @number', array('@number' => 5)),
    ),
  );
  $form['checkboxes'] = array(
    '#type' => 'checkboxes',
    '#title' => t('Checkboxes'),
    '#default_value' => $conf['checkboxes'],
    '#required' => TRUE,
    '#options' => array(
      'checkbox1' => t('Checkbox @number', array('@number' => 1)),
      'checkbox2' => t('Checkbox @number', array('@number' => 2)),
      'checkbox3' => t('Checkbox @number', array('@number' => 3)),
      'checkbox4' => t('Checkbox @number', array('@number' => 4)),
      'checkbox5' => t('Checkbox @number', array('@number' => 5)),
    ),
  );
  $editor_field = $conf['content'];
  $form['content'] = array(
    '#type' => 'text_format',
    '#base_type' => 'textarea',
    '#title' => t('Formatted input'),
    '#default_value' => $editor_field['value'],
    '#format' => $editor_field['format'],
  );

  return $form;
}

The submit callback will be pretty straightforward; we can store everything based on the keys so there is no need to handle each field separately.

/**
 * Submit callback.
 */
function YOUR_MODULE_pane_teaser_block_with_image_edit_form_submit(&$form, &$form_state) {
  foreach (array_keys($form_state['plugin']['defaults']) as $key) {
    if (isset($form_state['values'][$key])) {
      $form_state['conf'][$key] = $form_state['values'][$key];
    }
  }
}

Now comes the interesting part - rendering of the block. If you want to keep it clean, you can also declare your own template in hook_theme() of your module and just invoke here the theme() function instead. However, to keep this simple, I will just

/**
 * Render callback.
 */
function YOUR_MODULE_pane_teaser_block_with_image_render($subtype, $conf, $args, $contexts) {
  // All settings are stored in the $conf variable, with field names as array keys.
  // For example:
  $text_field_value = $conf['textfield'];
  $editor_field = $conf['content'];

  // Return value must be an object with title and content properties.
  $block = new stdClass();
  $block->title = NULL;
  // Instead of compiling the content in a variable and passing it here, you can
  // add hook_theme() implementation in your .module file and call theme() here
  // instead. That way you can have the template as a separate file.
  $block->content = 'Content';

  return $block;
}

The example above just scratched the surface and presented the basics. There are a lot of other things you can do here, such as work with contexts, add file uploads, combine this with dynamic rules, etc. The best way to learn how these more complex scenarios are setup is to examine the code of content types shipped with CTools module.

Links