How to Create Ctools Content Types in Drupal 7

Ctools content types, not to be confused with general content types, are Panels version of blocks. Another name for them are panel panes, but just think of them as more powerful blocks. Because they are Ctools plugins, the code to implement them is easy to write and maintain. Each plugin has their own file, whereas with blocks, you have to cram everything into hook_block_view().

They also integrate with Ctools contexts, which means you get a context object passed directly in the content type and most of the time this is an entity object. No longer will you have to write code which grabs the entity ID from the URL and load the entity.

In this post, we'll create a Ctools content type which displays a "Read more" link, with the link text being configurable. You'll learn how to create a plugin and a settings form for it. Basic PHP knowledge is required if you want to follow along.

Step 1: Integrate your Module with Ctools

Go into your custom module and add the following hook:

/**
 * Implements hook_ctools_plugin_directory().
 */
function morpht_ctools_content_type_ctools_plugin_directory($module, $plugin) {
  if ($module == 'ctools' && !empty($plugin)) {
    return "plugins/$plugin";
  }
}

This hook is used to tell Ctools where plugins are stored. It'll look for the content type plugin in "plugins/content_types".

Step 2: Create Plugin File

Create a file called "read_more.inc" and place it in "plugins/content_types", create these folders if required. Once created, the path to the file should be "plugins/content_types/read_more.inc".

Step 3: Describe Plugin

Open up "read_more.inc" and add the following bit of code at the top:

$plugin = array(
  'title' => t('Read more link'),
  'description' => t('Displays a read more link.'),
  'single' => TRUE,
  'content_types' => array('read_more'),
  'render callback' => 'read_more_content_type_render',
  'required context' => new ctools_context_required(t('Node'), 'node'),
  'edit form' => 'read_more_content_type_edit_form',
  'category' => array(t('Morpht Examples'), -9),
);

The $plugin array is used to describe the plugin. Let's break down this array:

  • 'title': This is the title of the plugin. It'll be used in the Panels administration area.
  • 'description': This is used as the description of the plugin, it's only used in the Panels administration area.
  • 'single': Content types have a concept called subtype, which is an advanced feature and beyond the scope of this tutorial. For basic content types leave this as TRUE. If you want to learn about subtypes check this Drupal Answers post.
  • 'content_types': This is the machine name of the plugin.
  • 'render callback': This is a callback to a function which will be used to render the content type.
  • 'required context': This tells Ctools which context is required for this content type. By using ctools_context_required(t('Node'), 'node'), this content type will only be available for node entities.
  • 'edit form': This is a callback for the edit form. Please note, this edit form must be implemented if you define a 'required context' key.
  • 'category': This allows you to add a content type into a Panels category.

Step 4: Settings Form

The "Read more" link for the content type needs to be configurable. Creating a configuration form for it is pretty easy. In the $plugin array we added "read_more_content_type_edit_form" as our form.

Go ahead and add the following bit of code after the $plugin:

/**
 * Ctools edit form.
 *
 * @param $form
 * @param $form_state
 * @return mixed
 */
function read_more_content_type_edit_form($form, &$form_state) {
  $conf = $form_state['conf'];
  $form['read_more_label'] = array(
    '#type' => 'textfield',
    '#title' => t('Label'),
    '#description' => t('The read more label.'),
    '#default_value' => !empty($conf['read_more_label']) ? $conf['read_more_label'] : 'Read more',
  );
  return $form;
}

/**
 * Ctools edit form submit handler.
 *
 * @param $form
 * @param $form_state
 */
function read_more_content_type_edit_form_submit($form, &$form_state) {
  foreach (array('read_more_label') as $key) {
    $form_state['conf'][$key] = $form_state['values'][$key];
  }
}

I won't go into too much detail about this because it's a basic Drupal form. The read_more_content_type_edit_form() function is the form and read_more_content_type_edit_form_submit() is the submit handler.

Important: If you want to use the 'required context' key, then you must implement an edit form. Thanks Sam152 for the heads up.

Step 5: Render Function

The last piece of the puzzle is the render function. This is the main function which will be used to render the content type. It is the equivalent of using hook_block_view() if you're creating a block.

