For the lack of documentation available focused on building custom compound widgets/fields for CCK, I've decided to write some based on personal work on building one such for use in my current project.
The widget that i'm building is a field to generate and select search results via user provided keywords. The end result given to the user in the node edit form is a field containing three elements:
- Textfield for Keywords.
- Submit Buttom to generate/refresh search results.
- Table of checkboxes for choosing individual search results.
For more information on the individual functions, themselves, please refer to CCK Hook Documentation
For this module we can get by with just three basic files (feel free to organize you code as you see fit, of course):
- a *.info file
- a *.install file
- a *.module file
For the info file i added just the basic information to tell Drupal about my module and that it is dependent on and grouped with CCK.
; $Id$ name = Search field description = Allows users to pull in content from search engines and then choose the results they wish to display dependencies[] = content package = CCK core = 6.x
For the .install file these functions are required to tell CCK that this is a new field to include and allocate db space for it. You do not need to worry about defining any schema at this point. We will define the schema for our field later on in this discussion. These following functions are what you need:
<?php // $Id$ // Notify CCK when this module is enabled, disabled, installed, // and uninstalled so CCK can do any necessary preparation or cleanup. /** * @file * Implementation of hook_install(). */ function search_widget_install() { drupal_load('module', 'content'); content_notify('install', 'search_widget'); } /** * Implementation of hook_uninstall(). */ function search_widget_uninstall() { drupal_load('module', 'content'); content_notify('uninstall', 'search_widget'); } /** * Implementation of hook_enable(). * * Notify content module when this module is enabled. */ function search_widget_enable() { drupal_load('module', 'content'); content_notify('enable', 'search_widget'); } /** * Implementation of hook_disable(). * * Notify content module when this module is disabled. */ function search_widget_disable() { drupal_load('module', 'content'); content_notify('disable', 'search_widget'); } ?>
I have divided the module code into three parts for discussion:
- Field Definition Functions
- Widget Definition Functions
- Theme Functions
Before we start, we should take into understanding the difference between fields and widgets according to how CCK organizes it's system.
Fields according to CCK are containers for data relating to a node. Widgets are the tools within a field that handle/manage the data for that field. More than one widget may exist within a field, which is what we would refer to as a compound field.
Widgets can get the data via many methods aside from just basic user input. It's up to the developer to decide how to get the data. Of course, after one understands the basics of building a custom field, what the widget/field can do is only limited to the developer's skill and imagination.
Okay, on to the code. We're going to take a look at this build as if we were building it from the outside-in.
First, in your module you will want define the field. Remember, at this point, we are not worried about defining any form elements. We only want to describe to Drupal and CCK what we're building.
Using CCK's hook_field_info() function we will give Drupal/CCK a brief description of our field. We want to return a nested array that is keyed by the machine-readable name for our field and within we'll provide a label, the human-readable name for the field which will appear in the field select box when adding a new field to a content type, and a description describing what our field is for.
<?php /** * Implementation of CCK's hook_field_info() * * Return an array containing basic information about our field */ function search_widget_field_info() { return array( 'search_widget' => array( 'label' => t('Search Engine Field'), 'description' => t('A widget tool to manage search results received from user provided keywords.'), ), ); }
The next function we will need is the hook_field_settings() function. Though CCK declares this function as optional I find that it is very much required if you really wish to do anything purely custom which is more than likely why you're reading this.
Within this function we may declare any field-specific settings and build their forms as well as declare how they are to be saved. We may also declare the structure of our database columns in terms of how the data from our widgets will look and be stored. Remember, that we are not building our form elements yet, just describing how and where the data will look and be stored.
/** * Implementation of CCK's hook_field_settings() * * Defines database storage for this field, plus any field settings forms. */ function search_widget_field_settings($op, $field) { switch ($op) { case 'form': return search_widget_field_settings_form($field); case 'save': return array('search_domain_settings'); case 'database columns': $columns['search_keywords'] = array( 'type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'sortable' => FALSE, 'default' => '', ); $columns['search_results'] = array( 'type' => 'varchar', 'length' => 9999, 'serialize' => TRUE, 'not null' => FALSE, 'sortable' => FALSE, 'default' => '', ); return $columns; } } /** * Generate the form for the field settings. * * return an array for generating a form to accept a url */ function search_widget_field_settings_form($field) { $form = array(); $form['search_domain_settings'] = array( '#type' => 'textfield', '#title' => t('Search Engine Domain Settings:'), '#description' => t('Enter the domain for your search engine. You must include the http:// declaration.'), '#default_value' => isset($field['search_domain_settings']) ? $field['search_domain_settings'] : 'Enter a valid Domain for you search engine.', ); return $form; }
Next we must invoke another required hook, the hook_field() function. Here, we can manipulate our field data at the same time as different actions on the node object, similar to the operation actions given in hook_nodeapi().
In my implementation of this hook, I've decided on just using the 'sanitize' op to help remove any possible XSS, etc. from the user provided keywords.
/** * Implementation of CCK's hook_field() * * Handles the database storage for the field * * We're also going to sanitize and clean the incoming search results * to make sure that we're not picking up any accidentally indexed porn * etc. */ function search_widget_field($op, $node, $field, &$items, $teaser, $page) { if ($op == 'sanitize') { search_widget_field_sanitize($node, $field, $items, $teaser, $page); } } /** * Sanitize the field data before inserting into the database */ function search_widget_field_sanitize($node, $field, &$items, $teaser, $page) { //scrub the user-provided keywords for any XSS, etc. return array(); }
For the last function in our field definitions we must include another function that is considered "optional" but in reality, CCK will WSOD you if the function is non-existant. This is the hook_content_is_empty() function.
Here we must tell CCK about what to do if our field is empty so that we don't end up with an unwanted/unexpected NULL value. In this instance I'm just returning a blank array to avoid any possible NULL value.
/** * Implementation of CCK's hook_content_is_empty() * * Provide some default value is the field is empty. */ function search_widget_content_is_empty($item, $field) { return array(); }
So, now since we have told Drupal/CCK all about our field we need to tell Drupal/CCK about our widgets to go inside the field.
First off, we need to tell Drupal/CCK about our widget and to what fields we want it to be available. We do so via the hook_widget_info() function.
In this function we want to return a nested array similar to what we returned in the hook_field_info() function in that it is an array keyed by the machine readable name of our widget followed by a label, the human-readable name of our widget, the field types to make our widget available to in an array containing the machine-readable name of the fields, and a description of our widget.
You'll also notice that i'm adding two attributes of callbacks and multiple values. There I'm telling CCK to handle multiple values of our widget as well as telling CCK that any/or all callbacks will be handled by myself.
/**----------- Widget Information -------------------**/ /** * Implementation of CCK's hook_widget_info() * * Returns basic information about our widget */ function search_widget_widget_info() { return array( 'search_widget' => array( 'label' => t('Search Keywords, Results'), 'field types' => array('search_widget'), 'multiple values' => CONTENT_HANDLE_CORE, 'callbacks' => array('default value' => CONTENT_CALLBACK_CUSTOM), 'description' => t('An edit widget for the Search Engine fields that accepts keywords and returns selectable search results.'), ), ); }
Now, for the fun part! Here, we get to actually build our FAPI form elements that will appear in the node edit form to retrieve our data.
The form elements that I'm putting together are the three elements that i described at the beginning of the article.
As a note, many articles concerning custom fields and compound fields have demonstrated such using hook_elements() and other processing functions to build the form elements. Having worked using that method I've found that it actually is another layer of abstraction that is really not necessary for it adds a layer of unneeded complexity. So, I actually build my FAPI form elements here in hook_widget().
When you're constructing you form elements, remember that they can only be one level deep in the array and must use the corresponding name that was given in the name of the database column declared in the hook_field_settings() function. If not, your data will not save at all.
Your default data for the form elements resides in the $items array.
/** * Implementation of CCK's hook_widget() * * Return a skeleton FAPI array that defines callbacks * for the widget form. * * @todo looks like i'm going to have to delve into the world of sessions * to be able to rectify a unique identifier for our content if the node * has not been created i.e. saved to the database. */ function search_widget_widget(&$form, &$form_state, $field, $items, $delta = 0) { $element = array(); $value = variable_get($form['created']['#value'], NULL); var_dump($form['created']['#value']); if ($value) { $options = $value; } else if (isset($form_state['results_options']['value'])) { $options = $form_state['results_options']['value']; } else { $options = array('none' => t('N/A')); } $element['search_keywords'] = array( '#type' => 'textfield', '#title' => t('Keywords'), '#default_value' => isset($items[$delta]['search_keywords']) ? $items[$delta]['search_keywords'] : NULL, '#autocomplete_path' => $element['#autocomplete_path'], '#size' => !empty($field['widget']['size']) ? $field['widget']['size'] : 60, '#attributes' => array('class' => 'search_engine_widget'), '#maxlength' => !empty($field['max_length']) ? $field['max_length'] : NULL, ); $element['generate_results'] = array( '#type' => 'submit', '#value' => t('Generate Results'), '#submit' => array('search_widget_get_results_submit'), ); $element['search_results'] = array( '#type' => 'checkboxes', '#title' => t('Search Results'), '#default_value' => isset($items[$delta]['search_results']) ? unserialize($items[$delta]['search_results']) : array(), '#options' => $options, ); // Used so that hook_field('validate') knows where to // flag an error in deeply nested forms. if (empty($form['#parents'])) { $form['#parents'] = array(); } $element['_error_element'] = array( '#type' => 'value', '#value' => implode('][', array_merge($form['#parents'], array('search_keywords'))), ); return $element; }
Just as with the field settings, we may also provide the same for our widget by implementing hook_widget_settings(). In this case, I am using the function to provide a method for validating data within my widget.
function search_widget_widget_settings($op, $widget) { switch ($op) { case 'validate': return search_widget_widget_settings_validate($widget); } } function search_widget_widget_settings_validate($widget) { return array(); } /** * Custom submit function * * Take the current keywords given, retrieve the results from the * search engine, and return them as the options for the checkboxes * on the node form. Rebuild node form * * @todo: find a true unique indentifier for a node that has not been created yet. */ function search_widget_get_results_submit($form, &$form_state) { if ($form_state['values']['op'] == 'Generate Results') { //var_dump($form_state['values']); $search_links = rocket_server_get_links_from_search(NULL, $form_state['values']['field_search_engine_widget'][0]['search_keywords']); $i = 1; foreach ($search_links as $link) { $options_array[$i] = l($link['title'], $link['url']); $i++; } variable_set($form_state['values']['created'], $options_array); $form_state['results_options']['value'] = $options_array; $form_state['rebuild'] = TRUE; } }
After we have built our field and widget and both are working as planned we need to tell Drupal how to present our data in the node.
Unfortunately, for this example, I have not fully completed the basic theming for this field in the currently system where i'm implementing this field i am handling the data presentation differently as opposed to Drupal's default theming. But, of course when I complete the code i will update the site with the new code and a little discussion about it.
/**----------- Theming Functions --------------------**/ /** * Implementation of hook_theme() * * Register themeable elements for widgets and formatters */ function search_widget_theme() { return array( 'search_keywords' => array( 'arguments' => array('element' => NULL), ), 'search_results' => array( 'arguments' => array('element' => NULL), ), ); } function theme_search_keywords(&$element) { return drupal_render($element); } function theme_search_results(&$element) { return theme('form_element', $element, $element['#children']); }
One last thing that I would like to point out in the discussion is that the widget fails to save the selected search results on the initial creation of the node because at the initial creation there is not a unique identifier for the node because the node has not been officially created at that point. However, the widget works perfectly after the node has been officially created.
The main reason for saving the search results is that we must save an "image" if you will of the results as they came in originally so that we may keep the results that the user selected in-line with the original selection otherwise it will all be lost as the search engine indexes new content and pushes older content down the line.
For more articles/information on building custom CCK widgets or for more information on the options and functionality available as well, please check out these sites:
Poplarware: Creating a Compound Field
Lullabot: Creating Custom CCK Fields
Sat, 01/16/2010 - 19:41
I was surprised about the hook_elements implementation inside the hook_widget. Thanks alot for posting this. :)
BTW have you finished the hook_theme implementation yet?
Alex
Mon, 01/18/2010 - 08:46
I've finished a good bit of work with the cck_hook_theme function I just haven't had the time to put up a post on what i did.
but, since you asked... I'll get one up for you.
thanks! :)