home » Blog » The Least You Need to Know: Creating a Ctools Content Pane

The Least You Need to Know: Creating a Ctools Content Pane

Creating a Ctools content pane doesn’t have to be a pain, I’ll help you out by giving you the least you need to know in order to get off the ground. If you want to just cut to the chase (read: copy-and-paste) you can view the source for the module created here on GitHub. Just to save you some time, I’m using Drupal 7 and ctools 7.x-1.7, as long as you are using Drupal 7 I’m sure this information will be good, if you’re using Drupal 8+ you should probably find a more up to date tutorial.

So, let’s get started! First off, you’ll have to create a module, I won’t spend time explaining this, if you’re here and not at a “How to create a Drupal module” tutorial then there should be no surprises here.

painlesspane.info

name = Painless Pane
description = A simple example of a Ctools Content Pane
version = 0.1
core = 7.x

dependencies[] = ctools

Now, we need to tell Chaos Tools about our pane, to do that we need to implement hook_ctools_plugin_directory() so ctools will scan it and pick up the plugins defined in it. Again, this code is fairly simple and self explanatory. Your module path is the base path, and you’re telling ctools to look in “plugins/content_types ” to find your plugin definition.

in painlesspane.module 

/*
* Tell ctools about your plugin.
* You should have a file in plugins/content_types
*/
function painlesspane_ctools_plugin_directory($owner, $plugin_type) {
if($owner == 'ctools' and $plugin_type == 'content_types') {
return 'plugins/' . $plugin_type;
}
}

Now that Chaos Tools knows to look for your plugin, you need to add the definition file. Create a file at /plugins/content_types/painlesspain.inc. The file name can be anything you want, it doesn’t have to match the module name, but if you’re only defining one plugin (like we are here) it’s a good idea to have it be the module name, just to keep things simple.

/plugins/content_types/painlesspain.inc

<?php

$plugin = array(
'single' => TRUE,
'title' => t('Add a Painless Pane'),
'description' => t('Add a pane, painlessly!'),
'category' => t('Add All the Panes!'),
'edit form' => 'painlesspane_pane_edit_form',
'render callback' => 'painlesspane_pane_render',
);

This is pretty simple, the title and description are pretty self explanatory. The ‘edit form’ parameter contains the name of a callback that will define the settings form that will be displayed when you create an instance of the Painless Pane. ‘render callback’ likewise contains the name of a callback that will be used to render the pane. The ‘single’ parameter is a little more advanced and thus beyond the scope of “least you need to know”, for now just set it to TRUE.

Now you need to define the edit form, its submit handler, and the render callback. These can either be in the plugin definition file, or in the module file, depending on what makes the most sense for your project. If the code for your pane isn’t used by any other code in your module then you should probably put it in the plugin definition file for easy reuse. Add those three functions to painlesspain.inc

/*
* Make sure not to overwrite the $form variable, but to add to it.
* ctools will put in a submit button and the title override field.
* $form_state['conf'] will be auto-populated based on previously
* saved values.
*/
function painlesspane_pane_edit_form($form, &$form_state) {
$form['name'] = array(
'#type' => 'textfield',
'#title' => t('What is your name?'),
'#default_value' => $form_state['conf']['name'],
);
return $form;
}

/*
* Handle the submitted data. $form_state['conf'] is automatically handled
* by ctools. It will save the data and provide it to the correct pane.
*/
function painlesspane_pane_edit_form_submit(&$form, &$form_state) {
if(isset($form_state['values']['name'])) {
$form_state['conf']['name'] = $form_state['values']['name'];
}
}

/*
* Renders the pane content, the $conf variable corresponds to the
* $form_state['conf'] from the submit handler.
*/
function painlesspane_pane_render($subtype, $conf, $args, $contexts) {
$block = new stdClass();
$block->title = 'Hi, name is:';
$block->content = $conf['name'];
return $block;
}

