5

I have created a custom field type that has 3 columns – product, color, and quantity.

Ideally, the user will only see 1 dropdown from which to pick a product. Picking a product should then load the 2nd dropdown showing only the colors available for that product. Picking a color should then show a textfield where the user can enter a quantity. If the user picks a different product, the 2nd dropdown would reload with the colors available for that product.

In a normal FAPI form I can do this kind of thing just fine by using the #ajax properties of an element and doing return drupal_rebuild_form('my_form_name', $form_state); in the callback, and then in the original form constructor actually checking the $form_state['values'] array to see what needs changing (i.e. if a "product" has been chosen, I can use that value to pre-fill the "colors" dropdown). In a widget this same logic doesn't seem to apply, mainly because you can have a multi-value field (in my current case its unlimited) and because doing something like return drupal_rebuild_form(...) doesn't run my hook_field_widget_form().

The only thing that I've gotten to work is using the #element_validate property on the #ajax element to run a validation function which invokes $form_state['rebuild'] = TRUE; and then throws some dummy content into the $form via something like $form['my_custom_namespace'][$delta_or_something]['product'] = $value_chosen_by_user;, then in the original form constructor I can check if (isset($form_state['my_custom_namespace'][$delta_or_something]['product'])) { ... }.

This is wacky, this is a lot of extraneous code, and it feels like a straight hack. I pulled the #element_validate trick from the addressfield module, which make me think this is the best method folks are currently using to do this. Is that really the case? Are there any other D7 modules that provide fields like this that I can use as an example?

My current code is below. This doesn't work with a multi-value field – there is a validation error that is occurring when multiple products and colors are chosen :( Regardless, maybe someone can help...

field_supplemental_item.install:

/**
 * Implements hook_field_schema().
 */
function field_supplemental_item_field_schema() {
  $columns = array(
    'product' => array('type' => 'int', 'not null' => FALSE),
    'color' => array('type' => 'int', 'not null' => FALSE),
    'qty' => array('type' => 'int', 'not null' => FALSE),
  );
  return array(
    'columns' => $columns,
  );
}

field_supplemental_item.module:

<?php

/**
 * Implements hook_field_info().
 */
function field_supplemental_item_field_info() {
  return array(
    'field_supplemental_item' => array(
      'label' => t('Supplemental Item'),
      'description' => t('A field that references additional products to add to an order.'),
      'settings' => array(),
      'instance_settings' => array(),
      'default_widget' => 'field_supplemental_item_default_widget',
      'default_formatter' => 'field_supplemental_item_default_formatter',
    ),
  );
}

/**
 * Implements hook_field_settings_form().
 */
function field_supplemental_item_field_settings_form($field, $instance, $has_data) {
  $form = array();
  return $form;
}

/**
 * Implements hook_field_instance_settings_form().
 */
function field_supplemental_item_field_instance_settings_form($field, $instance) {
  $form = array();
  return $form;
}

/**
 * Implements hook_field_formatter_info().
 */
function field_supplemental_item_field_formatter_info() {
  return array(
    'field_supplemental_item_default_formatter' => array(
      'label' => t('Default'),
      'field types' => array('field_supplemental_item'),
    ),
  );
}

/**
 * Implements hook_field_formatter_view().
 */
function field_supplemental_item_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
  foreach ($items as $delta => $item) {
    $element[] = array('#markup' => "here i am!");
  }
  return $element;
}

/**
 * Implements hook_field_widget_info().
 */
function field_supplemental_item_field_widget_info() {
  return array(
    'field_supplemental_item_default_widget' => array(
      'label' => t('Default'),
      'field types' => array('field_supplemental_item'),
      'settings' => array(),
      'behaviors' => array(
        'multiple values' => FIELD_BEHAVIOR_DEFAULT,
        'default value' => FIELD_BEHAVIOR_DEFAULT,
      ),
    ),
  );
}

/**
 * Implements hook_field_widget_form().
 */
