From 51cdafbefac71d2a46be740b31581f6117c27aee Mon Sep 17 00:00:00 2001
From: "Christian.wiedemann"
 <7688-Christian.wiedemann@users.noreply.drupalcode.org>
Date: Mon, 17 Jun 2024 19:48:21 +0000
Subject: [PATCH] Issue #3455074 by Christian.wiedemann: [2.0.0-alpha3]
 Refactoring of form elements

---
 src/Element/ComponentForm.php      |  68 ++---
 src/Element/ComponentFormBase.php  |  20 --
 src/Element/ComponentPropForm.php  | 210 ++++++++++++++++
 src/Element/ComponentPropsForm.php | 184 +++-----------
 src/Element/ComponentSlotForm.php  | 383 +++++++++++++++++++++++++++++
 src/Element/ComponentSlotsForm.php | 331 ++-----------------------
 6 files changed, 689 insertions(+), 507 deletions(-)
 create mode 100644 src/Element/ComponentPropForm.php
 create mode 100644 src/Element/ComponentSlotForm.php

diff --git a/src/Element/ComponentForm.php b/src/Element/ComponentForm.php
index fb04e0cc1..2624babde 100644
--- a/src/Element/ComponentForm.php
+++ b/src/Element/ComponentForm.php
@@ -4,44 +4,48 @@ namespace Drupal\ui_patterns\Element;
 
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Plugin\Context\Context;
+use Drupal\Core\Plugin\Context\ContextDefinition;
 
 /**
  * Provides a Component form builder element.
  *
- * Use #component_id to set an unchangeable component.
- *
- * The value of the form element contains following keys:
- *  [
- *    'component_id' => 'my_module:my_component',
- *    'variant_id' => 'variant',
- *    'slots' => [
- *      'slots_id' => [
- *        ['source_id' => 'id', 'value' => 'Source value']
- *      ],
- *    ],
- *    'props' => [
- *      ['props_id' =>
- *       ['source_id' => 'id', 'value' => 'Source value ']
- *      ]
- *    ],
- *  ]
- * The same component value is returned in the form element.
- *
  * Usage example:
  *
  * @code
  * $form['component_form'] = [
  *   '#type' => 'component_form',
- *
  *   '#default_value' => [
- *     'component_id' => 'my_module:my_component',
- *     'variant_id' => 'variant',
- *     'slots' => [],
- *     'props' => [],
  *   ],
  * ];
  * @endcode
  *
+ * Value example:
+ *
+ * @code
+ *   ['#default_value' => [
+ *     'component_id' => 'my_module:my_component',
+ *     'variant_id' => 'variant',
+ *     'slots' => [
+ *       'slots_id' => [
+ *         ['source_id' => 'id', 'value' => 'Source value']
+ *       ],
+ *     ],
+ *     'props' => [
+ *       ['props_id' =>
+ *        ['source_id' => 'id', 'value' => 'Source value ']
+ *       ]
+ *     ],
+ *   ]
+ *  ]
+ * @endcode
+ *
+ * Additional Configuration:
+ *
+ * '#component_id' => Optional Component Id. If not set a component selector is set.
+ * '#source_contexts' => The context of the sources.
+ * '#tag_filter' => Filter sources based on this tags.
+ *
  * @FormElement("component_form")
  */
 class ComponentForm extends ComponentFormBase {
@@ -58,6 +62,7 @@ class ComponentForm extends ComponentFormBase {
       '#multiple' => FALSE,
       '#default_value' => NULL,
       '#source_contexts' => [],
+      '#tag_filter' => [],
       '#process' => [
         [$class, 'buildForm'],
       ],
@@ -96,24 +101,21 @@ class ComponentForm extends ComponentFormBase {
    * Processes the main form element including component selector.
    */
   public static function buildForm(array &$element, FormStateInterface $form_state) {
-    $trigger_element = $form_state->getTriggeringElement();
-    if ($form_state->isRebuilding() && isset($trigger_element['#ui_patterns'])) {
-      $parents_for_trigger = self::getComponentFormStateParents($trigger_element['#parents']);
-      if ($parents_for_trigger == $element['#parents']) {
-        $value = $form_state->getValue($parents_for_trigger);
-        self::valueCallback($element, $value, $form_state);
-      }
-    }
 
     $initial_component_id = $element['#component_id'] ?? NULL;
     $component_id = $initial_component_id ?? $element['#default_value']['component_id'] ?? NULL;
     $wrapper_id = static::getElementId($element, 'ui-patterns-component');
+    if ($component_id) {
+      $contextComponentDefinition = ContextDefinition::create('string');
+      $element['#source_contexts']['component_id'] = new Context($contextComponentDefinition, $component_id);
+    }
     if ($initial_component_id === NULL) {
       $element["component_id"] = self::expandAjax(self::buildComponentSelectorForm(
         $wrapper_id,
         $component_id
       ));
     }
+
     self::buildComponentForm(
       $element,
       $wrapper_id,
@@ -221,6 +223,7 @@ class ComponentForm extends ComponentFormBase {
       '#type' => 'component_slots_form',
       '#component_id' => $component_id,
       '#source_contexts' => $element['#source_contexts'],
+      '#tag_filter' =>$element['#tag_filter'],
       '#ajax_url' => $element['#ajax_url'],
       '#access' => $element['#render_slots'] ?? TRUE,
       '#default_value' => [
@@ -238,6 +241,7 @@ class ComponentForm extends ComponentFormBase {
       '#type' => 'component_props_form',
       '#component_id' => $component_id,
       '#source_contexts' => $element['#source_contexts'],
+      '#tag_filter' =>$element['#tag_filter'],
       '#ajax_url' => $element['#ajax_url'],
       '#access' => $element['#render_props'] ?? TRUE,
       '#default_value' => [
diff --git a/src/Element/ComponentFormBase.php b/src/Element/ComponentFormBase.php
index 052d66243..d40ee0e29 100644
--- a/src/Element/ComponentFormBase.php
+++ b/src/Element/ComponentFormBase.php
@@ -50,24 +50,4 @@ abstract class ComponentFormBase extends FormElementBase {
     return $component_id ? $component_plugin_manager->find($component_id) : NULL;
   }
 
-  /**
-   * Returns the parents belonging to the form builder.
-   *
-   * @param array $parents
-   *   The form_state '#parents' value.
-   *
-   * @return array
-   *   The parents to locate the form builder.
-   */
-  protected static function getComponentFormStateParents(array $parents): array {
-    $reverse_parents = array_reverse($parents);
-    $needle = array_search('ui_patterns', $reverse_parents);
-    if (FALSE === $needle) {
-      return [];
-    }
-    $sliced = count($parents) - $needle;
-    $returned = array_slice($parents, 0, $sliced);
-    return $returned;
-  }
-
 }
diff --git a/src/Element/ComponentPropForm.php b/src/Element/ComponentPropForm.php
new file mode 100644
index 000000000..b119af138
--- /dev/null
+++ b/src/Element/ComponentPropForm.php
@@ -0,0 +1,210 @@
+<?php
+
+namespace Drupal\ui_patterns\Element;
+
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\Element;
+use Drupal\ui_patterns\SourceInterface;
+use Drupal\ui_patterns\SourcePluginBase;
+
+/**
+ * Component to render a single prop.
+ *
+ * Usage example:
+ *
+ * @code
+ * $form['prop_name'] = [
+ *   '#type' => 'component_prop_form',
+ *   '#component_id' => 'component_id',
+ *   '#prop_id' => 'prop'
+ *   '#default_value' => [
+ *     'source' => [],
+ *     'source_id' => 'textfield'
+ *   ],
+ * ];
+ * @endcode
+ *
+ * Value example:
+ *
+ * @code
+ * '#default_value' => ['source_id' => 'id', 'source' => []]
+ * @endcode
+ *
+ *  Configuration:
+ *
+ *  '#component_id' => Required Component ID.
+ *  '#prop_id' => Required Prop ID.
+ *  '#source_contexts' => The context of the sources.
+ *  '#tag_filter' => Filter sources based on these tags.
+ *
+ * @FormElement("component_prop_form")
+ */
+class ComponentPropForm extends ComponentFormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInfo() {
+    $class = get_class($this);
+    return [
+      '#input' => TRUE,
+      '#multiple' => FALSE,
+      '#default_value' => NULL,
+      '#source_contexts' => [],
+      '#tag_filter' => [],
+      '#component_id' => NULL,
+      '#slot_id' => NULL,
+      '#process' => [
+        [$class, 'buildForm'],
+      ],
+      '#theme_wrappers' => ['fieldset'],
+    ];
+  }
+
+  /**
+   * Build props forms.
+   */
+  public static function buildForm(array &$element, FormStateInterface $form_state): array {
+    $element['#tree'] = TRUE;
+    $prop_id = $element['#prop_id'];
+    $component = static::getComponent($element);
+    $props = $component->metadata->schema['properties'];
+    $definition = $props[$prop_id];
+    $configuration = $element['#default_value'] ?? [];
+    $sources = static::getSources($prop_id, $definition, $element);
+    $selected_source = static::getSelectedSource($configuration, $sources);
+    if (!$selected_source) {
+      $selected_source = static::getDefaultSource($prop_id, $definition, $element);
+      if (!isset($sources[$selected_source->getPluginId()])) {
+        $selected_source = current($sources);
+      }
+    }
+    if (!$selected_source) {
+      return [];
+    }
+
+    $wrapper_id = static::getElementId($element, 'ui-patterns-prop-item-' . $prop_id);
+    $source_selector = static::buildSourceSelector($sources, $selected_source, $wrapper_id);
+    $source_form = static::getSourcePluginForm($form_state, $selected_source, $wrapper_id);
+
+    $element += [
+      'source_id' => $source_selector,
+      'source' => $source_form,
+    ];
+    $element['#attributes']['style'] = 'position: relative;';
+    return $element;
+  }
+
+  /**
+   * Get source plugin form.
+   */
+  protected static function getSourcePluginForm(FormStateInterface $form_state, SourceInterface $source, string $wrapper_id): array {
+    $form = $source->settingsForm([], $form_state);
+    $form["#type"] = 'container';
+    $form['#attributes'] = [
+      'id' => $wrapper_id,
+    ];
+    // Weird, but :switchSourceForm() AJAX handler doesn't work without that.
+    foreach (Element::children($form) as $child) {
+      if (isset($form[$child]['#description']) && !isset($form[$child]['#description_display'])) {
+        $form[$child]['#description_display'] = 'after';
+      }
+    }
+    return $form;
+  }
+
+  /**
+   * Build sources selector widget.
+   */
+  protected static function buildSourceSelector(array $sources, SourceInterface $selected_source, string $wrapper_id): array {
+    if (empty($sources)) {
+      return [];
+    }
+    if (count($sources) == 1) {
+      return [
+        '#type' => 'hidden',
+        '#value' => array_keys($sources)[0],
+      ];
+    }
+    $options = [];
+    foreach ($sources as $source_id => $source) {
+      $options[$source_id] = $source->label();
+    }
+    return [
+      '#type' => 'select',
+      "#options" => $options,
+      '#default_value' => $selected_source->getPluginId(),
+      '#attributes' => [
+        'style' => "position: absolute; top: 0; right: 0;",
+      ],
+      '#prop_id' => $selected_source->getPropId(),
+      '#prop_definition' => $selected_source->getPropDefinition(),
+      '#ajax' => [
+        'callback' => [
+          static::class,
+          'switchSourceForm',
+        ],
+        'wrapper' => $wrapper_id,
+        'effect' => 'fade',
+      ],
+    ];
+  }
+
+  /**
+   * Ajax handler: Switch source plugin form.
+   */
+  public static function switchSourceForm(array $form, FormStateInterface $form_state): array {
+    $selector = $form_state->getTriggeringElement();
+    $parents = $selector["#array_parents"];
+    $subform = NestedArray::getValue($form, array_slice($parents, 0, -1));
+    return $subform["source"];
+  }
+
+  /**
+   * Get sources for a prop type.
+   */
+  protected static function getSources(string $prop_id, array $definition, array $element): array {
+    $configuration = $element['#default_value'] ?? [];
+    $source_contexts = $element['#source_contexts'];
+    $form_array_parents = $element['#array_parents'];
+    $tag_filter = $element['#tag_filter'];
+    $prop_type = $definition['ui_patterns']['type_definition'];
+    $source_plugin_manager = \Drupal::service("plugin.manager.ui_patterns_source");
+    $source_ids = array_keys($source_plugin_manager->getDefinitionsForPropType($prop_type->getPluginId(), $source_contexts, $tag_filter));
+    $source_ids = array_combine($source_ids, $source_ids);
+    if (empty($source_ids)) {
+      return [];
+    }
+    return $source_plugin_manager->createInstances($source_ids, SourcePluginBase::buildConfiguration($prop_id, $definition, $configuration, $source_contexts, $form_array_parents));
+  }
+
+  /**
+   * Get selected source plugin.
+   */
+  protected static function getSelectedSource(array $configuration, array $sources): ?SourceInterface {
+    if (isset($configuration['source_id']) && $sources[$configuration['source_id']]) {
+      return $sources[$configuration['source_id']];
+    }
+    return NULL;
+  }
+
+  /**
+   * Get default source plugin.
+   */
+  protected static function getDefaultSource(string $prop_id, array $definition, $element): ?SourceInterface {
+    $configuration = $element['#default_value'] ?? [];
+    $source_contexts = $element['#source_contexts'];
+    $form_array_parents = $element['#array_parents'];
+    $tag_filter = $element['#tag_filter'];
+    $source_plugin_manager = \Drupal::service("plugin.manager.ui_patterns_source");
+    $prop_type = $definition['ui_patterns']['type_definition'];
+    $source_id = $source_plugin_manager->getPropTypeDefault($prop_type->getPluginId(), $source_contexts, $tag_filter);
+    if (!$source_id) {
+      return NULL;
+    }
+    $plugin_configuration = SourcePluginBase::buildConfiguration($prop_id, $definition, $configuration, $source_contexts, $form_array_parents);
+    return $source_plugin_manager->createInstance($source_id, $plugin_configuration);
+  }
+
+}
diff --git a/src/Element/ComponentPropsForm.php b/src/Element/ComponentPropsForm.php
index 4186712d4..61851c7c1 100644
--- a/src/Element/ComponentPropsForm.php
+++ b/src/Element/ComponentPropsForm.php
@@ -2,29 +2,17 @@
 
 namespace Drupal\ui_patterns\Element;
 
-use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\Element;
-use Drupal\ui_patterns\SourceInterface;
-use Drupal\ui_patterns\SourcePluginBase;
 
 /**
- * Provides a Component Prop form builder element.
- *
- * The value of the form element contains following keys:
- *  [
- *    'props' => [
- *      ['props_id' =>
- *       ['source_id' => 'id', 'value' => 'Source value ']
- *      ]
- *    ],
- *  ]
- * The same component value is returned in the form element.
+ * Component to render all props of a component.
  *
  * Usage example:
  *
  * @code
- * $form['slots'] = [
+ * $form['props'] = [
+ *   '#component_id' => 'component_id',
  *   '#type' => 'component_props_form',
  *   '#default_value' => [
  *     'props' => [],
@@ -32,6 +20,24 @@ use Drupal\ui_patterns\SourcePluginBase;
  * ];
  * @endcode
  *
+ * Value example:
+ *
+ * @code
+ *   ['#default_value' =>
+ *     'props' => [
+ *       ['props_id' =>
+ *        ['source_id' => 'id', 'source' => []]
+ *       ]
+ *     ],
+ *   ]
+ * @endcode
+ *
+ * Configuration:
+ *
+ *  '#component_id' => Required Component ID.
+ *  '#source_contexts' => The context of the sources.
+ *  '#tag_filter' => Filter sources based on these tags.
+ *
  * @FormElement("component_props_form")
  */
 class ComponentPropsForm extends ComponentFormBase {
@@ -45,7 +51,9 @@ class ComponentPropsForm extends ComponentFormBase {
       '#input' => TRUE,
       '#multiple' => FALSE,
       '#default_value' => NULL,
+      '#component_id' => NULL,
       '#source_contexts' => [],
+      '#tag_filter' => [],
       '#process' => [
         [$class, 'buildForm'],
       ],
@@ -65,7 +73,16 @@ class ComponentPropsForm extends ComponentFormBase {
     }
     $configuration = $element['#default_value']['props'] ?? [];
     foreach ($props as $prop_id => $prop) {
-      $element[$prop_id] = static::buildPropForm($element, $form_state, $prop_id, $prop, $configuration[$prop_id] ?? [], $contexts);
+      $prop_type = $prop['ui_patterns']['type_definition'];
+      $element[$prop_id] = [
+        '#type' => 'component_prop_form',
+        '#title' => $prop["title"] ?? $prop_type->label(),
+        '#default_value' => $configuration[$prop_id] ?? [],
+        '#source_contexts' => $contexts,
+        '#tag_filter' => $element['#tag_filter'],
+        '#component_id' => $component->getPluginId(),
+        '#prop_id' => $prop_id,
+      ];
     }
     if (count(Element::children($element)) === 0) {
       hide($element);
@@ -73,139 +90,4 @@ class ComponentPropsForm extends ComponentFormBase {
     return $element;
   }
 
-  /**
-   * Get sources for a prop type.
-   */
-  protected static function getSources(string $prop_id, array $definition, array $configuration, array $source_contexts, array $form_array_parents): array {
-    $prop_type = $definition['ui_patterns']['type_definition'];
-    $source_plugin_manager = \Drupal::service("plugin.manager.ui_patterns_source");
-    $source_ids = array_keys($source_plugin_manager->getDefinitionsForPropType($prop_type->getPluginId(), $source_contexts));
-    $source_ids = array_combine($source_ids, $source_ids);
-    if (empty($source_ids)) {
-      return [];
-    }
-    return $source_plugin_manager->createInstances($source_ids, SourcePluginBase::buildConfiguration($prop_id, $definition, $configuration, $source_contexts, $form_array_parents));
-  }
-
-  /**
-   * Get selected source plugin.
-   */
-  protected static function getSelectedSource(array $configuration, array $sources): ?SourceInterface {
-    if (isset($configuration['source_id']) && $sources[$configuration['source_id']]) {
-      return $sources[$configuration['source_id']];
-    }
-    return NULL;
-  }
-
-  /**
-   * Get default source plugin.
-   */
-  protected static function getDefaultSource(string $prop_id, array $definition, array $configuration, array $source_contexts, array $form_array_parents): ?SourceInterface {
-    $source_plugin_manager = \Drupal::service("plugin.manager.ui_patterns_source");
-    $prop_type = $definition['ui_patterns']['type_definition'];
-    $source_id = $source_plugin_manager->getPropTypeDefault($prop_type->getPluginId(), $source_contexts);
-    if (!$source_id) {
-      return NULL;
-    }
-    $plugin_configuration = SourcePluginBase::buildConfiguration($prop_id, $definition, $configuration, $source_contexts, $form_array_parents);
-    return $source_plugin_manager->createInstance($source_id, $plugin_configuration);
-  }
-
-  /**
-   * Build single prop form.
-   */
-  protected static function buildPropForm(array $element, FormStateInterface $form_state, string $prop_id, array $definition, array $configuration, array $source_contexts): array {
-    $form_array_parents = $element["#array_parents"];
-    $sources = static::getSources($prop_id, $definition, $configuration, $source_contexts, $form_array_parents);
-    $selected_source = static::getSelectedSource($configuration, $sources);
-    if (!$selected_source) {
-      $selected_source = static::getDefaultSource($prop_id, $definition, $configuration, $source_contexts, $form_array_parents);
-    }
-    if (!$selected_source) {
-      return [];
-    }
-
-    $form_array_parents[] = $prop_id;
-    $form_array_parents[] = "source";
-    $wrapper_id = static::getElementId($element, 'ui-patterns-prop-item-' . $prop_id);
-    $source_selector = static::buildSourceSelector($sources, $selected_source, $wrapper_id);
-    $source_form = static::getSourcePluginForm($form_state, $selected_source, $wrapper_id, $form_array_parents);
-    $prop_type = $definition['ui_patterns']['type_definition'];
-    $build = [
-      '#type' => 'fieldset',
-      '#title' => $definition["title"] ?? $prop_type->label(),
-      '#attributes' => [
-        'style' => "position: relative;",
-      ],
-      'source_id' => $source_selector,
-      'source' => $source_form,
-    ];
-    return $build;
-  }
-
-  /**
-   * Get source plugin form.
-   */
-  protected static function getSourcePluginForm(FormStateInterface $form_state, SourceInterface $source, string $wrapper_id, array $form_array_parents): array {
-    $form = $source->settingsForm([], $form_state);
-    $form["#type"] = 'container';
-    $form['#attributes'] = [
-      'id' => $wrapper_id,
-    ];
-    // Weird, but :switchSourceForm() AJAX handler doesn't work without that.
-    foreach (Element::children($form) as $child) {
-      if (isset($form[$child]['#description']) && !isset($form[$child]['#description_display'])) {
-        $form[$child]['#description_display'] = 'after';
-      }
-    }
-    return $form;
-  }
-
-  /**
-   * Build sources selector widget.
-   */
-  protected static function buildSourceSelector(array $sources, SourceInterface $selected_source, string $wrapper_id): array {
-    if (empty($sources)) {
-      return [];
-    }
-    if (count($sources) == 1) {
-      return [
-        '#type' => 'hidden',
-        '#value' => array_keys($sources)[0],
-      ];
-    }
-    $options = [];
-    foreach ($sources as $source_id => $source) {
-      $options[$source_id] = $source->label();
-    }
-    return [
-      '#type' => 'select',
-      "#options" => $options,
-      '#default_value' => $selected_source->getPluginId(),
-      '#attributes' => [
-        'style' => "position: absolute; top: 0; right: 0;",
-      ],
-      '#prop_id' => $selected_source->getPropId(),
-      '#prop_definition' => $selected_source->getPropDefinition(),
-      '#ajax' => [
-        'callback' => [
-          static::class,
-          'switchSourceForm',
-        ],
-        'wrapper' => $wrapper_id,
-        'effect' => 'fade',
-      ],
-    ];
-  }
-
-  /**
-   * Ajax handler: Switch source plugin form.
-   */
-  public static function switchSourceForm(array $form, FormStateInterface $form_state): array {
-    $selector = $form_state->getTriggeringElement();
-    $parents = $selector["#array_parents"];
-    $subform = NestedArray::getValue($form, array_slice($parents, 0, -1));
-    return $subform["source"];
-  }
-
 }
diff --git a/src/Element/ComponentSlotForm.php b/src/Element/ComponentSlotForm.php
new file mode 100644
index 000000000..ee23efe7f
--- /dev/null
+++ b/src/Element/ComponentSlotForm.php
@@ -0,0 +1,383 @@
+<?php
+
+namespace Drupal\ui_patterns\Element;
+
+use Drupal\Component\Utility\Html;
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\Element;
+use Drupal\ui_patterns\SourcePluginBase;
+
+/**
+ * Component to render a single slot.
+ *
+ * Usage example:
+ *
+ * @code
+ * $form['slot'] = [
+ *   '#type' => 'component_slot_form',
+ *   '#component_id' => 'card',
+ *   '#slot_id' => 'body',
+ *   '#default_value' => [
+ *     'sources' => [],
+ *   ],
+ * ];
+ * @endcode
+ *
+ * Value example:
+ *
+ * @code
+ *    ['#default_value' =>
+ *      ['sources' =>
+ *        ['source_id' => 'id', 'value' => []]
+ *      ]
+ *    ]
+ * @endcode
+ *
+ * Configuration:
+ *
+ * '#component_id' => Optional Component ID. A slot can rendered without knowing any context.
+ * '#slot_id' => Optional Slot ID.
+ * '#source_contexts' => The context of the sources.
+ * '#tag_filter' => Filter sources based on these tags.
+ * '#display_remove' => Display or hide the remove button. Default = true
+ * '#cardinality_multiple' => Allow or disallow multiple slot items
+ *
+ * @FormElement("component_slot_form")
+ */
+class ComponentSlotForm extends ComponentFormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInfo() {
+    $class = get_class($this);
+    return [
+      '#input' => TRUE,
+      '#multiple' => FALSE,
+      '#default_value' => NULL,
+      '#source_contexts' => [],
+      '#tag_filter' => [],
+      '#display_remove' => TRUE,
+      '#component_id' => NULL,
+      '#slot_id' => NULL,
+      '#cardinality_multiple' => TRUE,
+      '#process' => [
+        [$class, 'buildForm'],
+      ],
+      '#theme_wrappers' => ['fieldset'],
+    ];
+  }
+
+  /**
+   * Build single slot form.
+   */
+  public static function buildForm(array &$element, FormStateInterface $form_state): array {
+    $slot_id = $element['#slot_id'];
+    $trigger_element = $form_state->getTriggeringElement();
+    if ($form_state->isRebuilding() && isset($trigger_element['#ui_patterns_slot'])) {
+      if ($trigger_element['#ui_patterns_slot_parents'] == $element['#parents']) {
+        $value = $form_state->getValue($trigger_element['#ui_patterns_slot_parents']);
+        $element['#default_value'] = $value;
+      }
+    }
+
+    $component = static::getComponent($element);
+    if ($component !== NULL) {
+      $slots = $component->metadata->slots;
+      $definition = $slots[$slot_id];
+    }
+    else {
+      /** @var \Drupal\ui_patterns\PropTypePluginManager $prop_type_manager */
+      $prop_type_manager = \Drupal::service("plugin.manager.ui_patterns_prop_type");
+      $definition = [
+        'ui_patterns' => $prop_type_manager->createInstance('slot', [])
+      ];
+    }
+
+    $wrapper_id = static::getElementId($element, 'ui-patterns-slot-' . $slot_id);
+    $element['#prefix'] = '<div id="' . $wrapper_id . '">';
+    $element['#suffix'] = '</div>';
+    $element['#tree'] = TRUE;
+
+    $element['sources'] = static::buildSourcesForm($element, $form_state, $definition, $wrapper_id);
+    if ($element['#cardinality_multiple'] === TRUE ||
+      (!isset($element['#default_value']['sources']) || count($element['#default_value']['sources']) === 0)) {
+      $element['add_more_button'] = static::buildSourceSelector($element, $wrapper_id);
+    }
+
+    return $element;
+  }
+
+  /**
+   * Returns the dropdown options array.
+   */
+  private static function getSourceOptions($element):array {
+    if (isset($element['#source_options'])) {
+      return $element['#source_options'];
+    }
+    /** @var \Drupal\ui_patterns\SourcePluginManager $sources_manager */
+    $sources_manager = \Drupal::service("plugin.manager.ui_patterns_source");
+    $source_contexts = $element['#source_contexts'] ?? [];
+    $tag_filter = $element['#tag_filter'] ?? [];
+    $sources = $sources_manager->getDefinitionsForPropType('slot', $source_contexts, $tag_filter);
+
+    $source_ids = array_keys($sources);
+    $source_ids = array_combine($source_ids, $source_ids);
+
+    $valid_source_plugins = $sources_manager->createInstances(
+      $source_ids,
+      SourcePluginBase::buildConfiguration('slot', [], [], $source_contexts, $element['#array_parents']),
+    );
+    $options = [];
+    foreach ($valid_source_plugins as $valid_source_plugin) {
+      $options[$valid_source_plugin->getPluginId()] = $valid_source_plugin->label();
+    }
+    // Cache source options for performance reasons.
+    $element['#source_options'] = $options;
+    return $options;
+  }
+
+  /**
+   * Build single slot's sources form.
+   */
+  protected static function buildSourcesForm($element, $form_state, array $definition, string $wrapper_id): array {
+    $configuration = $element['#default_value'] ?? [];
+    $form = [
+      '#theme' => 'field_multiple_value_form',
+      '#title' => $element['#title'] ?? '',
+      '#cardinality_multiple' => $element['#cardinality_multiple'],
+    ];
+    // Add fake #field_name to avoid errors from
+    // template_preprocess_field_multiple_value_form.
+    $form['#field_name'] = "foo";
+    if (!isset($configuration['sources'])) {
+      return $form;
+    }
+    foreach ($configuration['sources'] as $delta => $source_configuration) {
+      if (!isset($source_configuration['source_id'])) {
+        continue;
+      }
+      $form[$delta] = static::buildSourceForm($element, $form_state, $definition, $source_configuration, $delta, $wrapper_id);
+    }
+    return $form;
+  }
+
+  /**
+   * Build single source form.
+   */
+  protected static function buildSourceForm(array $element, FormStateInterface $form_state, array $definition, array $configuration, int $delta, string $wrapper_id): array {
+    $slot_id = $element['#slot_id'] ?? NULL;
+    $form_array_parents = $element["#array_parents"];
+    $source_contexts = $element['#source_contexts'] ?? [];
+    $form_array_parents[] = $slot_id ?? 'default';
+    $form_array_parents[] = $delta;
+    $form = [];
+    $sources_manager = \Drupal::service("plugin.manager.ui_patterns_source");
+    $source = $sources_manager->createInstance(
+      $configuration['source_id'],
+      SourcePluginBase::buildConfiguration($slot_id, $definition, $configuration, $source_contexts, $form_array_parents)
+    );
+    $form['source'] = $source->settingsForm([], $form_state);
+    $form['source_id'] = [
+      '#type' => 'hidden',
+      '#value' => $source->getPluginId(),
+    ];
+    $form['_weight'] = [
+      '#type' => 'weight',
+      '#title' => t(
+        'Weight for row @number',
+        ['@number' => $delta + 1]
+      ),
+      '#title_display' => 'invisible',
+      '#delta' => count($form),
+      '#default_value' => $configuration['_weight'] ?? $delta,
+      '#weight' => 100,
+    ];
+    if ($element['#display_remove'] === TRUE) {
+      $form['_remove'] = static::buildRemoveSourceButton($element, $slot_id, $wrapper_id, $delta);
+    }
+
+    return $form;
+  }
+
+  /**
+   * Build widget to remove source.
+   */
+  protected static function buildRemoveSourceButton(array $element, string $slot_id, string $wrapper_id, int $delta): array {
+    $id = implode('-', $element['#array_parents']);
+    $remove_action = [
+      '#type' => 'submit',
+      '#name' => strtr($slot_id, '-', '_') . $id . '_' . $delta . '_remove',
+      '#value' => t('Remove'),
+      '#submit' => [
+        static::class . '::removeSource',
+      ],
+      '#access' => TRUE,
+      '#delta' => $delta,
+      '#ui_patterns_slot' => TRUE,
+      '#ui_patterns_slot_parents' => $element['#parents'],
+      '#ui_patterns_slot_array_parents' => $element['#array_parents'],
+      '#ajax' => [
+        'callback' => [static::class, 'refreshForm'],
+        'wrapper' => $wrapper_id,
+        'effect' => 'fade',
+      ],
+    ];
+    return [
+      '#type' => 'container',
+      'dropdown_actions' => [
+        static::expandComponentButton($element, $remove_action),
+      ],
+    ];
+  }
+
+  /**
+   * Build source selector.
+   */
+  protected static function buildSourceSelector(array $element, string $wrapper_id): array {
+    $options = self::getSourceOptions($element);
+    $slot_id = $element['#slot_id'];
+    $action_buttons = [];
+    foreach ($options as $source_id => $source_label) {
+      $action_buttons[$source_id] = static::expandComponentButton($element, [
+        '#type' => 'submit',
+        '#name' => strtr($slot_id, '-', '_') . implode('-', $element['#array_parents']) . '_' . $source_id . '_add_more',
+        '#value' => t('Add %source', ['%source' => $source_label]),
+        '#submit' => [
+          static::class . '::addSource',
+        ],
+        '#access' => TRUE,
+        '#source_id' => $source_id,
+        '#ui_patterns_slot' => TRUE,
+        '#ui_patterns_slot_parents' => $element['#parents'],
+        '#ui_patterns_slot_array_parents' => $element['#array_parents'],
+        '#ajax' => [
+          'callback' => [
+            static::class,
+            'refreshForm',
+          ],
+          'wrapper' => $wrapper_id,
+          'effect' => 'fade',
+        ],
+      ]);
+    }
+    return static::buildComponentDropbutton($action_buttons);
+  }
+
+  /**
+   * Build drop button.
+   *
+   * @param array $elements
+   *   Elements for drop button.
+   *
+   * @return array
+   *   Drop button array.
+   */
+  protected static function buildComponentDropbutton(array $elements = []): array {
+    $build = [
+      '#type' => 'container',
+      '#attributes' => ['class' => ['ui-patterns-dropbutton-wrapper']],
+    ];
+
+    $operations = [];
+    // Because we are cloning the elements into title sub element we need to
+    // sort children first.
+    foreach (Element::children($elements, TRUE) as $child) {
+      // Clone the element as an operation.
+      $operations[$child] = ['title' => $elements[$child]];
+
+      // Flag the original element as printed so it doesn't render twice.
+      $elements[$child]['#printed'] = TRUE;
+    }
+
+    $build['operations'] = [
+      '#type' => 'ui_patterns_operations',
+      // Even though operations are run through the "links" element type, the
+      // theme system will render any render array passed as a link "title".
+      '#links' => $operations,
+      '#dropbutton_type' => 'small',
+    ];
+
+    return $build + $elements;
+  }
+
+  /**
+   * Expand button base array into a paragraph widget action button.
+   *
+   * @param array $element
+   *   Element.
+   * @param array $button_base
+   *   Button base render array.
+   *
+   * @return array
+   *   Button render array.
+   */
+  protected static function expandComponentButton(array $element, array $button_base): array {
+    // Do not expand elements that do not have submit handler.
+    if (empty($button_base['#submit'])) {
+      return $button_base;
+    }
+
+    $button = $button_base + [
+      '#type' => 'submit',
+      '#theme_wrappers' => ['input__submit__ui_patterns_action'],
+    ];
+
+    // Html::getId will give us '-' char in name but we want '_' for now so
+    // we use strtr to search&replace '-' to '_'.
+    $button['#name'] = strtr(Html::getId($button_base['#name']), '-', '_');
+    $button['#id'] = static::getElementId($element, $button['#name']);
+
+    if (isset($button['#ajax'])) {
+      $button['#ajax'] += [
+        'effect' => 'fade',
+        // Since a normal throbber is added inline, this has the potential to
+        // break a layout if the button is located in dropbuttons. Instead,
+        // it's safer to just show the fullscreen progress element instead.
+        'progress' => ['type' => 'fullscreen'],
+      ];
+    }
+
+    return static::expandAjax($button);
+  }
+
+  /**
+   * Ajax submit handler: Add source.
+   */
+  public static function addSource(array $form, FormStateInterface $form_state) {
+    $trigger_element = $form_state->getTriggeringElement();
+    $source_id = $trigger_element['#source_id'];
+    $component_form_parents = $trigger_element['#ui_patterns_slot_parents'];
+    $configuration = $form_state->getValue($component_form_parents);
+    $configuration['sources'][] = [
+      'source_id' => $source_id,
+      'source' => [],
+    ];
+
+    $form_state->setValue($component_form_parents, $configuration);
+    $form_state->setRebuild();
+  }
+
+  /**
+   * Ajax handler: Refresh sources form.
+   */
+  public static function refreshForm(array $form, FormStateInterface $form_state) {
+    $parents = $form_state->getTriggeringElement()['#ui_patterns_slot_array_parents'];
+    return NestedArray::getValue($form, $parents);
+  }
+
+  /**
+   * Ajax submit handler: Remove source.
+   */
+  public static function removeSource(array $form, FormStateInterface $form_state): void {
+    $trigger_element = $form_state->getTriggeringElement();
+    $delta = $trigger_element['#delta'];
+    $component_form_parents = $trigger_element['#ui_patterns_slot_parents'];
+    $configuration = $form_state->getValue($component_form_parents);
+    unset($configuration['sources'][$delta]);
+    $form_state->setValue($component_form_parents, $configuration);
+    $form_state->setRebuild();
+  }
+
+}
diff --git a/src/Element/ComponentSlotsForm.php b/src/Element/ComponentSlotsForm.php
index bd11c3879..880317746 100644
--- a/src/Element/ComponentSlotsForm.php
+++ b/src/Element/ComponentSlotsForm.php
@@ -2,38 +2,37 @@
 
 namespace Drupal\ui_patterns\Element;
 
-use Drupal\Component\Utility\Html;
-use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Render\Element;
-use Drupal\ui_patterns\SourcePluginBase;
 
 /**
- * Provides a Component form builder element.
- *
- * Use #component_id to set an unchangeable component.
- *
- * The value of the form element contains following keys:
- *  [
- *    'slots' => [
- *      'slots_id' => [
- *        ['source_id' => 'id', 'value' => 'Source value']
- *      ],
- *    ],
- *  ]
- * The same component value is returned in the form element.
+ * Component to render slots for a component.
  *
  * Usage example:
  *
  * @code
  * $form['slots'] = [
  *   '#type' => 'component_slots_form',
+ *   '#component_id' => 'id'
  *   '#default_value' => [
  *     'slots' => [],
  *   ],
  * ];
  * @endcode
  *
+ * Value example:
+ *
+ * @code
+ *   ['#default_value' =>
+ *     'slots' => [
+ *       'slots_id' => [
+ *         ['sources' =>
+ *           ['source_id' => 'id', 'value' => []]
+ *         ]
+ *       ],
+ *     ],
+ *   ]
+ * @endcode
+ *
  * @FormElement("component_slots_form")
  */
 class ComponentSlotsForm extends ComponentFormBase {
@@ -47,7 +46,9 @@ class ComponentSlotsForm extends ComponentFormBase {
       '#input' => TRUE,
       '#multiple' => FALSE,
       '#default_value' => NULL,
+      '#component_id' => NULL,
       '#source_contexts' => [],
+      '#tag_filter' => [],
       '#process' => [
         [$class, 'buildForm'],
       ],
@@ -60,20 +61,6 @@ class ComponentSlotsForm extends ComponentFormBase {
    */
   public static function buildForm(array &$element, FormStateInterface $form_state): array {
 
-    /** @var \Drupal\ui_patterns\SourcePluginManager $sources_manager */
-    $sources_manager = \Drupal::service("plugin.manager.ui_patterns_source");
-    $source_contexts = $element['#source_contexts'] ?? [];
-    $sources = $sources_manager->getDefinitionsForPropType('slot', $source_contexts);
-    $source_ids = array_keys($sources);
-    $source_ids = array_combine($source_ids, $source_ids);
-    $valid_source_plugins = $sources_manager->createInstances(
-      $source_ids,
-      SourcePluginBase::buildConfiguration('slot', [], [], $source_contexts, NULL),
-    );
-    $options = [];
-    foreach ($valid_source_plugins as $valid_source_plugin) {
-      $options[$valid_source_plugin->getPluginId()] = $valid_source_plugin->label();
-    }
     $component = static::getComponent($element);
     if (!isset($component->metadata->slots) || count(
         $component->metadata->slots
@@ -81,284 +68,20 @@ class ComponentSlotsForm extends ComponentFormBase {
       hide($element);
       return $element;
     }
+    $contexts = $element['#source_contexts'] ?? [];
     $configuration = $element['#default_value']['slots'] ?? [];
     foreach ($component->metadata->slots as $slot_id => $slot) {
-      $element[$slot_id] = static::buildSlotForm($element, $form_state, $slot_id, $slot, $configuration[$slot_id] ?? [], $options, $source_contexts);
-    }
-    return $element;
-  }
-
-  /**
-   * Build single slot form.
-   */
-  protected static function buildSlotForm(array $element, FormStateInterface $form_state, string $slot_id, array $definition, array $configuration, array $options, array $source_contexts): array {
-    $form = [];
-    $wrapper_id = static::getElementId($element, 'ui-patterns-slot-' . $slot_id);
-    $form['#slot_id'] = $slot_id;
-    $form['sources'] = static::buildSourcesForm($element, $form_state, $slot_id, $definition, $configuration, $wrapper_id, $source_contexts);
-    $form['add_more_button'] = static::buildSourceSelector($element, $slot_id, $wrapper_id, $options);
-    return $form;
-  }
-
-  /**
-   * Build single slot's sources form.
-   */
-  protected static function buildSourcesForm(array $element, FormStateInterface $form_state, string $slot_id, array $definition, array $configuration, string $wrapper_id, array $source_contexts): array {
-    $form = [
-      '#theme' => 'field_multiple_value_form',
-      '#title' => $definition['title'],
-      '#cardinality_multiple' => TRUE,
-      '#prefix' => '<div id="' . $wrapper_id . '">',
-      '#suffix' => '</div>',
-    ];
-    // Add fake #field_name to avoid errors from
-    // template_preprocess_field_multiple_value_form.
-    $form['#field_name'] = "foo";
-    if (!isset($configuration['sources'])) {
-      return $form;
-    }
-    foreach ($configuration['sources'] as $delta => $source_configuration) {
-      if (!isset($source_configuration['source_id'])) {
-        continue;
-      }
-      $form[$delta] = static::buildSourceForm($element, $form_state, $slot_id, $definition, $source_configuration, $delta, $wrapper_id, $source_contexts);
-    }
-    return $form;
-  }
-
-  /**
-   * Build single source form.
-   */
-  protected static function buildSourceForm(array $element, FormStateInterface $form_state, string $slot_id, array $definition, array $configuration, int $delta, string $wrapper_id, array $source_contexts): array {
-    $form_array_parents = $element["#array_parents"];
-    $form_array_parents[] = $slot_id;
-    $form_array_parents[] = $delta;
-    $form = [];
-    $sources_manager = \Drupal::service("plugin.manager.ui_patterns_source");
-    $source = $sources_manager->createInstance(
-        $configuration['source_id'],
-        SourcePluginBase::buildConfiguration($slot_id, $definition, $configuration, $source_contexts, $form_array_parents)
-      );
-    $form['source'] = $source->settingsForm([], $form_state);
-    $form['source_id'] = [
-      '#type' => 'hidden',
-      '#value' => $source->getPluginId(),
-    ];
-    $form['_weight'] = [
-      '#type' => 'weight',
-      '#title' => t(
-          'Weight for row @number',
-          ['@number' => $delta + 1]
-      ),
-      '#title_display' => 'invisible',
-      '#delta' => count($form),
-      '#default_value' => $configuration['_weight'] ?? $delta,
-      '#weight' => 100,
-    ];
-    $form['_remove'] = static::buildRemoveSourceButton($element, $slot_id, $wrapper_id, $delta);
-    return $form;
-  }
-
-  /**
-   * Build widget to remove source.
-   */
-  protected static function buildRemoveSourceButton(array $element, string $slot_id, string $wrapper_id, int $delta): array {
-    $remove_action = [
-      '#type' => 'submit',
-      '#name' => strtr($slot_id, '-', '_') . $delta . '_remove',
-      '#value' => t('Remove'),
-      '#submit' => [static::class . '::removeSource'],
-      '#access' => TRUE,
-      '#delta' => $delta,
-      '#ui_patterns' => [
-        'parents' => 5,
-      ],
-      '#slot_id' => $slot_id,
-      '#ajax' => [
-        'callback' => [static::class, 'refreshAfterSourceRemoval'],
-        'wrapper' => $wrapper_id,
-        'effect' => 'fade',
-      ],
-    ];
-    return [
-      '#type' => 'ui_patterns_actions',
-      '#ui_patterns_header' => TRUE,
-      'dropdown_actions' => [
-        static::expandComponentButton($element, $remove_action),
-      ],
-    ];
-  }
-
-  /**
-   * Build source selector.
-   */
-  protected static function buildSourceSelector(array $element, string $slot_id, string $wrapper_id, array $options): array {
-    $action_buttons = [];
-    foreach ($options as $source_id => $source_label) {
-      $unique_name = implode("_", array_merge($element["#array_parents"], [$slot_id, $source_id, "add_more"]));
-      $action_buttons[$source_id] = static::expandComponentButton($element, [
-        '#type' => 'submit',
-      // strtr($slot_id, '-', '_') . $source_id . '_add_more',.
-        '#name' => $unique_name,
-        '#value' => t('Add %source', ['%source' => $source_label]),
-        '#submit' => [
-          static::class . '::addSource',
-        ],
-        '#access' => TRUE,
-        '#ui_patterns' => TRUE,
+      $element[$slot_id] = [
+        '#title' => $slot['title'] ?? '',
+        '#type' => 'component_slot_form',
+        '#default_value' => $configuration[$slot_id] ?? [],
+        '#component_id' => $component->getPluginId(),
         '#slot_id' => $slot_id,
-        '#source_id' => $source_id,
-        '#ajax' => [
-          'callback' => [
-            static::class,
-            'refreshAfterSourceAddition',
-          ],
-
-          'wrapper' => $wrapper_id,
-          'effect' => 'fade',
-        ],
-      ]);
-    }
-    return static::buildComponentDropbutton($action_buttons);
-  }
-
-  /**
-   * Ajax submit handler: Add source.
-   */
-  public static function addSource(array $form, FormStateInterface $form_state) {
-    $trigger_element = $form_state->getTriggeringElement();
-    $slot_id = $trigger_element['#slot_id'];
-    $source_id = $trigger_element['#source_id'];
-    $component_form_parents = static::getComponentFormStateParents($trigger_element['#parents']);
-    $configuration = $form_state->getValue($component_form_parents);
-    $configuration['slots'][$slot_id]['sources'][] = [
-      'source_id' => $source_id,
-      'source' => [],
-    ];
-    $component_form_parents[] = "slots";
-    $form_state->setValue($component_form_parents, $configuration["slots"]);
-    $form_state->setRebuild();
-  }
-
-  /**
-   * Ajax handler: Refresh sources form.
-   */
-  public static function refreshAfterSourceAddition(array $form, FormStateInterface $form_state) {
-    $parents = $form_state->getTriggeringElement()['#array_parents'];
-    $form_layers = count([
-      "(source)",
-      "add_more_button",
-    ]);
-    $form = NestedArray::getValue($form, array_slice($parents, 0, -$form_layers));
-    if (!array_key_exists("sources", $form) || !is_array($form["sources"])) {
-      return [];
-    }
-    return $form['sources'];
-  }
-
-  /**
-   * Ajax submit handler: Remove source.
-   */
-  public static function removeSource(array $form, FormStateInterface $form_state): void {
-    $trigger_element = $form_state->getTriggeringElement();
-    $delta = $trigger_element['#delta'];
-    $slot_id = $trigger_element['#slot_id'];
-    $component_form_parents = static::getComponentFormStateParents($trigger_element['#parents']);
-    $configuration = $form_state->getValue($component_form_parents);
-    unset($configuration['slots'][$slot_id]['sources'][$delta]);
-    $form_state->setValue($component_form_parents, $configuration);
-    $form_state->setRebuild();
-  }
-
-  /**
-   * Ajax handler: Refresh sources form.
-   */
-  final public static function refreshAfterSourceRemoval(array $form, FormStateInterface $form_state) {
-    $parents = $form_state->getTriggeringElement()['#array_parents'];
-    $form_layers = count([
-      "sources",
-      "(delta)",
-      "_remove",
-      "dropdown_actions",
-    ]);
-    return NestedArray::getValue($form, array_slice($parents, 0, -$form_layers));
-  }
-
-  /**
-   * Build drop button.
-   *
-   * @param array $elements
-   *   Elements for drop button.
-   *
-   * @return array
-   *   Drop button array.
-   */
-  protected static function buildComponentDropbutton(array $elements = []): array {
-    $build = [
-      '#type' => 'container',
-      '#attributes' => ['class' => ['ui-patterns-dropbutton-wrapper']],
-    ];
-
-    $operations = [];
-    // Because we are cloning the elements into title sub element we need to
-    // sort children first.
-    foreach (Element::children($elements, TRUE) as $child) {
-      // Clone the element as an operation.
-      $operations[$child] = ['title' => $elements[$child]];
-
-      // Flag the original element as printed so it doesn't render twice.
-      $elements[$child]['#printed'] = TRUE;
-    }
-
-    $build['operations'] = [
-      '#type' => 'ui_patterns_operations',
-      // Even though operations are run through the "links" element type, the
-      // theme system will render any render array passed as a link "title".
-      '#links' => $operations,
-      '#dropbutton_type' => 'small',
-    ];
-
-    return $build + $elements;
-  }
-
-  /**
-   * Expand button base array into a paragraph widget action button.
-   *
-   * @param array $element
-   *   Element.
-   * @param array $button_base
-   *   Button base render array.
-   *
-   * @return array
-   *   Button render array.
-   */
-  protected static function expandComponentButton(array $element, array $button_base): array {
-    // Do not expand elements that do not have submit handler.
-    if (empty($button_base['#submit'])) {
-      return $button_base;
-    }
-
-    $button = $button_base + [
-      '#type' => 'submit',
-      '#theme_wrappers' => ['input__submit__ui_patterns_action'],
-    ];
-
-    // Html::getId will give us '-' char in name but we want '_' for now so
-    // we use strtr to search&replace '-' to '_'.
-    $button['#name'] = strtr(Html::getId($button_base['#name']), '-', '_');
-    $button['#id'] = static::getElementId($element, $button['#name']);
-
-    if (isset($button['#ajax'])) {
-      $button['#ajax'] += [
-        'effect' => 'fade',
-        // Since a normal throbber is added inline, this has the potential to
-        // break a layout if the button is located in dropbuttons. Instead,
-        // it's safer to just show the fullscreen progress element instead.
-        'progress' => ['type' => 'fullscreen'],
+        '#source_contexts' => $contexts,
+        '#tag_filter' => $element['#tag_filter'],
       ];
     }
-
-    return static::expandAjax($button);
+    return $element;
   }
 
 }
-- 
GitLab