Score:1

AJAX in a plugin form results in "The specified #ajax callback is empty or not callable."

in flag

I have a configuration form for a Condition plugin I want to have ajax in. When selecting from this field, I want to get a list of displays for that View:

  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
    $form = parent::buildConfigurationForm($form, $form_state);
    $views = $this->entityTypeManager->getStorage('view')->loadMultiple();
    $options = [];

    /** @var \Drupal\views\Views $view */
    foreach ($views as $view) {
      $options[$view->id()] = $view->label();
    }

    $form['#prefix'] = '<div id="views-settings">';
    $form['#suffix'] = '</div>';

    $view_id = $form_state->getValue('view_id');

    if (empty($view_id)) {
      $view_id = $form_state->getUserInput()["conditions"]["sfp_condition_view_not_empty"]["view_id"] ?? NULL;
    }

    if (empty($view_id)) {
      $view_id = $this->configuration['view_id'] ?? NULL;
    }

    $displays = isset($view_id) ? $this->getViewsDisplays($view_id) : [];

    $form['view_id'] = [
      '#type' => 'select',
      '#title' => $this->t('View'),
      '#required' => TRUE,
      '#options' => $options,
      '#default_value' => $view_id ? $options[$view_id] : '',
      '#ajax' => [
        'wrapper' => 'views-settings',
        'callback' => '::updateViewsDisplay',
        'event' => 'change',
      ],
    ];

    if (!empty($displays)) {
      $form['view_display'] = [
        '#type' => 'select',
        '#title' => $this->t('View display'),
        '#required' => TRUE,
        '#options' => $displays,
        '#default_value' => $displays[$this->configuration['view_display']] ?? $displays['default'],
      ];
    }

    return $form;
  }

      /**
       * Trigger form rebuild.
       *
       * @param array $form
       * @param \Drupal\Core\Form\FormStateInterface $form_state
       * @return array
       */
      public function updateViewsDisplay(array $form, FormStateInterface $form_state) {
        return $form;
      }

Xdebug hits the method fine, but I get this AJAX error:

"The specified #ajax callback is empty or not callable."

and nothing happens after that.

I checked elsewhere where I have done this and the only difference I can see is the ones that worked are regular Drupal Form API forms, and this is a plugin form passed from buildConfigurationForm. Outside of that, I am not seeing the issue.

cn flag
Any chance you're using this in a Context UI form (as opposed to e.g. block visibility)? I remember having awful trouble trying to get AJAX working with the Context module
Kevin avatar
in flag
Yeah, that is exactly where I am. Changing the callback to a procedural function in a .module works, but I don't know why. Shouldn't this still be a reachable method if made static? Nothing seemed to work. Whats bizarre is xdebug never failed to hit it. Form state also does not always have the selected value, unlike usual forms.
cn flag
It was a while ago but IIRC it had something to do with context UI having its own entry point for form AJAX, it doesn't use `/system/ajax`. I'm sure I remember the same thing happening, the callback definitely being hit but the results not being included in the new form. Whatever I ended up doing to get around it is somewhere in this module if it helps: https://www.drupal.org/project/request_data_conditions
Kevin avatar
in flag
Wow. Yeah. Who knew? Posting answer.
Kevin avatar
in flag
That actually fixed the AJAX, but wound up getting 'illegal choice detected' error. I wound up just axing AJAX and supplying the entire array in a list.
cn flag
Don't blame you, I just re-read the rambling comment I wrote years ago trying to explain what was going on (line 152 of BaseCondition) and it came flooding back. Here be dragons.
Score:3
in flag

Thanks to Clives link, for reasons I am not 100% on the configuration forms for a Context plugin the AJAX callback needs to do this:

  /**
   * Trigger form rebuild.
   *
   * @param array $form
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   * @return array
   */
  public function updateViewsDisplay(array $form, FormStateInterface $form_state) {
    $triggering_element = $form_state->getTriggeringElement();
    $parents = array_slice($triggering_element['#array_parents'], 0, -1);
    return NestedArray::getValue($form, $parents);
  }

Then it began to work.

Also, the AJAX declaration needed to be:

'callback' => [$this, 'updateViewsDisplay'],

I assume this has to do with it being a 'form within a form'.

4uk4 avatar
cn flag
Yes, a form declaration in a plugin usually is not standalone, it's a subtree which the code using the plugin can place in its own form. You see this trick with array slicing also in field widgets. BTW basing the callback on $this is not recommended, you might not get the form instance you are expecting. I would use `'[static::class, 'updateViewsDisplay']` if `'::updateViewsDisplay'` doesn't work. If you then need to reference the form object you can get it from $form_state.
Kevin avatar
in flag
I see, also note that static::class requires the callback to be static as well, or the error persists.
mangohost

Post an answer

Most people don’t grasp that asking a lot of questions unlocks learning and improves interpersonal bonding. In Alison’s studies, for example, though people could accurately recall how many questions had been asked in their conversations, they didn’t intuit the link between questions and liking. Across four studies, in which participants were engaged in conversations themselves or read transcripts of others’ conversations, people tended not to realize that question asking would influence—or had influenced—the level of amity between the conversationalists.