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);
}
HS
module. I suppose can achieve this but combine usingHS
and custom part.HS
module and some customization in our moudle ). What is your needs?