1

I'm trying to create a custom element that consist of a few other elements, namely some select box and one timeperiod element.

I've set up custom #process, #value_callback, and #element_validate and everything works well with one exception. The #element_validate` of timeperiod element is run after my own elemen's callback is which means I can't effectively use the results of that field for either validation or creation some structured output.

As far as I know it's a normal drupal7 behaviour, callbacks on forms are supposed to be run top->down. That said, however, I still need some after-submit callback on higher-level element that can access already final results of all of its children elements.


I'd also love to know why has top-down approach been chosen. Because no matter how much I think about it it's just completely non-sensical.


Specific example:

function TST_element_info() {
  return array(
    'station_channel' => array(
      '#input' => TRUE,
      '#tree' => TRUE,
      '#process' => array('TST_element_process'),
      '#value_callback' => 'TST_value_callback',
      '#element_validate' => array('TST_element_validate'),
    ),
  );
}

function TST_element_process($element, &$form_state, $form) {
  global $user;
  $opts = array(/* magic */);
  $element['Sel'] = array(
    '#type' => 'select',
    '#options' => $opts,
    '#empty_option' => t('Tsss'),
    '#empty_value' => 'default',
  );

  ...

  $element['Per'] = array(
    '#type' => 'timeperiod_select',
    '#title' => t('Tsss:'),
    '#required' => true,
     '#units' => array(
       '86400' => array('max' => 365, 'step size' => 1),
       '3600' => array('max' => 23, 'step size' => 1),
       '60' => array('max' => 55, 'step size' => 5),
     ),
  );
  return $element;
}


function station_channel_value_callback($element, $input = FALSE, &$form_state) {

  if (empty($input) || $input == 'default') {

    if(isset($element['#default_value'])) {

     /*magic*/

    } else {

      return false;

    }

  } else {

    $Sel =  $input['Sel'];
    $Per = $input['Per'];

  }

  return array(
    //I'd need need to return something like (or I could do it in validate):
    //But can't do it here because $input['Per'] contains raw input from its three textfields instead of computed timestamp (and even if I altered the module to do the computation in its #value_callback it would still not help because their callback is run after mine).

    'path' => "weirdStuff/$Sel/period/$Per/json"

     //Therefore I have to return:
    'Per' => $Per,
    'Sel' => $Sel,

  );
}

function TST_element_validate(&$element, &$form_state) {

  // Magic validation

  $Sel = $element['#value']['Sel'];
  $Per = $element['#value']['Per'];

  $validated_input_array = array(
     //Again, I'd need to do this but can't because timeperiod's element_validate is run after mine which means their $element['#value']['Per'] still contains raw data.
     'path' => "weirdStuff/$Sel/period/$Per/json"
  );
  form_set_value($element, $validated_input_array, $form_state);

}

//It's perfectly ok in form_submit, the form_state['#values'] contains computed (timestamp) data for the period field. But that is kinda too late for my own element.

PS: Consider this example but with one of the sub-elements that are used for connection string connection being a bit more complex one that computes its true value in #element_validate.

2 Answers 2

2
+50

The whole workflow of form API is shown here enter image description here (source: https://www.drupal.org/node/165104).

You can notice that #process is top-bottom whereas #after_build is bottom-top, and #element_validate is a whole another step and bottom-top. You can also see that #value_callback is called quite early (even before #process).

Thus, and by experience, your #value_callback should not be more than 15 lines long, and in the case of a nested element, always take form_type_checkboxes_value or form_type_radios_value as examples: they only care about edge cases and rarely modify values.

This said, another input to take into consideration is the timeperiod module, because your way of handling will depend entirely of this module. (source: http://cgit.drupalcode.org/timeperiod/tree/timeperiod.module)

/**
 * Implements hook_element_info().
 */
function timeperiod_element_info() {
  // A duration form element which represents the duration in seconds.
  $types['timeperiod_select'] = array(
    '#input' => TRUE,
    '#tree' => TRUE,
    '#default_value' => 0,
    '#value_callback' => 'timeperiod_element_value',
    '#process' => array('timeperiod_element_process', 'ajax_process_form'),
    '#after_build' => array('timeperiod_element_after_build'),
    '#pre_render' => array('form_pre_render_conditional_form_element'),
    // Controls the available units. Array keys are the units, see
    // timeperiod_element_units(). The values control the number of available
    // values per unit, taking steps in the configured size starting from 0.
    '#units' => array(
      '3600' => array('max' => 23, 'step size' => 1),
      '60' => array('max' => 55, 'step size' => 5),
    ),
  );
  return $types;
}
  • timeperiod_element_value() does literally nothing (see its comment)
  • timeperiod_element_process() breaks down into units and sets up sub-elements
  • timeperiod_element_after_build() actually sets the value

So based on these informations, you also need to implement #after_build, which is bottom-up, so it will run after timeperiod, and set the value using form_set_value()

Remember that validation is only for validation, and will always have processed values as input.

0

In your function TST_element_valdate, to get the value of your timeperiod element, instead of $Per = $element['#value']['Per']; try either $Per = $element['Per']['#value']; or $Per = $form_state['values'][$element['#name']]['Per']; to see if you can get what you need.

1
  • Regarding your question about the order of validation of the elements, if you look at the beginning of the function _form_validate() you'll see that an element's children are validated first.
    – dblue
    Jul 31, 2015 at 19:40

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.