Update: Feb 17, 2010 This article was written nearly a year ago when CTools was still in alpha. I don't know how much of it applies anymore but I've been told there's an up to date plugin example that ships with CTools now so you'll want to look to that as an example.
Note: This article requires the current Panels 3 / CTools development snapshots (or the beta when it comes out) as the alphas do not contain the recent changes to content types.
When you open the dialog to add content to your panel, there are many items already in there which come from core and contributed modules you have installed. But what if you want to add something new? There are a couple of options.
The simple way is to do it right through the UI with the option to add "New custom content" but this is only recommended if you just need to add a few lines of text or want to test something out. For dynamic content, you'll want to make your own content type. A content type is one of several types of CTools plugins along with "relationships", "layouts", and more. It is a discrete bit of content, defined in code, that can be added as a pane to your panel. You define the content type in a module and then it is available to the Panels UI. This article will walk you through creating a content type using my Author Pane as an example. You can also look at ctools/plugins/content_types for examples that come with CTools.
Note that you need to create content types inside of a module. If you have a site module for customizations you can use that or you can create a module just for this.
Step 1: Name your content type
Each content type needs a distinct name that will not conflict with any other content type name, including those coming from other contributed modules. Because of this, it's best to include your module's name in the content type name. If your module is only providing one content type, you can simply use your module name for the content type name. In Author Pane, I only create one content type so I named it simply "author_pane".
Step 2: Create the file and tell CTools where to find it
Your content type needs to live in an include file of the same name. In my case, author_pane.inc. You should put this in a directory that only contains content types. The recommended directory structure is: moduledir/plugins/content_types. For Author Pane, the complete path with file is author_pane/plugins/content_types/author_pane.inc
Once you have the file and directories set up, you need to tell CTools where your plugins are. If you followed the recommended directory structure, you can simply drop this code into your module (be sure to change MODULE_NAME to your module's name.):
/**
* Implementation of hook_ctools_plugin_directory().
*/
function MODULE_NAME_ctools_plugin_directory($module, $plugin) {
if ($module == 'ctools') {
return 'plugins/' . $plugin;
}
}
?>
This code will look for content types in plugins/content_types, relationships in plugins/relationships, and so on for all the various plugins. Don't worry about creating the other directories if your module doesn't make any other plugin types.
Step 3: Create the content type
The rest of this will walk you through the Author Pane content type, explaining what each bit does, and how you should change it for your own content type. All of these functions go in the ct_name.inc file you created in the last step.
Tell CTools about your content type:
/**
* Callback function to supply a list of content types.
*/
function author_pane_author_pane_ctools_content_types() {
return array(
'single' => TRUE,
'title' => t('Author Pane'),
'icon' => 'icon_user.png',
'description' => t('Author related variables gathered from helper modules.'),
'required context' => new ctools_context_required(t('User'), 'user'),
'category' => t('Advanced Profile Kit'),
'defaults' => array('image_path' => '', 'template_file' => 'author-pane'),
);
}
?>
Here's what each bit in that code means:
- The function name is
MODULE_NAME_CT_NAME_ctools_content_types. Because our module name and content type name are the same, we have "author_pane" twice. This looks a little funny but doesn't hurt anything. - We are only creating one content type so we set 'single' to true.
- The title is the name that shows up in the add content dialog.
- The icon is the icon that shows next to the name that shows up in the add content dialog. Since this is user related, we are using the user icon. This icon is in CTools and you will either need to copy it to your plugin directory or put the full path to it (or make your own icon). If you don't provide an icon, CTools will provide a default.
- The description shows up when you hover over the item in the add content dialog.
- Required context is what context this content type needs to work. In this case, we need the user object so we make use of the user context. If the Panel does not have a user context on it, our content type will not show up as an option to add.
- The category defines where our content type shows up in the add content dialog. For a user related item, you could use simply "User". In this case, I wanted it to be grouped with other items provided by Advanced Profile Kit as that is where it will most commonly be used. If you are creating this content type for your own site, you can use whatever category you like. If you are creating it for others to use, be careful what you choose here to avoid cluttering the add content dialog.
- Defaults is an array that contains the default values, if any, for the items on your add/edit form.
Tell CTools how to display your content type:
/**
* Output function for the 'author pane' content type.
*/
// The function name is MODULE_NAME_CT_NAME_content_type_render
function author_pane_author_pane_content_type_render($subtype, $conf, $panel_args, $context) {
// $context in this case is a user context, so we can get the user object
// from it and put it into $account.
$account = isset($context->data) ? drupal_clone($context->data) : NULL;
// Make a new empty "block" which will be a Pane you can add to your Panel.
$block = new stdClass();
if ($account) {
// Set the title of the block to the name of the user. It can be overridden
// through the UI as well.
$block->title = check_plain($account->name);
// Call the function that makes the author pane and use that for the block
// content. In our case, this is just one line but you can have whatever
// code you need to assemble the content and then assign it to the
// $block->content variable.
$block->content = theme('author_pane', $account, $conf['image_path'], $conf['template_file']);
}
else {
// If somehow the user context is empty, this is a fallback message but
// that should never happen.
$block->content = "User information not available";
}
return $block;
}
?>
Define the settings form for the pane:
/**
* Returns an edit form for the custom type.
*/
// The function name is MODULE_NAME_CT_NAME_content_type_edit_form
function author_pane_author_pane_content_type_edit_form(&$form, &$form_state) {
// The current configuration
$conf = $form_state['conf'];
// This and the next one are normal FAPI form making.
$form['image_path'] = array(
'#type' => 'textfield',
'#title' => t('Image directory'),
'#size' => 50,
'#description' => t('Full path to image directory, not including leading or trailing slashes. Use [theme_path] to substitute the active theme\'s path. If left blank the images in the module directory will be used.'),
'#default_value' => $conf['image_path'],
'#prefix' => '
'#suffix' => '
',
);
$form['template_file'] = array(
'#type' => 'textfield',
'#title' => t('Template file'),
'#size' => 50,
'#description' => t('Template file to use for the author pane.'),
'#default_value' => $conf['template_file'],
'#prefix' => '
'#suffix' => '
',
);
}
?>
Prepare the settings form for submission.
function author_pane_author_pane_content_type_edit_form_submit(&$form, &$form_state) {
// For each part of the form defined in the 'defaults' array set when you
// defined the content type, copy the value from the form into the array
// of items to be saved. We don't ever want to use
// $form_state['conf'] = $form_state['values'] because values contains
// buttons, form id and other items we don't want stored. CTools will handle
// the actual form submission.
foreach (array_keys($form_state['plugin']['defaults']) as $key) {
$form_state['conf'][$key] = $form_state['values'][$key];
}
}
?>
Define the title displayed on the layout page of the panel:
This is the title you see when you are editing the Panel. It is not the same as the title
of the pane when you are viewing the panel, which is defined in the render function.
function author_pane_author_pane_content_type_admin_title($subtype, $conf, $context) {
return t('"@s" author pane', array('@s' => $context->identifier));
}
?>
Explore the possibilities
This example has shown the most common bits you will need to create a content type. With it, you can make use of the panel's context as well as settings on the pane and your own code to come up with your own dynamic, custom content. This lives in a file that can be checked into version control rather than just being set in the database. You can also include it with a module for distribution to share with others. From here, there are more complicated things you can do such as multi-page settings and more.
If you maintain a contributed module, consider what parts of it could be encapsulated into content types to make it easier for your users to add them to their own panels. While Panels will make use of blocks you provide, that is only part of the story. Making it a true content type gives you more options for integration such as showing content for a particular user based on the user context or a node based on the node context.
The possibilities are endless.
Much credit goes to merlinofchaos who helped me quite a bit in writing this as well as converting my content types from Panels 2
Comments
April 22: I've incorporated
April 22: I've incorporated these comments except for the very last one since I haven't had a chance to try that out, yet.
Earl made some comments on IRC but my son is up from his nap so I can't work them into the article just yet. Pasting them here (with permission) until I have a chance to update the article.
[14:31] Michelle: Hm. Step 1: You need a module. =)
[14:32] Michelle: Ok. For the 'icon' it assumes it will be in the same directory as the plugin. If it is not you'll have to provide the full path to the icon. If you have no icon CTools provides a default, bland icon for you.
[14:34] $block->module = 'author_pane'; is no longer necessary -- it's a relic from when we just used theme('block') to display panes. You can eliminate it.
[14:36] I would also group the _submit with the form, since they are related. Having the _admin_title between the form and the _submit is minorly confusing. One last thing you missed is that there's a new _admin_info so you can specify what appears in the box on the drag & drop page. For example, the view type displays some of its config. Blocks just show you the block.
Thanks for the corrections, Earl!
Michelle
Michelle, thanks so much for
Michelle, thanks so much for bringing this to us live!
CTools is definitely part of the Drupal Quiet Revolution, and amidst all the growing pains and work and daily obligations we all have, it is something that up till now has resided on my "got to get around to seeing that" list.
Despite your own obligations, I really thank you for making this effort and for socializing it, making it available to all of us. A real ray of sunshine!
Thanks!
Victor Kane
http://awebfactory.com.ar
http://projectflowandtracker.com
@victorkane: Glad you like
@victorkane: Glad you like it. Content types are one of the most useful (to me) parts of panels. And they really aren't hard to make once you grasp it. It took me a while (and a lot of help from Earl) to get past the hump and get everything named correctly so I wanted to help everyone else along by writing it up while it was still fresh in my mind.
Michelle
Added to DrupalSightings.com
Added to DrupalSightings.com
Thanks for this article, it
Thanks for this article, it really helped a lot!
In order to get this example working in panels-6.x-3.0-alpha4, I had to add the key 'title callback' to the array returned by author_pane_author_pane_ctools_content_types():
function author_pane_author_pane_ctools_content_types() {
return array(
'single' => TRUE,
'title' => t('Author Pane'),
// NEW *REQUIRED* CALLBACK - otherwise you get "Deleted/missing content type" errors
'title callback'=>'author_pane_author_pane_title_callback',
'icon' => 'icon_user.png',
'description' => t('Author related variables gathered from helper modules.'),
'required context' => new ctools_context_required(t('User'), 'user'),
'category' => t('Advanced Profile Kit'),
'defaults' => array('image_path' => '', 'template_file' => 'author-pane'),
);
}
// callback for the title ...
function author_pane_author_pane_title_callback($subtype, $conf, $context, $incoming_content) {
}
Best, Fredrik!
@Fredrik: This article is not
@Fredrik: This article is not meant for alpha 4. There have been a lot of changes since then. That's why I have the bold message at the top. ;)
Michelle
ouch, indeed! oops, sorry, my
ouch, indeed! oops, sorry, my mistake :)
Thanks a lot for this it was
Thanks a lot for this it was very helpfull :)
Do you have the same kind of tutorial for migrating panels2 context relationship to the new ctools-dev API ?
Because I have patched the content profile module for use with panels 3 (but in alpha), and i supposed that for the upcoming beta release (and the current dev snapshot) thing must have changed.
Here is my patch for using content profile with panels 3 ALPHA
? .svn
? content_profile_panels3.patch
? modules/.svn
? tests/.svn
? views/.svn
Index: sites/all/modules/content_profile/content_profile.panels.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/content_profile/Attic/content_profile.panels.inc,v
retrieving revision 1.1.2.1
diff -u -p -r1.1.2.1 sites/all/modules/content_profile/content_profile.panels.inc
--- sites/all/modules/content_profile/content_profile.panels.inc 4 Jan 2009 12:02:19 -0000 1.1.2.1
+++ sites/all/modules/content_profile/content_profile.panels.inc 16 Apr 2009 13:42:30 -0000
@@ -9,15 +9,15 @@
/**
* Plugin to provide an relationship handler for node from user
*/
-function content_profile_panels_relationships() {
+function content_profile_ctools_relationships() {
$args['node_from_user'] = array(
'title' => t("Profile Node from user"),
'keyword' => 'content_profile',
'description' => t('Adds a Content Profile from user context'),
- 'required context' => new panels_required_context(t('User'), 'user'),
+ 'required context' => new ctools_context_required(t('User'), 'user'),
'context' => 'content_profile_panels_context',
- 'settings form' => 'content_profile_panels_settings_form',
- 'settings form validate' => 'content_profile_panels_settings_form_validate',
+ 'settings form' => 'content_profile_ctools_settings_form',
+ 'settings form validate' => 'content_profile_ctools_settings_form_validate',
);
return $args;
}
@@ -25,10 +25,12 @@ function content_profile_panels_relation
/**
* Return a new context based on an existing context
*/
-function content_profile_panels_context($context = NULL, $conf) {
+function content_profile_ctools_context($context = NULL, $conf) {
+
+
// If unset it wants a generic, unfilled context, which is just NULL
if (empty($context->data)) {
- return panels_context_create_empty('node', NULL);
+ return ctools_context_create_empty('node', NULL);
}
if (isset($context->data->uid)) {
@@ -37,17 +39,17 @@ function content_profile_panels_context(
$content_profile_node = content_profile_load($conf['type'], $uid);
// Send it to panels
- return panels_context_create('node', $content_profile_node);
+ return ctools_context_create('node', $content_profile_node);
}
else {
- return panels_context_create_empty('node', NULL);
+ return ctools_context_create_empty('node', NULL);
}
}
/**
* Settings form for the relationship
*/
-function content_profile_panels_settings_form($conf) {
+function content_profile_ctools_settings_form($conf) {
$options = content_profile_get_types('names');
$form['type'] = array(
'#type' => 'select',
No, this is all I've written.
No, this is all I've written. You should be able to look at the instructions / example here and compare it against what you have now to see the differences.
Michelle
Thanks Michelle! This article
Thanks Michelle! This article was very useful. I had to created a content type to be able to put the breadcrumb trail in a panel pane.
Hi Michelle and very thanks
Hi Michelle and very thanks for this article.
I've a problem with context..in UI selector for context on my panel page I didn't get 'user' (only 'node', 'node add form', 'node edit form', taxonomy term' and 'taxonomy vocabulary').
I have to remove this line:
'required context' => new ctools_context_required(t('User'), 'user'),
for content type to appear.
I'm using version 6.x-3.0-beta2, so what I'm missing?
Thanks!
Luca
@lussoluca: This article
@lussoluca: This article walks through an example that uses the user context. If you are using the node context then, yes, you'd need to change that to node.
Michelle
Greatly written indeed… I
Greatly written indeed… I really enjoyed your article and found it to be very informative, keep up the good work, I’ll be coming back to read any of your future articles..
Thank you
rapida
Really helpful. I had a need
Really helpful. I had a need to do this and it worked perfectly. I had a question. in the plugins directory can you have multiple inc files? and then name the hooks differently?
Brian.
I'm sorry but I haven't
I'm sorry but I haven't really had my head in Panels since I wrote this so it's all rather fuzzy and I don't remember. I guess try it and see. :)
Michelle