Login

Drupal 7 Field API Simple Example - Digging Deeper

This article is a follow up to "Drupal 7 Field API - A simple example". This article is intended for programmers. We build a proof-of-concept module who's sole purpose is to demonstrate features of the Drupal 7 Field API.

Please note! Folks should always start their learning process by reviewing the official "Examples for Developers" on Drupal.org. I also recommend that programmers peruse Drupal's source code itself to find real-world examples that follow best practices.

Links to the full source code listings are located at the bottom of this article.

two sample date formats
Figure 1

Our new module is called "my date".  We add several new features: the ability to style the date display; better control over the default date values; use of the JQuery date picker widget to select dates.

In the screen shot (Figure 1) we see 2 different content types. I've configured a custom display for each of the 2 content types demonstrated in the Figure 1. "my date" allows you to customize the display through configuration dialogs detailed below.

I broke each of the date instances in to 4 HTML div elements: container, month, day, year.

In addition, each of the date parts (month, day, year) can be toggled on or off (displayed or not displayed). In the Figure 1 examples, I turned the display of the year off.

I also allow an HTML style attribute to be applied to each HTML element. Again, that is the container div, month, date, year.

The date can be aligned (float) either right or left.

You also have the option of choosing a back ground image.

I am not a graphics designer nor a themer.  The idea was to show you how easy it is to set the configuration controls  .

Drupal 7 allows you to create a separate configuration for a teaser view versus a full view, etc.

 

The next screen shots detail the custom "manage display" dialogs. First the "summary" dialog in Figure 2:

Figure 2

 

Now the detailed dialog in Figure 3:

Figure 3

 

Note again. in Figure 3 we've toggled the "Year" date display off. The "Use Year" check box is not checked. Therefore, the dialog does not display a corresponding text input field (to inline style the year). We use the new Drupal 7 FAPI "#states" directive to dynamically display and hide form elements. The following is a code snipplet demonstrating the technique:

    $element['date_part']['show_year'] = array(
        '#type' => 'checkbox',
        '#title' => 'Use Year',
        '#default_value' => $settings['date_part']['show_year'] ? TRUE : FALSE,
        '#states' =>
        array('visible' =>
            array(':input[name="' . $element_name . '"]' =>
                array('checked' => TRUE),
            )
        ),
    );
 
    $element_name = 'fields[' . $field_name . '][settings_edit_form][settings][date_part][show_year]';
    $element['date_part']['inline_style_year'] = array(
        '#type' => 'textfield',
        '#title' => 'Inline Style Year',
        '#default_value' => $settings['date_part']['inline_style_year'],
        '#states' => array('visible' =>
            array(':input[name="' . $element_name . '"]' =>
                array('checked' => TRUE),
            )
        ),
    );

Explanation: The first element is the check box, the second element if the textfield with "#states" directive displaying itself only if the corresponding check box is on (checked). 


