Compound fields in Drupal 7

With Field API being part of core as of Drupal 7.0, creating custom fields just became a lot easier than they were in Drupal 6.x. Suppose you need to create a compound field, which consists of several different form elements (a select and several text fields). You can now have a basic compound field in three basic steps:

Tobby Hagler, Director of Engineering
#Drupal | Posted

With Field API being part of core as of Drupal 7.0, creating custom fields just became a lot easier than they were in Drupal 6.x. Suppose you need to create a compound field, which consists of several different form elements (a select and several text fields). You can now have a basic compound field in three basic steps:

  • Define the field (info and schema)
  • Define the field form (widget)
  • Define the output (formatter)

Note: Much of what will be discussed here can be found in the Field Example module, but with some much-needed explanations.

Step 1: Tell Drupal about the field

As with most APIs in Drupal, there are hooks that are required in order to tell Drupal about what it is you're creating. The hooks you'll need to initially define your field are:

  • hook_field_info() -- The initial hook that defines which widget and formatter elements to use for the field
  • hook_field_schema() -- The schema for storing your field information. This is similar to hook_schema() for modules, and also belongs in your module's .info file.
  • hook_field_validate() -- A validation function that will validate all input from the field forms.
  • hook_field_is_empty() -- Determine if your field has data, so that you may or may not be able to modify the widget later.

hook_field_info()

  1. function dnd_fields_field_info() {
  2. return array(
  3. 'dnd_fields_ability' => array(
  4. 'label' => t('D&D ability'),
  5. 'description' => t("This field stores PC ability scores"),
  6. 'settings' => array('allowed_values' => array(), 'allowed_values_function' => ''),
  7. 'default_widget' => 'dnd_fields_ability',
  8. 'default_formatter' => 'dnd_fields_ability', // This doesn't *have* to be the same name as default_widget's value, this is only coincidence
  9. ),
  10. 'dnd_fields_skill' => array(
  11. 'label' => t('D&D skill'),
  12. 'description' => t("This field stores PC skill values"),
  13. 'settings' => array('allowed_values' => array(), 'allowed_values_function' => ''),
  14. 'default_widget' => 'dnd_fields_skill',
  15. 'default_formatter' => 'dnd_fields_skill',
  16. ),
  17.  
  18. );
  19. }

This hook defines the initial widget. It defines the widget and formatter internally, but it also contains what will be displayed in the Manage Fields page of your content type admin UI. The default_widget and default_formatter aren't functions, they are operations that will be passed to hooks later. The settings element will let you set additional (optional) functions to handle default values, which may come in handy if you want the values of a select field to come from your database.

hook_field_schema()

Note: This goes in your .install file.

 

  1. function dnd_fields_field_schema($field) {
  2. switch($field['type']) {
  3. case 'dnd_fields_ability':
  4. $columns = array(
  5. 'ability' => array(
  6. 'type' => 'varchar',
  7. 'length' => '32',
  8. 'not null' => FALSE,
  9. ),
  10. 'score' => array(
  11. 'type' => 'int',
  12. 'size' => 'small',
  13. 'not null' => TRUE,
  14. 'default' => 0,
  15. ),
  16. 'mod' => array(
  17. 'type' => 'int',
  18. 'size' => 'small',
  19. 'not null' => TRUE,
  20. 'default' => 0,
  21. ),
  22. 'tempscore' => array(
  23. 'type' => 'int',
  24. 'size' => 'small',
  25. 'not null' => TRUE,
  26. 'default' => 0,
  27. ),
  28. 'tempmod' => array(
  29. 'type' => 'int',
  30. 'size' => 'small',
  31. 'not null' => TRUE,
  32. 'default' => 0,
  33. ),
  34. );
  35. break;
  36. case 'dnd_fields_skill':
  37. $columns = array(
  38. 'skill' => array(
  39. 'type' => 'varchar',
  40. 'length' => '128',
  41. 'not null' => FALSE,
  42. ),
  43. 'ranks' => array(
  44. 'type' => 'int',
  45. 'size' => 'small',
  46. 'not null' => TRUE,
  47. 'default' => 0,
  48. ),
  49. );
  50. $indexes = array(
  51. 'skill' => array('skill'),
  52. );
  53. break;
  54. }
  55. return array(
  56. 'columns' => $columns,
  57. 'indexes' => $indexes,
  58. );
  59. }

 

This is identical to hook_schema() in the sense that syntax and data types are the same. For each field element in your widget, you will need a corresponding column (unless of course you are combining data from multiple input fields).

hook_field_validate()

  1. function dnd_fields_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {
  2. foreach ($items as $delta => $item) {
  3. // ...
  4. }
  5. }

hook_field_is_empty()

 

  1. function dnd_fields_field_is_empty($item, $field) {
  2. $temp = array_keys($field['columns']);
  3. $key = array_shift($temp);
  4. return empty($item[$key]);
  5. }

In the simplest form, this hook tells Drupal that your field is or is not empty. This is checked when a site administrator attempts to modify an existing widget. In cases when there is existing field data, it is unadvisable to modify field settings, such as the list of allowed values (since the content in your existing fields will not be re-validated).