function field_supplemental_item_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  $element_key = drupal_html_id('field-supplemental-item');

  $element['#prefix'] = "<div id='{$element_key}'>";
  $element['#suffix'] = "</div>";
  $element['element_key'] = array(
    '#type' => 'value',
    '#value' => $element_key
  );

  if (isset($form_state['field_supplemental_item'][$element_key]['product'])) {
    $product_id = $form_state['field_supplemental_item'][$element_key]['product'];
  } else if (isset($items[$delta]['product'])) {
    $product_id = $items[$delta]['product'];
  } else {
    $product_id = NULL;
  }

  // Product
  $element['product'] = array(
    '#type' => 'select',
    '#title' => 'Product',
    '#options' => array('_none' => '- Select -') + _field_supplemental_item_get_data('products'),
    '#default_value' => $product_id,
    '#ajax' => array(
      'callback' => 'field_supplemental_item_field_widget_product_callback',
      'wrapper' => $element_key,
      'method' => 'replace',
    ),
    '#element_validate' => array('field_supplemental_item_field_widget_product_validator'),
    '#limit_validation_errors' => array(), // Don't validate any element when the product is changed.
  );

  // Color
  $element['color'] = array(
    '#type' => 'select',
    '#title' => 'Color',
    '#options' => (($product_id)) ? _field_supplemental_item_get_data('colors', $product_id) : array(),
    '#limit_validation_errors' => array(), // Don't validate any element when the product/color is changed.
  );

  // Quantities
  // ...

  return $element;
}

/**
 * Element validate callback: rebuilds the form on value change and stores the current value in the $form_state for retrieval on rebuild.
 */
function field_supplemental_item_field_widget_product_validator($element, &$form_state) {
  // If the product was changed, rebuild the form.
  if ($element['#default_value'] != $element['#value']) {
    $form_state['rebuild'] = TRUE;
  }

  // Store the value in the form state for retrieval by the
  // widget form regardless of where the widget sits in the $form array.
  $form_state['field_supplemental_item'][$element['#ajax']['wrapper']]['product'] = $form_state['triggering_element']['#value'];
}

function field_supplemental_item_field_widget_product_callback($form, $form_state) {
  $product_id = $form_state['triggering_element']['#value'];

  // The target element is one element below the triggering selector.
  $array_parents = $form_state['triggering_element']['#array_parents'];
  array_pop($array_parents);

  // Iterate over the form parents to find the element.
  $element = $form;
  foreach ($array_parents as $name) {
    $element = &$element[$name];
  }

  $element['product']['#default_value'] = $product_id;
  $element['color']['#options'] = _field_supplemental_item_get_data('colors', $product_id);

  // Return the element, but remove the '_weight' element inserted
  // by the field API.
  unset($element['_weight']);

  return $element;
}

/**
 * Implements hook_field_validate().
 */
function field_supplemental_item_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {}

/**
 * Implements hook_field_widget_error().
 */
function field_supplemental_item_field_widget_error($element, $error, $form, &$form_state) {}

/**
 * Implements hook_field_is_empty().
 */
function field_supplemental_item_field_is_empty($item, $field) {
  return (empty($item['product']));
}

EDIT 9/16/12: Let me add that I'm currently able to achieve this by replacing the entire form, which feels completely ridiculous and can be quite intensive on a large and bulky entity edit form – and requires looking through the $form_state to get the data I need for all $deltas...:

function hook_field_widget_form(...) {
  $form['#prefix'] = '<div id="mymodule-field-widget-form-wrapper">';
  $form['#suffix'] = '</div>';
  ...
}
function mymodule_field_widget_form_ajax(&$form, &$form_state) {
  return drupal_rebuild_form($form_state['build_info']['form_id'], $form_state, $form);
}
6
  • I will give mad bounty to anyone that can answer this. This was my 2nd attempt to do this in a clean way – first attempt resulted in #states and an inefficient form, really hoping I can learn this time around. Sep 6, 2012 at 3:07
  • Hi, there is module that does what you want to do already for terms in a taxonomy vocabulary. It is call Hierarchical Select (drupal.org/project/hierarchical_select) I will recommend you to check the source code to see how they did it. To see a working demo go: wimleers.com/demo/hierarchical-select/taxonomy
    – Emil Orol
    Sep 13, 2012 at 22:44
  • 1
    @redhatlab , pelase read question carefully, his require is more than HS module. I suppose can achieve this but combine using HS and custom part.
    – Yuseferi
    Sep 14, 2012 at 6:02
  • 1
    @CharlieS I can suggest you a way to achieve this but without creating cck field,(with using of HS module and some customization in our moudle ). What is your needs?
    – Yuseferi
    Sep 14, 2012 at 6:49
  • It needs to be in a custom field widget (CCK field widget in Drupal 6), not in a custom form or form alter. However, since HS does interface with entity form widgets (such as a term reference field) I'll definitely look into their source! Sep 17, 2012 at 1:58

