Please don't abuse node references! Part 2

Welcome back to the second part of "Please don't abuse node references!"

In this post I'll cover some of the details behind some of the issues I encountered when I implemented the solution to our problem outlined in part 1.

Well this is great, but...

Almost immediately after letting the client check out the changes, they provided some great feedback. It turned out that while this solution might perform better, it was going to almost double the time it took for the content editors to enter in talent data. Previously, they would just enter in each talent's name into their respective role-reference fields. Now they also had to type in the role as well as the talent's name. When you have a lot of talent and a lot of products, this can be a major headache.

We're here to help

This was a great opportunity to really help out the content editors. After some discussion to learn a bit more about the situation, it was proposed that instead of an autocomplete field for the role, we could display the top three roles that content editors typically use when entering in credited talent on a product, with an option to toggle the display of all of the roles. This would allow the content editors to easily enter the role of a credited talent while keeping the performance we needed.

End result

Technical details

I was able to do the above in Drupal by creating a new widget for our credit role reference field (in the field collection). Here's some basics on creating a field widget.

Declare our field widget by implementing hook_field_widget_info

/**
 * Implements hook_field_widget_info()
 */
 function talent_pages_field_widget_info() {
    return array(
      'credit_role' => array(
        'label' => t('Credit Role'),
        'description' => t('Checkboxes for credit roles, with certain roles given priority'),
        'field types' => array('entityreference'),
        'settings' => array(
        ),
        'behaviors' => array(
          'multiple values' => FIELD_BEHAVIOR_CUSTOM,
        ),
      ),
    );
 }

Declare what our widget form will look like by implementing hook_field_widget_form

/**
 * Implements hook_field_widget_form()
 */
function talent_pages_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  $show_more = array();
  switch ($instance['widget']['type']) {
    case 'credit_role':
      // Define items that are initially displayed
      $priority_options = array(
        t('Writer'),
        t('Artist'),
        t('Cover'),
      );
 
      // Prepare the list of options.
      $options = _options_get_options($field, $instance, $properties, $entity_type, $entity);
      $original_options = $options;
 
      // Get the default values
      $default_values = _options_storage_to_form($items, $options, $value_key, $properties);
 
      $outer_options = array();
      $value_key = key($field['columns']);
 
      // remove top X options
      foreach ($priority_options as $option) {
        $key = array_search($option, $options);
        if ($key !== FALSE) {
          $outer_options[$key] = $option;
          unset($options[$key]);
        }
      }
      $combined_options = array(
        'outer' => $outer_options, 
        'inner' => $options,
      );
 
      //build widget form element -- This is where we're tying in our validation and theme methods
      $element += array(
        '#type' => 'checkboxes',
        '#options'  => $original_options,
        '#combined_options'  => $combined_options,
        '#default_value' => $default_values,
        '#value_key' => $value_key,
        '#attributes' => array('class' => array('credit-role')),
        '#theme' => 'talent_pages_credit_role_checkboxes',
        '#element_validate' => array('talent_pages_credit_role_checkboxes_validate', ),
      );
 
      // add attached styling and js
      $element['#attached']['css'] = array(drupal_get_path('module', 'talent_pages') . '/talent_pages.css');
      $element['#attached']['js'] = array(drupal_get_path('module', 'talent_pages') . '/talent_pages.js');
    break;
  }
 
  return $element;
}

Include an optional validation method for the widget

/**
 *  Our validation method for the credit role checkboxes
 */
function talent_pages_credit_role_checkboxes_validate($element, &$form_state, $form) {
  $items = _options_form_to_storage($element);
  $value = array();
  $value_key = $element['#value_key'];
  // If there was a value
  if (!empty($element['#value'])) {
    $entities = $element['#value'];
    $value = array();
    $check = taxonomy_term_load_multiple($entities);
    foreach($check as $term) {
      $value[] = array(
        $value_key => $term->tid,
      );
    }
  }
  form_set_value($element, $value, $form_state);
}

Add an optional theme method to produce the DOM output for your new widget by implementing a theme hook. Note that I've included html directly in this method. This is just to simplify the showcase of this functionality. Ideally all html should be in template files :)

/**
 *  Theme method for credit role checkboxes widget
 */
function talent_pages_theme_credit_role_checkboxes(&$variables) {
  $element = array_pop($variables);
  $attributes = array();
  if (isset($element['#id'])) {
    $attributes['id'] = $element['#id'];
  }
  $attributes['class'][] = 'form-checkboxes';
  if (!empty($element['#attributes']['class'])) {
    $attributes['class'] = array_merge($attributes['class'], $element['#attributes']['class']);
  }
  if (isset($element['#attributes']['title'])) {
    $attributes['title'] = $element['#attributes']['title'];
  }
 
  $default_values = array();
  if (!empty($element['#entity']->field_credit_type[LANGUAGE_NONE][0])) {
    foreach($element['#entity']->field_credit_type[LANGUAGE_NONE] as $value) {
      $default_values[$value['target_id']] = $value['target_id'];
    }
  }
  foreach ($element['#combined_options'] as $key => $options) {
    $our_attributes = array(
      'class' => array($key),
    );
    $output .= '<div ' . drupal_attributes($our_attributes) . '>';
    foreach ($options as $key => $option) {
      $id = $element['#id'] . '-' . $key;
      $name = $element['#name'] . '[' . $key . ']';
      // print out option html here
      $option_attributes = array( 
        'class' => array('form-checkbox'),
        'type' => 'checkbox',
        'id' => $id,
        'name' => $name,
        'value' => $key,
      );
      if (in_array($key, $default_values)) {
        $option_attributes['checked'] = 'checked';
      }
      $label_attributes = array(
        'class' => array('option'),
        'for' => $id
      );
      $title = filter_xss_admin($option);
 
      $wrapper_attributes = array(
        'class' => array('form-item', 'form-type-checkbox'),
      );
      // wrapper
      $output .= '<div' . drupal_attributes($wrapper_attributes) . '>';
      // input
      $output .= '<input ' . drupal_attributes($option_attributes) . '/>';
      // label
      $output .= '<label ' . drupal_attributes($label_attributes) . '>' . $title . '</label>';
      // close wrapper
      $output .= '</div>';
    }
    $output .= '</div>';
  }
  return $output;
}

Reflection

It's often easy to forget the needs of the content editors as a developer focusing on increasing performance. Thankfully, in this instance, the client provided near instant feedback that helped in a better solution. Always try to keep their needs in mind throughout development.

Good thinking. Drupal development tends to have a knee-jerk response to use autocomplete text fields when better widgets are available. I almost never use autocomplete for content editors.

Add new comment

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • You can enable syntax highlighting of source code with the following tags: <code>, <blockcode>, <cpp>, <java>, <php>. The supported tag styles are: <foo>, [foo].
  • Web page addresses and email addresses turn into links automatically.
  • Lines and paragraphs break automatically.

Ready for transformation?