Add the following function into the plugin:

/**
 * Render callback function.
 *
 * @param $subtype
 * @param $conf
 * @param $args
 * @param $context
 * @return stdClass
 */
function read_more_content_type_render($subtype, $conf, $args, $context) {
  $node = $context->data;
  $block = new stdClass();

  $block->content = l(t("@read_more", array('@read_more' => $conf['read_more_label'])), 'node/' . $node->nid);

  return $block;
}

The HTML that you want to render needs to be added to the "content" property on the $block object, e.g, $block->content.

The "Read more" label from the settings form can be accessed via the $conf variable. And finally, you can get the node entity from the $context via $context->data.

Summary

Now that you know how powerful Ctools content types are I hope this opens your eyes to new possibilities. Best of all, content types can also be used with Display Suite. This means that you can write your content type once and use it with Panels or Display Suite.

Comments

Great post Ivan, thanks.

Just an FYI for those creating content type plugins, 'edit form' must be implemented in order to take advantage of the 'required context' key. This one caught me out.

Thanks. I've updated the article.

Hi, I haven't found 'content_types' as machine_name of a plugin in ctools API . Could you provide a link to clarify this nuance?

What do you mean by "haven't found"?

Here's a link to the core "Ctools content types": http://cgit.drupalcode.org/ctools/tree/plugins/content_types

I'm talking about the line of code that you wrote in "Step 3: Describe Plugin": 'content_types' => array('read_more').
I think this line is unnecessary. What is your point of view on this?

It's not required, but it's used to set the default callback functions. There's not much documentation about it, here a link to the code: http://cgit.drupalcode.org/ctools/tree/includes/content.inc

I think this line is unnecessary. What is your point of view on this?

I'm leaving it in there so readers are aware of it.

I've found the best documentation for ctools plugins within the actual ctools module inside the "help" directory. That documentation doesn't seem to come up in search much.

Can you do a follow up tutorial, please on how to add extra settings dependant on a choice you make in the plugin edit form.

In other words the submit button will say 'Continue' rather than 'Finish'

I was desperate to find out about this, so I dug around and found the answer in ctools/plugins/views_content/plugins/content_types/views.inc - and its amazingly simple...

$plugin = array(
'title' => t('Read more link'),
'description' => t('Displays a read more link.'),
'single' => TRUE,
'content_types' => array('read_more'),
'render callback' => 'read_more_content_type_render',
'required context' => new ctools_context_required(t('Node'), 'node'),
'edit form' => array(
'read_more_STEP_1_content_type_edit_form' => t('Title at the top of the window here'),
'read_more_STEP_2_content_type_edit_form' => t('Different title at the top of the window here'),
),
'category' => array(t('Morpht Examples'), -9),
);

I followed your code and tweaked a bit for my module and for some reason ctools was not identifying my custom plugin. Tried changing "morpht_ctools_content_type_ctools_plugin_directory" with "mymodule_ctools_plugin_directory" and it worked.
Thanks for putting your thoughts on web.

Yes, you need to be careful to name the hook properly. As you have seen, you need to make sure that the module name is the first part of the function name. In your case this was "mymodule". This code should then be placed into the .module file for your module.

Interesting article. Thanks! It would be made even better if you could add screen shots of what it looks like from a user perspective.

Is it possible to create ctools content types using a hook ?

Plugins are discovered once they are placed into the correct folder. This folder is defined by a hook as outlined in the article: hook_ctools_plugin_directory(). So from this perspective a hook does define how they are discovered.

Great Article. Thanks! I bookmarked it! :)

Add new comment

(If you're a human, don't change the following field)
(If you're a human, don't change the following field)

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.

Filtered HTML

  • Web page addresses and e-mail addresses turn into links automatically.
  • Each email address will be obfuscated in a human readable fashion or, if JavaScript is enabled, replaced with a spam resistent clickable link. Email addresses will get the default web form unless specified. If replacement text (a persons name) is required a webform is also required. Separate each part with the "|" pipe symbol. Replace spaces in names with "_".
  • Allowed HTML tags: <a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.
By submitting this form, you accept the Mollom privacy policy.