0

i´m working on a custom display extender for Views.

The display extender itself works as expected, i´ve taken the code mostly from Studiogd The display extender plugin now i needed in the options form a fieldset with add more button, so i tried to implement/adapt \Drupal\form_api_example\Form\AjaxAddMore for my options form.

But i have a problem calling the submit and ajax callback.

Here is a subset of my display extender that is probalby relevant

namespace Drupal\foo\Plugin\views\display_extender;

use Drupal\views\Plugin\views\display_extender\DisplayExtenderPluginBase;
use Drupal\Core\Form\FormStateInterface;

/**
 * Foo display extender plugin.
 *
 * @ingroup views_display_extender_plugins
 *
 * @ViewsDisplayExtender(
 *   id = "foo",
 *   title = @Translation("Foo"),
 *   help = @Translation("Foo Bar."),
 *   no_ui = FALSE
 * )
 */
class Foo extends DisplayExtenderPluginBase {

  ...


  /**
   * Provide a form to edit options for this plugin.
   */
  public function buildOptionsForm(&$form, FormStateInterface $form_state) {
   
   ...

    $form['foo']['filters_fieldset']['actions']['add_filter'] = [
      '#type' => 'submit',
      '#value' => $this->t('Add an entry'),
      '#submit' => [
        '::addFilterEntry', // <---- Taken from example
      ],
      '#ajax' => [
        'callback' =>  '::filtersFieldsetCallback',
        'wrapper' => 'filters-fieldset-wrapper',
      ],
    ];

    ...
  }

  ...

  /**
   * Submit handler for the "add-one-more" button.
   *
   * Increments the max counter and causes a rebuild.
   */
  public function addFilterEntry(array &$form, FormStateInterface $form_state) {

    $filter_field = $form_state->get('num_filters');
    $add_button = $filter_field + 1;
    $form_state->set('num_filters', $add_button);

    // Since our buildForm() method relies on the value of 'num_filters' to
    // generate 'filter' form elements, we have to tell the form to rebuild. If we
    // don't do this, the form builder will not call buildForm().
    $form_state->setRebuild();
  }

  /**
   * Callback for both ajax-enabled buttons.
   *
   * Selects and returns the fieldset with the filters in it.
   */
  public function filtersFieldsetCallback(array &$form, FormStateInterface $form_state) {
    return $form['foo']['filters_fieldset'];
  }

  ...

}

Now when i click the add more button in the options form it Results in:

call_user_func_array() expects parameter 1 to be a valid callback, class 'Drupal\views_ui\Form\Ajax\Display' does not have a method 'addFilterEntry'

So I tried to specify it like:

'#submit' => [
    [$this, 'addFilterEntry'],
],

which actually lets call_user_func try to call 'addFilterEntry' on my class and i get

The specified #ajax callback is empty or not callable.

makes sense, now when I also change the ajax callback to

'#ajax' => [
   'callback' => [$this, 'filtersFieldsetCallback'],

I cant open the options form anymore resulting in a 5xx response

InvalidArgumentException: Recursion detected in Symfony\Component\HttpFoundation\JsonResponse->setData() (line 169 of /srv/www/htdocs/foo.dev/vendor/symfony/http-foundation/JsonResponse.php).

Drupal\Core\Ajax\AjaxResponseAttachmentsProcessor->processAttachments() (Line: 62)
Drupal\Core\EventSubscriber\AjaxResponseSubscriber->onResponse()
call_user_func() (Line: 142)
Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch() (Line: 191)
Symfony\Component\HttpKernel\HttpKernel->filterResponse() (Line: 179)
Symfony\Component\HttpKernel\HttpKernel->handleRaw() (Line: 80)
Symfony\Component\HttpKernel\HttpKernel->handle() (Line: 58)
Drupal\Core\StackMiddleware\Session->handle() (Line: 48)
Drupal\Core\StackMiddleware\KernelPreHandle->handle() (Line: 48)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle() (Line: 51)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle() (Line: 23)
Stack\StackedHttpKernel->handle() (Line: 708)
Drupal\Core\DrupalKernel->handle() (Line: 19)

i have a feeling that the difference is that the options form itself is already loaded with ajax but I have no clue how i can solve this or where to find example code

1 Answer 1

1

The plugin class is not a form class. The method buildOptionsForm is called by the parent form to get additional form elements. Callbacks referencing $this will reference an instance of the form class, not the plugin class.

You can only define static plugin callbacks

public static function filtersFieldsetCallback(array &$form, FormStateInterface $form_state) {
  return $form['foo']['filters_fieldset'];
}

referenced by the form element:

'#ajax' => [
  'callback' => [static::class, 'filtersFieldsetCallback'],

This is true for most option forms defined in plugins. Specific to this Views plugin is that form caching is disabled, so we can't use $form_state to store data between requests, only within the request. So try to count the fields in the submitted user input:

  // Gather the number of filters in the form already.
  $num_filters = $form_state->get('num_filters');

  if ($num_filters === NULL) {
    // Count the filter fields submitted. If none, start with one filter
    $input = $form_state->getUserInput();
    if (isset($input['foo']['filters_fieldset']['filter'])) {
      $num_filters = count($input['foo']['filters_fieldset']['filter']);
    }
    else {
      $num_filters = 1;          
    }
    $form_state->set('num_filters', $num_filters);
  }
5
  • Thanks mate, you´re a legend! To be complete on this i also had to return $form['options']['foo']['filters_fieldset']; instead of return $form['foo']['filters_fieldset']; Actually i already stumbled across static::class but i did expect it to work different for '#submit' and '#ajax', much thx again
    – john Smith
    Commented Aug 29, 2022 at 12:01
  • not actually sure why, but it seems the formstate only updates once, e.g i can only add 1 more element and then $form_state->get('num_filters'); keeps stuck at 2, are there any known downsides of making those callbacks static?
    – john Smith
    Commented Aug 29, 2022 at 13:02
  • No, as long as you only use $form and $form_state and no properties of the plugin. This looks more like you reset the form state value without checking if there is already a value set.
    – 4uk4
    Commented Aug 29, 2022 at 13:28
  • Yes, buildOptionsForm is called twice when click "add Entry" button and the first time $form_state->get('num_filters') is always null, which will reset it to 1 and addFilterEntry increments it to 2 over and over again.
    – john Smith
    Commented Aug 29, 2022 at 14:15
  • Yes, this is the first submit. In the second submit the form is cached, so the form build before the submit handler is no longer necessary and the submit handler adds 2 + 1 = 3.
    – 4uk4
    Commented Aug 29, 2022 at 14:57

Your Answer

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

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