Next, let's look at the our implementation of hook_field_formatter_info(). We define 2 different date formats:

  • A calendar format (in which we use the JQuery date picker as a display only widget).
  • A detailed format (who's results are displayed in Figures 1,2 and 3).

 

/**
 * Implements hook_field_formatter_info().
 */
function my_date_field_formatter_info() {
  return array(
      'my_date_calendar_formatter' => array(
          'label' => t('My Date Calendar Formatter'),
          'field types' => array('my_date'),
          'settings' => array(
              'container' => array(
                  'float' => 'right',
                  'class' => '',
                  'inline_style' => '',),
          ),
      ),
      'my_date_detailed_formatter' => array(
          'label' => t('My Date Detailed Formater'),
          'field types' => array('my_date'),
          'settings' => array(
              'container' => array(
                  'float' => 'right',
                  'class' => '',
                  'inline_style' => '',
                  array('background' =>
                      array('image' => ''),
                  ),
              ),
              'date_part' => array(
                  'show_month' => TRUE,
                  'show_day' => TRUE,
                  'show_year' => TRUE,
                  'inline_style_month' => '',
                  'inline_style_day' => '',
                  'inline_style_year' => ''),
          ),
      ),
  );
}

Next we implement the hook which creates the summary displayed in Figure 2. That is an implementation of hook_field_formatter_settings_summary()  :

/**
 *  Implements hook_field_formatter_settings_summary()
 */
function my_date_field_formatter_settings_summary($field, $instance, $view_mode) {
  $format = $instance['display'][$view_mode];
  $settings = $format['settings'];
 
  $summary = '';
 
  $styles = my_date_field_float_options();
  $summary = t('Float') . ': ' . $styles[$settings['container']['float']] . ', ';
 
  $class_summary = t('Class: ');
  $class_summary .= empty($settings['container']['class']) ? t('None') : $settings['container']['class'] . ', ';
  $summary .= $class_summary;
 
  $inline_summary = t(' Inline Style:');
  $inline_summary .= empty($settings['container']['inline_style']) ? t('None') : $settings['container']['inline_style'];
  $summary .= $inline_summary;
 
 
  if ('my_date_detailed_formatter' == $format['type']) {
 
    $show_month = isset($settings['date_part']['show_month']) ? TRUE : FALSE;
    $show_day = isset($settings['date_part']['show_day']) ? TRUE : FALSE;
    $show_year = isset($settings['date_part']['show_year']) ? TRUE : FALSE;
 
    $date_part_summary = 'Date Parts : ';
    $date_part_summary .= $show_month ? 'Month-' : '';
    $date_part_summary .= $show_day ? 'Day' : '';
    $date_part_summary .= $show_year ? '-Year' : '';
    $summary = $date_part_summary . ' ' . $summary;
  }
 
  return $summary;
}

Explanation: We have a few attributes like "float/align" that are shared between the calendar formatter and the detailed formatter. Thus those common attributes are not distinguished by which formatter (calendar or detailed) we are processing.


We implement hook_field_formatter_settings_form() to get the detailed dialog demonstrated in Figure 3. The method is lengthy but not complicated. We have a lot of attributes which must have a corresponding form element. 

What you should note is, the elements defined in tho following method follow the exact same hierarchy expressed in hook_field_formatter_info().  Thus the "float" attribute is expressed as a child of "container" (e.g. $element['container']['float']).

**
 *  Implements hook_field_formatter_settings_form()
 */
function my_date_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
  $format = $instance['display'][$view_mode];
  $settings = $format['settings'];
  $type = $format['type'];
 
  $element = array();
 
  $element['container'] = array(
      '#type' => 'fieldset',
      '#title' => t('Container Attributes'),
  );
 
  $element['container']['float'] = array(
      '#title' => t('Float'),
      '#type' => 'select',
      '#options' => my_date_field_float_options(),
      '#default_value' => isset($settings['container']['float']) ? $settings['container']['float'] : 'right',
  );
 
  $element['container']['class'] = array(
      '#title' => t('Class(es)'),
      '#type' => 'textfield',
      '#size' => 15,
      '#default_value' => !empty($settings['container']['class']) ? $settings['container']['class'] : '',
      '#required' => false,
  );
 
  $element['container']['inline_style'] = array(
      '#title' => t('Inline Style'),
      '#type' => 'textfield',
      '#default_value' => !empty($settings['container']['inline_style']) ? $settings['container']['inline_style'] : '',
      '#required' => false,
  );
 
  if ($type == 'my_date_detailed_formatter') {
 
    $field_name = $instance['field_name'];
 
    $element['container']['background'] = array(
        '#type' => 'fieldset',
        '#attributes' => array('style' => 'float:right;margin-left:4px;'),
        '#weight' => -20,
    );
 
    $element['container']['background']['image'] = array(
        '#type' => 'radios',
        '#title' => 'Choose Background Image',
        '#default_value' => $settings['container']['background']['image'],
        '#options' => my_date_field_image_options(),
    );
 
    $element['date_part'] = array(
        '#type' => 'fieldset',
        '#title' => t('Date Part Attributes'),
    );
 
    $element['date_part']['show_month'] = array(
        '#type' => 'checkbox',
        '#title' => 'Use Month',
        '#default_value' => isset($settings['date_part']['show_month']) ? TRUE : FALSE,
    );
 
    $element_name = 'fields[' . $field_name . '][settings_edit_form][settings][date_part][show_month]';
    $element['date_part']['inline_style_month'] = array(
        '#type' => 'textfield',
        '#title' => 'Inline Style Month',
        '#default_value' => $settings['date_part']['inline_style_month'],
        '#states' => array('visible' =>
            array(':input[name="' . $element_name . '"]' =>
                array('checked' => TRUE),
            )
        ),
    );
 
    $element['date_part']['show_day'] = array(
        '#type' => 'checkbox',
        '#title' => 'Use Day',
        '#default_value' => $settings['date_part']['show_day'] ? TRUE : FALSE,
    );
 
    $element_name = 'fields[' . $field_name . '][settings_edit_form][settings][date_part][show_day]';
    $element['date_part']['inline_style_day'] = array(
        '#type' => 'textfield',
        '#title' => 'Inline Style Day',
        '#default_value' => $settings['date_part']['inline_style_day'],
        '#states' => array('visible' =>
            array(':input[name="' . $element_name . '"]' =>
                array('checked' => TRUE),
            )
        ),
    );
 
    $element['date_part']['show_year'] = array(
        '#type' => 'checkbox',
        '#title' => 'Use Year',
        '#default_value' => $settings['date_part']['show_year'] ? TRUE : FALSE,
        '#states' =>
        array('visible' =>
            array(':input[name="' . $element_name . '"]' =>
                array('checked' => TRUE),
            )
        ),
    );
 
    $element_name = 'fields[' . $field_name . '][settings_edit_form][settings][date_part][show_year]';
    $element['date_part']['inline_style_year'] = array(
        '#type' => 'textfield',
        '#title' => 'Inline Style Year',
        '#default_value' => $settings['date_part']['inline_style_year'],
        '#states' => array('visible' =>
            array(':input[name="' . $element_name . '"]' =>
                array('checked' => TRUE),
            )
        ),
    );
  }
 
  return $element;
}