If you’ve done any Drupal development most of this code should be pretty familiar to you. painlesspane_pane_edit_form defines a form using the Drupal 7 Form API. CTools will add a few fields before passing you the $form array, so don’t overwrite it, just add to it. Also, and this can be kind of a gotcha, do not implment a submit button! Ctools will add a submit button for you, and if you try to create your own you’ll just mess things up.

As the comments explain, $form_state[‘conf’] will be auto-populated by ctools. This is assuming you are using it to store your configuration information, which you probably should since it’s easy and managing storage sucks. In my code you can see that I add a single field called name, which asks you to enter your name. In the submit handler I check to make sure a name has been entered, and if it has I add it to the $form_state[‘conf’] array. Then, in the render function I create an object named $block; which is mostly by convention, everyone seems to name it $block, but really it could be anything. Set the title and content properties, here I just set $block->content to $conf[‘name’].

So..that’s it! If you’ve done everyting correctly you should be able to enable your module, flush your cache, go into Panels and see something like this:

Adding the pane.

When you add the pane you see the form defined in painlesspane_pane_edit_form:

The Add a Painless Pane form

When submitted the panels UI should look like this:

Panels UI

Notice that the module name is the description of the pane, I’ll talk about that more later. When you save this Panel you should see this output on the page the panel creates:

Rendered Output


Well, assuming your name is also Jason Mickela, that’s what you’ll see. You may have entered some other name for some reason.

Now, let’s say you want to add a second instance of this pane with some other name in it. I’m not doing anything in my code to take multiple instances into account. I’m not generating any ids, I’m not storing anything with unique names. Let’s see what happens:

Some other name?

So far so good…

Two Painless Panes

Well, it worked, in the sense that no errors were thrown. But clearly this isn’t exactly what you would want. I’ll address this later, but for now I want to stick to the LEAST you NEED to get your output on the page. Speaking of output, let’s check out the rendered output:

Two rendered outputs

It worked! Great, it did what it was supposed to do. Without writing any code to take multiple values into account Ctools managed to correctly store both values and supply them to the panes correctly. That’s the beauty of using the $form_state[‘conf’] variable to store the data you need for your pane.

Ok, this is all great, now you know the least you need to know in order to create a Ctools Content Pane, but it would be nice if you could see which pane was which from the Panels UI that way you can tell them apart without having to remember what order they are in or having to check the rendered page to see which is which. So now I’ll show you just a little more. I’ll add an admin info call back, and also add a default value for name just for fun and because you’ll probably want to add sensible defaults for your pane. To do this I have to edit my plugin definition.

$plugin = array(
'single' => TRUE,
'title' => t('Add a Painless Pane'),
'description' => t('Add a pane, painlessly!'),
'category' => t('Add All the Panes!'),
'edit form' => 'painlesspane_pane_edit_form',
'render callback' => 'painlesspane_pane_render',

//Just a little more:
'admin info' => 'painlesspane_pane_admin_info', //Provide information about your pane to give some context in the ui
'defaults' => array( //Give defaults for common options
'name' => 'rootid',
),
);

So, I’ve added a defaults array and provided a default value for the name field. This works exactly how you would expect it to. I also defined a new callback: painlesspane_pane_admin_info. You can see that definition here:

/*
* Just a little more:
* Provide some context for the end user so they can tell the difference between
* two panes of the same type.
*/
function painlesspane_pane_admin_info($subtype, $conf, $contexts) {
if (!empty($conf)) {
$block = new stdClass();
$block->title = $conf['name'];
$block->content = "That's all I have to say about that.";
return $block;
}
}

This is very simpilar to the render callback. Flush your cache and now your Panel should look like this:

Better Panels UI output

Now, instead of the module name, the value of $form_state[‘conf’][‘name’] is used as the title of the pane in the Panels UI. This is much more helpful.

Well, that’s it! Hopefully I haven’t left anything out. If you get hung up, or something that I claim is simple turns out to not work for you, please leave a comment below so I can fix it!