Hi guys,
I have developed a custom form element for my college assignment which contains 4 textfields. The element displays fine in my custom form but when I replace a container element with my custom element in an ajax callback only the title and description get replaced while my children textfield elements are omitted.

Here is the custom form element definition

function club_element_info() {
  $type['selection_element'] = [
    '#input' => TRUE,
    '#process' => ['selection_element_process'],
    '#element_validate' => ['selection_element_validate'],
    '#autocomplete path' => FALSE,
    '#value callback' => 'selection_element_value',
    '#theme_wrappers' => ['inline_form_element'],
    '#default_value' => [
      'selection_1' => '',
      'selection_2' => '',
      'selection_3' => '',
      'selection_4' => '',
    ],
  ];
  return $type;
}

function selection_element_process($element, &$form_state, $complete_form) {
  $element['#tree'] = TRUE;
  $element['selection_1'] = [
    '#type' => 'textfield',
    '#size' => 3,
    '#maxlength' => 3,
    '#value' => $element['#value']['selection_1'],
    '#attributes' => array('autocomplete' => 'off'),
    '#required' => TRUE,
  ];
  $element['selection_2'] = [
    '#type' => 'textfield',
    '#size' => 3,
    '#maxlength' => 3,
    '#prefix' => '-',
    '#value' => $element['#value']['selection_2'],
    '#attributes' => array('autocomplete' => 'off'),
    '#required' => TRUE,
  ];
  $element['selection_3'] = [
    '#type' => 'textfield',
    '#size' => 3,
    '#maxlength' => 3,
    '#prefix' => '-',
    '#value' => $element['#value']['selection_3'],
    '#attributes' => array('autocomplete' => 'off'),
    '#required' => TRUE,
  ];
  $element['selection_4'] = [
    '#type' => 'textfield',
    '#size' => 3,
    '#maxlength' => 3,
    '#prefix' => '-',
    '#value' => $element['#value']['selection_4'],
    '#attributes' => array('autocomplete' => 'off'),
      //'#required' => TRUE,
  ];
  return $element;
}

function selection_element_validate($element, &$form_state) {
  $value = $element['#value']['selection_1'];
  if ($value == '' || !is_numeric($value) || $value < 1) {
    form_error($element['selection_1']);
  }
  $value = $element['#value']['selection_2'];
  if ($value == '' || !is_numeric($value) || $value < 1) {
    form_error($element['selection_2']);
  }
  $value = $element['#value']['selection_3'];
  if ($value == '' || !is_numeric($value) || $value < 1 ) {
    form_error($element['selection_3']);
  }
  $value = $element['#value']['selection_4'];
  if ($value == '' || !is_numeric($value) || $value < 1) {
    form_error($element['selection_4']);
  }
  return $element;
}

function selection_element_value(&$element, $input = FALSE, $form_state = NULL) {
  if (!$form_state['process_input']) {
    $matches = array();
    $match = preg_match('/^(\d{3})(\d{3})(\d{4})$/', $element['#default_value'], $matches);
    if ($match) {
      // Get rid of the "all match" element.
      array_shift($matches);
      list($element['selection_1'], $element['selection_2'], $element['selection_3'], $element['selection_4']) = $matches;
    }
  }
  return $element;
}