To render "my date" we implement hook_field_formatter_view(). Again. the implementation has many attributes to handle. Thus it's not complicated, it's just lengthy.

We are rendering 2 different views: 1) A calendar view using the JQuery date widget, 2) Our detailed view demonstrated in Figures 1,2 and 3. Thus the "switch" construct to differentiate which formatter we are processing.

/**
 * Implements hook_field_formatter_view().
 */
function my_date_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
 
  $element = array();
 
  $container = '<div ';
  $style = 'float:right;margin-left:4px;';
 
  if ($display['settings']['container']['float'] == 'right') {
    $style = 'float:right;margin-left:4px;';
  } else {
    $style = 'float:left;margin-right:4px;';
  }
 
  if ($display['settings']['container']['background']['image'] != 'none') {
    $back_ground_image = 
      $GLOBALS['base_path'] . drupal_get_path('module', 'my_date') . '/images/' . 
      $display['settings']['container']['background']['image'] . '-sky.png';
 
    $style .= 'background:url(\'' . $back_ground_image . '\');';
  }
 
  if (!empty($display['settings']['container']['inline_style'])) {
    $style .= $display['settings']['container']['inline_style'];
  }
 
  if (!empty($display['settings']['container']['class'])) {
    $container .= 'class="' . $display['settings']['container']['class'] . '" ';
  }
 
  $field_name = $instance['field_name'];
  $field_id = str_replace('_', '-', $instance['field_name']) . $delta;
 
 
  switch ($display['type']) {
    case 'my_date_calendar_formatter':
      foreach ($items as $delta => $item) {
        if ($item['my_date']) {
 
          $date_string = my_date_field_to_string_format($item['my_date']);
 
          $js = 
            '(function($){$(function() { $("#' . $field_id . '").datepicker({defaultDate:"' . 
            $date_string . '", minDate: "' . $date_string . '", maxDate: "' . $date_string . '"}); })})(jQuery);';
 
          $element[$delta]['#markup'] = $container . 'id="' . $field_id . '" style="' . $style . '"></div>';
          $element['#attached']['library'][] = array('system', 'ui.datepicker');
          $element['#attached']['js'][] = array('data' => $js, 'type' => 'inline');
        }
      }
      break;
 
    case 'my_date_detailed_formatter':
      foreach ($items as $delta => $item) {
        if ($item['my_date']) {
          $date_value = getdate($item['my_date']);
          $settings = $display['settings'];
 
          $output = $container . 'id="' . $field_id . '" style="' . $style . '" align="center">';
          if ($display['settings']['date_part']['show_month']) {
            $output .= '<div style="' . 
            $settings['date_part']['inline_style_month'] . '">' . $date_value['month'] . '</div>';
          }
          if ($display['settings']['date_part']['show_day']) {
            $output .= '<div style="' . 
            $settings['date_part']['inline_style_day'] . '">' . $date_value['mday'] . '</div>';
          }
          if ($display['settings']['date_part']['show_year']) {
            $output .= '<div style="' . 
            $settings['date_part']['inline_style_year'] . '">' . $date_value['year'] . '</div>';
          }
          $output .= '</div>';
          $element[$delta]['#markup'] = $output;
        }
      }
      break;
  }
  return $element;
}