Step 2: Create the field widget

The next step is to create the field widget. This is essentially the form (using Form API) for user input as well as any interface elements that you wish to create. The hooks needed to create the field widget are:

hook_field_widget_info()

This hook tells Drupal enough information about your widget in order to start using the field and to accept user input.

  1. function dnd_fields_field_widget_info() {
  2. return array(
  3. 'dnd_fields_ability' => array(
  4. 'label' => t('D&D ability score'),
  5. 'field types' => array('dnd_fields_ability'),
  6. ),
  7. 'dnd_fields_skill' => array(
  8. 'label' => t('D&D skill values'),
  9. 'field types' => array('dnd_fields_skill'),
  10. ),
  11. );
  12. }

At minimum, this returns a label that is displayed when selecting this widget from the Manage fields page while editing a content type. It also ties together that label to the field types, which are defined next.

hook_field_widget_form()

  1. function dnd_fields_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  2. switch ($instance['widget']['type']) {
  3. case 'dnd_fields_ability':
  4. $settings = $form_state['field'][$instance['field_name']][$langcode]['field']['settings'];
  5.  
  6. $fields = array(
  7. 'ability' => t('Ability'),
  8. 'score' => t('Score'),
  9. 'mod' => t('Modifier'),
  10. 'tempscore' => t('Temp score'),
  11. 'tempmod' => t('Temp modifier'),
  12. );
  13.  
  14. $abilities = (!empty($field['settings']['abilities'])) ? explode("n", $field['settings']['abilities']) : array();
  15.  
  16. foreach ($fields as $key => $label) {
  17. $value = isset($items[$delta][$key]) ? $items[$delta][$key] : '';
  18. if (empty($value) && $key == 'ability') {
  19. $value = $abilities[$delta];
  20. }
  21.  
  22. $element[$key] = array(
  23. '#attributes' => array('class' => array('edit-dnd-fields-ability'), 'title' => t(''), 'rel' => strtolower($abilities[$delta])),
  24. '#type' => 'textfield',
  25. '#size' => 3,
  26. '#maxlength' => 3,
  27. '#title' => $label,
  28. '#default_value' => $value,
  29. '#attached' => array(
  30. 'css' => array(drupal_get_path('module', 'dnd_fields') . '/dnd_fields.css'),
  31. 'js' => array(drupal_get_path('module', 'dnd_fields') . '/dnd_fields.js'),
  32. ),
  33. '#prefix' => '<div class="dnd-fields-ability-field dnd-fields-ability-' . $key . '-field dnd-fields-ability-' . $key . '-' . strtolower($abilities[$delta]) . '-field">',
  34. '#suffix' => '</div>',
  35. );
  36. if ($key == 'ability') {
  37. $element[$key]['#size'] = 10;
  38. $element[$key]['#maxlength'] = 32;
  39. if (arg(0) != 'admin') {
  40. $element[$key]['#attributes'] = array('readonly' => 'readonly');
  41. }
  42. }
  43. }
  44. break;
  45. case 'dnd_fields_skill':
  46. $settings = $form_state['field'][$instance['field_name']][$langcode]['field']['settings'];
  47.  
  48. // Get the list of skills broken into an array, and split those elements into a
  49. // multi-dimensional arrays
  50. $skills_temp = (!empty($settings['skill'])) ? preg_split('/(rn?|n)/', $settings['skill']) : array();
  51. $skills = array(0 => t('-Choose a skill-'));
  52. foreach ($skills_temp as $skill) {
  53. if(strpos($skill, '|') === FALSE) {
  54. $skills[] = array($skill);
  55. }
  56. else {
  57. $temp = explode('|', $skill);
  58. $skills[$temp[0]] = $temp[1];
  59. }
  60. }
  61.  
  62. $element['skill'] = array(
  63. '#attributes' => array('class' => array('edit-dnd-fields-skill'), 'title' => t('')),
  64. '#type' => 'select',
  65. '#options' => $skills,
  66. '#title' => t('Skill name'),
  67. '#description' => t('Choose a skill you wish to allocate ranks to.'),
  68. '#attached' => array(
  69. 'css' => array(drupal_get_path('module', 'dnd_fields') . '/dnd_fields.css'),
  70. 'js' => array(drupal_get_path('module', 'dnd_fields') . '/dnd_fields.js'),
  71. ),
  72. '#prefix' => '<div class="dnd-fields-skill-field dnd-fields-skill-skill-field">',
  73. '#suffix' => '</div>',
  74. );
  75. $element['ranks'] = array(
  76. '#attributes' => array('class' => array('edit-dnd-fields-ranks'), 'title' => t('')),
  77. '#type' => 'textfield',
  78. '#size' => 3,
  79. '#maxlength' => 3,
  80. '#title' => t('Skill ranks'),
  81. '#prefix' => '<div class="dnd-fields-skill-field dnd-fields-skill-ranks-field">',
  82. '#suffix' => '</div>',
  83. );
  84. // Loop through all the element children and set a default value if we have one. Then set HTML wrappers.
  85. foreach (element_children($element) as $element_key) {
  86. $value = isset($items[$delta][$element_key]) ? $items[$delta][$element_key] : '';
  87. $element[$element_key]['#default_value'] = $value;
  88. }
  89. break;
  90.  
  91. }
  92. return $element;
  93. }