function inline_form_element($variables) {
  $element = $variables['element'];
  // Add element #id for #type 'item'.
  if (isset($element['#markup']) && !empty($element['#id'])) {
    $attributes['id'] = $element['#id'];
  }
  // Add element's #type and #name as class to aid with JS/CSS selectors.
  $attributes['class'] = array('form-item');
  if (!empty($element['#type'])) {
    $attributes['class'][] = 'form-type-' . strtr($element['#type'], '_', '-');
  }
  if (!empty($element['#name'])) {
    $attributes['class'][] = 'form-item-' . strtr($element['#name'], array(
          ' ' => '-',
          '_' => '-',
          '[' => '-',
          ']' => '',
            )
    );
  }
  // Add a class for disabled elements to facilitate cross-browser styling.
  if (!empty($element['#attributes']['disabled'])) {
    $attributes['class'][] = 'form-disabled';
  }
  $output = '<div' . drupal_attributes($attributes) . '>' . "\n";

  // If #title is not set, we don't display any label or required marker.
  if (!isset($element['#title'])) {
    $element['#title_display'] = 'none';
  }
  $prefix = isset($element['#field_prefix']) ? '<span class="field-prefix">' . $element['#field_prefix'] . '</span> ' : '';
  $suffix = isset($element['#field_suffix']) ? ' <span class="field-suffix">' . $element['#field_suffix'] . '</span>' : '';

  $output .= ' ' . theme('form_element_label', $variables);
  $output .= ' ' . '<div class="container-inline">' . $prefix . $element['#children'] . $suffix . "</div>\n";

  if (!empty($element['#description'])) {
    $output .= ' <div class="description">' . $element['#description'] . "</div>\n";
  }

  $output .= "</div>\n";

  return $output;
}

Here is the form definiton with ajax callback.

function test_form($form, &$form_state) {
  $form['#tree'] = TRUE;
  $form['selection_element_1'] = [
    '#type' => 'selection_element',
    '#title' => 'Selection 1',
    '#required' => TRUE,
    '#description' => 'TEST'
  ];
 
  $form['selection_element_container'] = [
    '#type' => 'container',
    '#prefix' => '<div id="optional-selection-container">',
    '#suffix' => '</div>',
  ];
  $form['submit'] = [
    '#type' => 'submit',
    '#value' => 'Test'
  ];
  $form['add_more'] = array(
    '#type' => 'submit',
    '#value' => t('Add Selection Element'),
    '#href' => '',
    '#ajax' => array(
      'callback' => 'add_selection_element',
      'wrapper' => 'optional-selection-container',
      // 'method' => 'add',
      'effect' => 'fade')
  );
  return $form;
}


function add_selection_element($form, &$form_state) {
  $form['selection_element_2'] = [
    '#type' => 'selection_element',
    '#title' => 'Selection 2',
    '#description' => 'TEST'
  ];
  return $form['selection_element_2'];
}
?>

I've been scratching my head for hours and I suspect the issue lies in the theme wrapper callback but I cant see anything wrong. Perhaps some more experienced programmers can identify the problem.

Any help would be greatly appreciated.

Kind regards,
Dave

Comments

jaypan’s picture

There are two things here that are causing your problems. The first is this:

  $form['selection_element_container'] = [
    '#type' => 'container',
    '#prefix' => '<div id="optional-selection-container">',
    '#suffix' => '</div>',
  ];

Because this element has no children, and no #markup element on the initial form load, it will not be rendered. Since it's not rendered, the div will not exist, and so the ajax has nowhere in which to insert the contents. You can fix this by doing this:

  $form['selection_element_container'] = [
    '#type' => 'container',
    '#prefix' => '<div id="optional-selection-container">',
    '#suffix' => '</div>',
    '#markup' => '',
  ];

This will allow your content to be inserted into the page, however when you try to submit the form you will get an error. This is because you are creating form elements in your ajax callback. You cannot do this in Drupal. All form elements have to be defined in the form definition (or a form_alter function). You can check to see if the form has been submitted or not by checking $form_state['values'] in your form definition. Use this to generate your form elements, then return that part of the form in your ajax callback.

Contact me to contract me for D7 -> D10/11 migrations.

dmac10’s picture

Thanks Jaypan. I have tried replacing the type of element in the callback to be a textfield without '#markup' => '' and the textfield gets added.
Adding '#markup' => '' to the container element my custom element still does not display its children...
But I do understand now that my approach is wrong and I will run into issues with form submission. Thanks for taking the time to respond, you have saved me lots of time "barking up the wrong tree"!

Happy XMAS,
Dave