Demonstrate rendering add-more AJAX form as a block

Created on 27 June 2016, almost 9 years ago
Updated 17 June 2024, 10 months ago

Problem

I'm creating a custom block class that extends BlockBase. In the overridden blockForm function I'm attempting to add a field with an 'Add More' button. The button successfully issues the callback, but the callback always comes back empty.

I can't tell if the form/form_status are not being passed to the callback, or if the callback is not being correctly called, or what specifically the issue is. Is there a different, correct way to do this in a block that I am unaware of?

Form code is based off an upcoming Example module commit https://www.drupal.org/node/2720361 β†’

<?php
/**
 * @file
 * Contains \Drupal\test_module\Plugin\Block\UniqueTestBlock.
 */
namespace Drupal\test_module\Plugin\Block;

use Drupal\Core\Block\BlockBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Block\BlockManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Provides my custom block.
 *
 * @Block(
 *   id = "unique_test_block",
 *   admin_label = @Translation("Unique Test Block"),
 *   category = @Translation("Test Blocks")
 * )
 */
class UniqueTestBlock extends BlockBase implements ContainerFactoryPluginInterface {

  /**
   * The Plugin Block Manager.
   *
   * @var \Drupal\Core\Block\BlockManagerInterface.
   */
  protected $blockManager;

  /**
   * Constructs a new BlockContentBlock.
   *
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $plugin_id
   *   The plugin ID for the plugin instance.
   * @param mixed $plugin_definition
   *   The plugin implementation definition.
   * @param \Drupal\Core\Block\BlockManagerInterface $block_manager
   *   The Plugin Block Manager.
   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
   *   The entity manager service.
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, BlockManagerInterface $block_manager) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);

    $this->blockManager = $block_manager;

  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('plugin.manager.block')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function build() {
    $config = $this->getConfiguration();

    $block = array(
      '#type' => '#markup',
      '#markup' => 'This is a block\'s configuration: <br><pre>' . print_r($config, TRUE) . '</pre>',
    );

    return $block;
  }

  /**
   * Overrides \Drupal\Core\Block\BlockBase::blockForm().
   *
   * Adds body and description fields to the block configuration form.
   */
  public function blockForm($form, FormStateInterface $form_state) {
    $form = parent::blockForm($form, $form_state);
    $config = $this->getConfiguration();

    $i = 0;
    $name_field = $form_state->get('num_names');
    $form['#tree'] = TRUE;
    $form['names_fieldset'] = [
      '#type' => 'fieldset',
      '#title' => $this->t('People coming to picnic'),
      '#prefix' => '<div id="names-fieldset-wrapper">',
      '#suffix' => '</div>',
    ];
    if (empty($name_field)) {
      $name_field = $form_state->set('num_names', 1);
    }
    for ($i = 0; $i < $name_field; $i++) {
      $form['names_fieldset']['name'][$i] = [
        '#type' => 'textfield',
        '#title' => t('Name'),
        '#default_value' => $config['names']['name'][$i],
      ];
    }
    $form['actions'] = [
      '#type' => 'actions',
    ];
    $form['names_fieldset']['actions']['add_name'] = [
      '#type' => 'submit',
      '#value' => t('Add one more'),
      '#submit' => array('::addOne'),
      '#ajax' => [
        'callback' => '\Drupal\test_module\Plugin\Block\UniqueTestBlock::addmoreCallback',
        'wrapper' => 'names-fieldset-wrapper',
      ],
    ];
    if ($name_field > 1) {
      $form['names_fieldset']['actions']['remove_name'] = [
        '#type' => 'submit',
        '#value' => t('Remove one'),
        '#submit' => array('::removeCallback'),
        '#ajax' => [
          'callback' => '\Drupal\test_module\Plugin\Block\UniqueTestBlock::addmoreCallback',
          'wrapper' => 'names-fieldset-wrapper',
        ]
      ];
    }
    $form_state->setCached(FALSE);
    $form['actions']['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Submit'),
    ];

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function blockSubmit($form, FormStateInterface $form_state) {
    $this->setConfigurationValue('names', $form_state->getValue('names_fieldset'));
  }

  /**
   * @param array $form
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   */
  public function addOne(array &$form, FormStateInterface $form_state) {
    $name_field = $form_state->get('num_names');
    $add_button = $name_field + 1;
    $form_state->set('num_names', $add_button);
    $form_state->setRebuild();
  }

  /**
   * @param array $form
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   * @return mixed
   */
  public function addmoreCallback(array &$form, FormStateInterface $form_state) {
    $name_field = $form_state->get('num_names');
    return $form['names_fieldset'];
  }

  /**
   * @param array $form
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   */
  public function removeCallback(array &$form, FormStateInterface $form_state) {
    $name_field = $form_state->get('num_names');
    if ($name_field > 1) {
      $remove_button = $name_field - 1;
      $form_state->set('num_names', $remove_button);
    }
    $form_state->setRebuild();
  }

}
✨ Feature request
Status

Needs work

Version

4.0

Component

AJAX Example

Created by

πŸ‡ΊπŸ‡ΈUnited States dhansen

Live updates comments and jobs are added and updated live.
  • Needs tests

    The change is currently missing an automated test that fails when run with the original code, and succeeds when the bug has been fixed.

Sign in to follow issues

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • πŸ‡¨πŸ‡³China jungle Chongqing, China

    Move this to the current active dev branch

  • πŸ‡ΊπŸ‡ΈUnited States joshua.boltz

    @Taiger The code from #12 does work. Maybe it did't back when you made this comment, but it does in the test I just did with it.
    The main things that made it work in this manner were:
    * Getting the brackets right between [[$this, 'callback']] and [$this, 'callback'] depending on where they are used
    * In the addMoreCallback, for some reason we need the "settings" part of the form to return from

    This is working fine in a custom block. I was about to switch to using your workaround in #13 before I realized I had to have those 2 things correct before it would work properly.

Production build 0.71.5 2024