To prevent this article from becoming too long, I'll summarize the remaining source code here.

The Drupal 7 Field API allows programmers to customize the content editing widget. In our case "my date" allows users to select a data value.  "my date" uses the JQuery date picker widget to select dates. Note! Yes I used the JQuery date picker for both a editing widget and a display only element.

The editing widget is used in several scenarios. The main scenarios are:

  • A website administrator is configuring an instance of "my date". The administrator configures a default date value for a particular attachment instance. For example, an administrator attaches "my date" to a custom content type called "holiday". The administrator sets the default date for "holiday" to 07/04/2011.
  • A content author either creates a new post or edits an existing post. The "my date" widget allows the author to select a new date.

To define and render our widget we implement hook_field_widget_form(). Again our implementation is not complicated, it's just lengthy. We are customizing each of the scenarios above. We determine in which context is the widget form called: 1) Configuration setting 2) Content editing.

We also account for the situation where an administrator attaches multiple instances of "my date" to a single content type. I decided that the administrator should only have to decide a default date value for the set. Thus, the code identifies when a set is being configured, the default date is applied from the first instance to the whole set.

When a user selects a date, the result is a string formatted as a date. However, I've engineered "my date" to persist the date value as a time-stamp integer (see my_date_field_info() and my_date.install). Thus I defined a custom method which validates and converts the data string in to a time-stamp integer.

Again as described above, our widget is used in several contexts. Thus we need to navigate deeply nested Drupal Form API structures to reset our value from a date string to a time-stamp integer. The method my_date_str_to_date_validate() performs the work:

/**
 * Converts a validates a date string to a time stamp.
 *
 * In addition, this function sets the form to correct timestamp value
 * so that the field api service stores the correct value.
 *
 * If $element does not contain a valid date string this method sets
 * a form error.
 *
 * We use the drupal api methods
 * drupal_array_get_nested_value() and drupal_array_set_nested_value()
 * to get the date string form the deeply nested form_state array and
 * the set the corresponding timestamp value (again back in to the deeply
 * nested form_state structure).
 *
 *
 * @param type $element
 * @param type $form_state
 *
 */
function my_date_str_to_date_validate($element, &$form_state) {
 
  try {
 
    $date_string = drupal_array_get_nested_value($form_state['values'], $element['#parents']);
    $timestamp = my_date_field_convert_to_timestamp($date_string);
    drupal_array_set_nested_value($form_state['values'], $element['#parents'], $timestamp, TRUE);
  } catch (Exception $e) {
    form_error($element, $e->getMessage());
  }
}

To summarize, Drupal 7's Field API allows programmers to create a rich set of custom features with relatively little code. I hope this example helps.

Full source code listings are located at the following locations:

 

Comments

Very nice writeup ! Thanks

Very nice writeup ! Thanks for sharing :-)

There's an issue in the way you detect whether the widget is displayed in a regular 'entity edit' form or on the 'field configuration' form (for 'default value') :

$is_settings_form_type = isset($form['type']['#type']) ? FALSE : TRUE;

$form['type']['#type'] is only present for node edit forms. That is, your widget will incorrectly assume it's displayed on the 'field configuration' form when the field is used on users, taxo terms, or any other entity type.

Besides, beware that the $form param received by hook_field_widget_form() might not be the 'full' form, but just a subpart of a larger form. For instance, that's how http://drupal.org/project/field_collection does to provide 'fields within fields' (or 'combo fields'). In this case you wouldn't have access to the form elements that live on the top-level.

I'm actually not sure that there's a safe way to tell the difference. Widgets are supposed to be unaware where they're being included : an edit form, the field config form, ... - could even be displayed as an exposed Views filter (although not currently ;-)