1 Answer 1

2
+200

Hierarchal Select module is the perfect solution for the problem you are facing. I have done this to list Categories in First Select Box and In Second Select box list of Groups associated with categories using HS plugin.

Please see below code:

function category_groups_hr_form_alter(&$form, &$form_state, $form_id) {
  global $user;
  $account = clone($user);
  if ($form_id == "question_node_form") {
    $category_group_hs = array();
    $group_categories = taxonomy_get_tree(3, 0, 1, FALSE);
    asort($group_categories);
    foreach ($group_categories as $item) {
      $category_group_hs[$item->tid]['label'] = $item->name;
      $query = db_select('taxonomy_index', 't');
      $query->addTag('node_access');
      $query->join('node', 'n', 'n.nid = t.nid');
      $query->join('og', 'og', 'n.nid = og.etid');
      $query->condition('t.tid', $item->tid);
      $query->condition('n.type', 'groups');
      $query->addField('t', 'nid');
      $query->addField('t', 'tid');
      $query->addField('n', 'title');
      $query->addField('og', 'gid');
      $query->addField('n', 'uid');
      $result = $query->execute();
      while ($group = $result->fetchObject()) {
          $category_group_hs[$item->tid]['children'][$group->nid] = array('label' => $group->title);
      }
    $category_id = '';
    $group_nid = '';
    $title = t('Select Category > Group :');
    $form['hs_category_group'] = array(
      '#type' => 'hierarchical_select',
      '#title' => $title,
      '#size' => 1,
      '#required' => TRUE,
      '#config' => array(
        'module' => 'hs_smallhierarchy',
        'params' => array(
          'hierarchy' => $category_group_hs,
          'id' => 'hs-category-group',
          'separator' => '|',
        ),
        'save_lineage' => 0,
        'enforce_deepest' => 0,
        'entity_count' => 0,
        'require_entity' => 0,
        'resizable' => 1,
        'level_labels' => array(
          'status' => 0,
          'labels' => array(
            0 => t('Group Category'),
            1 => t('Group')
          ),
        ),
        'dropbox' => array(
          'status' => 0,
          'title' => t('All selections'),
          'limit' => 0,
          'reset_hs' => 1,
        ),
        'editability' => array(
          'status' => 0,
          'item_types' => array(),
          'allowed_levels' => array(
            0 => 0,
            1 => 0,
            2 => 1,
          ),
          'allow_new_levels' => 0,
          'max_levels' => 2,
        ),
        'animation_delay' => 400,
        'exclusive_lineages' => array(),
        'render_flat_select' => 0,
      ),      
      '#default_value' => $category_id . '|' . $group_nid,
    );
    $form['#validate'][] = 'hs_category_and_group_validate';
    $form['#submit'][] = 'hs_category_and_group_submit';
  }
}

I am using hs_smallhierarchy module to do heavy lifting... For more information on HS Plugin API CHECK API.txt file inside hierarchical_select module....

Hope you can able to crack it using HS module..

5
  • 2
    This is exaclty I means. but maybe he want create a cck widget that use it every where.
    – Yuseferi
    Sep 14, 2012 at 7:23
  • @zhilevan He may need to look @ hs_taxonomy module to create a cck field using hs module API
    – Anil Sagar
    Sep 14, 2012 at 7:29
  • yes you right. however thanks for atention and spend time to solve others porblem. :). very welcome to drupal.stackexchange.com @};-
    – Yuseferi
    Sep 14, 2012 at 7:54
  • 1
    @Anil Sagar thanks for the post. I'm definitely looking for the way to do this through a custom field widget (CCK field widget in Drupal 6) and not a way to do this via normal #ajax in a custom form or via form_alter() or anything like that. The complexities of doing it in a custom field widget where the delta can be unlimited make it rather tricky. Sep 17, 2012 at 1:56
  • 1
    I went ahead and gave you the bounty because there are no other answers and it was about to expire. I poked around with HS module; it uses quite a lot of code and overhead to perform what it does, which makes me bash my head against the table. I'm only able to do this right now via normal #ajax and a complete form replacement. Perhaps that's how it has to be... Sep 20, 2012 at 19:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.