With this hook, you now have something tangible that you can see and work with. This hook doubles as both the widget that the user sees when entering content into a node as well as the form the site administrator uses when creating a field with this widget on the content type. This hook is called for each field looking for widget information. In this case, we'll need to check the $instance['widget']['type'] value, which will correspond to the 'field types' element defined in our hook_field_widget_info() function above. Just as with regular forms using Form API, there is a $form_state variable that contains information that we want about the current state of the form widget. This is important for instances where a site administrator creates the field with default data. It also helps when doing multi-step forms with extremely complex widgets (yes, multi-step forms are possible, but those are a blog post of their own). Everything else is just Forms API. Feel free to embellish the form fields with jQuery plugins for a nice interface. The #attached array element provides an easy way to add JavaScript and CSS files to your form. You can also add #prefix and #suffix elements to provide HTML wrappers, which we will use later in the formatter hooks below.

Step 3: Format the field

Perhaps the most satisfying aspect of creating a custom field is the formatter. This is the output displayed to the end user. Just like with the widget hooks, there are two simple formatter hooks that we will use:

hook_field_formatter_info()

  1. function dnd_fields_field_formatter_info() {
  2. return array(
  3. 'dnd_fields_ability' => array(
  4. 'label' => t('Ability scores'),
  5. 'field types' => array('dnd_fields_ability'),
  6. ),
  7. 'dnd_fields_skill' => array(
  8. 'label' => t('Skill fields'),
  9. 'field types' => array('dnd_fields_skill'),
  10. ),
  11. );
  12. }

Again, this is simply a list of the label for your field and which field types that are involved in the next hook.

hook_field_formatter_view()

This is the hook that will format and display the field.

  1. function dnd_fields_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
  2. $element = array();
  3.  
  4. switch ($display['type']) {
  5. case 'dnd_fields_ability':
  6. $headers = array(
  7. t('Skill'),
  8. t('Score'),
  9. t('Modifier'),
  10. t('Temp score'),
  11. t('Temp modifier'),
  12. );
  13.  
  14. $element[0]['#theme'] = 'table';
  15. $element[0]['#data'] = array('header' => $headers, 'rows' => $items);
  16. break;
  17.  
  18. case 'dnd_fields_skill':
  19. // Set the skill name to the human readable name instead of the internal machine name.
  20. $skills = dnd_fields_skill_list_array(explode("n", $field['settings']['skill']));
  21. foreach ($items as $delta => $item) {
  22. $items[$delta]['skill'] = $skills[$item['skill']];
  23. }
  24.  
  25. $headers = array(
  26. t('Skill'),
  27. t('Ranks'),
  28. );
  29.  
  30. $element[0]['#theme'] = 'table';
  31. $element[0]['#data'] = array('header' => $headers, 'rows' => $items);
  32.  
  33. break;
  34. }
  35. return $element;
  36. }

There are countless ways to format the field data. For simplicity's sake, I'll water this down to the bare minimum needed to display the field data in an HTML table. Just like the hook_field_widget_form hook above, this looks at the display type to determine how to format the field. This hook returns a renderable array. In the example above, a simple table is used, which prints multiple entries as part of the same table. In other cases, you may want to loop through each field item in order to theme it individually.

  1. foreach ($items as $delta => $item) {
  2. $element[$delta] = array(
  3. '#theme' => 'some_theme_function',
  4. '#data' => $item['value'],
  5. );
  6. }

In the original example above, $element[0] is the first instance of $element[$delta].

Final thoughts

While a field requires eight hooks to get started, most of it is straightforward. This example focuses mainly compound fields, but many other options are possible. For another good example, check out the Field Example module that comes with core. That module uses a jQuery color picker to help with field input. There are other hooks worth considering:

  • hook_field_presave() - When dealing with compound fields, it's important to set any values for form elements that weren't set by the user. This will help prevent SQL errors when inserting null values into database tables that have 'not null' set.
  • hook_form_field_ui_field_edit_form_alter() - This is hook_form_FORM_ID_alter(). This is a really useful hook for manipulating existing field forms, and adding default values that have to be calculated.

In the widget form hook above, I alluded to adding some HTML wrappers to help with formatting the form. By default, all the form fields will be displayed one on top of each other. Using this CSS, you can make forms that look a little more cohesive:

  1. .field-type-dnd-fields-skill {
  2. display: inline-block;
  3. width: 100%;
  4. }
  5. .dnd-fields-skill-field {
  6. float: left;
  7. margin: 0 10px 0 0;
  8. }
  9. .dnd-fields-skill-ranks-field .form-text {
  10. width: 52px;
  11. }
  12. .dnd-fields-skill-field label {
  13. font-weight: normal;
  14. font-size: 12px;
  15. padding: 0 0 2px;
  16. }
  17. .dnd-fields-skill-ranks-field .form-item .description {
  18. width: 200px;
  19. display: none;
  20. }
Tobby Hagler

Tobby Hagler

Director of Engineering