You are here

class YamlFormMultiple in YAML Form 8

Provides a form element to assist in creation of multiple elements.

Plugin annotation

@FormElement("yamlform_multiple");

Hierarchy

Expanded class hierarchy of YamlFormMultiple

2 #type uses of YamlFormMultiple
YamlFormElementOptions::processYamlFormElementOptions in src/Element/YamlFormElementOptions.php
Processes a form element options element.
YamlFormOptions::processYamlFormOptions in src/Element/YamlFormOptions.php
Process options and build options widget.

File

src/Element/YamlFormMultiple.php, line 16

Namespace

Drupal\yamlform\Element
View source
class YamlFormMultiple extends FormElement {

  /**
   * {@inheritdoc}
   */
  public function getInfo() {
    $class = get_class($this);
    return [
      '#input' => TRUE,
      '#label' => t('item'),
      '#labels' => t('items'),
      '#header' => NULL,
      '#element' => [
        '#type' => 'textfield',
        '#title' => t('Item value'),
        '#title_display' => 'invisible',
        '#placeholder' => t('Enter value'),
      ],
      '#cardinality' => FALSE,
      '#empty_items' => 1,
      '#add_more' => 1,
      '#process' => [
        [
          $class,
          'processYamlFormMultiple',
        ],
      ],
      '#theme_wrappers' => [
        'form_element',
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
    if ($input === FALSE) {
      return isset($element['#default_value']) ? $element['#default_value'] : [];
    }
    elseif (is_array($input) && isset($input['items'])) {
      return $input['items'];
    }
    else {
      return NULL;
    }
  }

  /**
   * Process items and build multiple elements widget.
   */
  public static function processYamlFormMultiple(&$element, FormStateInterface $form_state, &$complete_form) {
    $element['#tree'] = TRUE;

    // Add validate callback that extracts the array of items.
    $element['#element_validate'] = [
      [
        get_called_class(),
        'validateYamlFormMultiple',
      ],
    ];

    // Wrap this $element in a <div> that handle #states.
    YamlFormElementHelper::fixStatesWrapper($element);
    if ($element['#cardinality']) {

      // If the cardinality is set limit number of items to this value.
      $number_of_items = $element['#cardinality'];
    }
    else {

      // Get unique key used to store the current number of items.
      $number_of_items_storage_key = self::getStorageKey($element, 'number_of_items');

      // Store the number of items which is the number of
      // #default_values + number of empty_items.
      if ($form_state
        ->get($number_of_items_storage_key) === NULL) {
        if (empty($element['#default_value']) || !is_array($element['#default_value'])) {
          $number_of_default_values = 0;
          $number_of_empty_items = (int) $element['#empty_items'];
        }
        else {
          $number_of_default_values = count($element['#default_value']);
          $number_of_empty_items = 1;
        }
        $form_state
          ->set($number_of_items_storage_key, $number_of_default_values + $number_of_empty_items);
      }
      $number_of_items = $form_state
        ->get($number_of_items_storage_key);
    }
    $table_id = implode('_', $element['#parents']) . '_table';

    // DEBUG: Disable AJAX callback by commenting out the below callback and
    // wrapper.
    $ajax_settings = [
      'callback' => [
        get_called_class(),
        'ajaxCallback',
      ],
      'wrapper' => $table_id,
    ];
    $element['#child_keys'] = Element::children($element['#element']);

    // Build (single) element header.
    $header = self::buildElementHeader($element);

    // Build (single) element rows.
    $row_index = 0;
    $weight = 0;
    $rows = [];
    if (!$form_state
      ->isProcessingInput() && isset($element['#default_value']) && is_array($element['#default_value'])) {
      foreach ($element['#default_value'] as $default_value) {
        $rows[$row_index] = self::buildElementRow($table_id, $row_index, $element, $default_value, $weight++, $ajax_settings);
        $row_index++;
      }
    }
    while ($row_index < $number_of_items) {
      $rows[$row_index] = self::buildElementRow($table_id, $row_index, $element, NULL, $weight++, $ajax_settings);
      $row_index++;
    }

    // Build table.
    $element['items'] = [
      '#prefix' => '<div id="' . $table_id . '" class="yamlform-multiple-table">',
      '#suffix' => '</div>',
      '#type' => 'table',
      '#header' => $header,
      '#tabledrag' => [
        [
          'action' => 'order',
          'relationship' => 'sibling',
          'group' => 'yamlform-multiple-sort-weight',
        ],
      ],
    ] + $rows;

    // Build add items actions.
    if (empty($element['#cardinality'])) {
      $element['add'] = [
        '#prefix' => '<div class="container-inline">',
        '#suffix' => '</div>',
      ];
      $element['add']['submit'] = [
        '#type' => 'submit',
        '#value' => t('Add'),
        '#limit_validation_errors' => [],
        '#submit' => [
          [
            get_called_class(),
            'addItemsSubmit',
          ],
        ],
        '#ajax' => $ajax_settings,
        '#name' => $table_id . '_add',
      ];
      $element['add']['more_items'] = [
        '#type' => 'number',
        '#min' => 1,
        '#max' => 100,
        '#default_value' => $element['#add_more'],
        '#field_suffix' => t('more @labels', [
          '@labels' => $element['#labels'],
        ]),
      ];
    }
    $element['#attached']['library'][] = 'yamlform/yamlform.element.multiple';
    return $element;
  }

  /**
   * Build a single element header.
   *
   * @param array $element
   *   The element.
   *
   * @return array
   *   A render array containing inputs for an element's header.
   */
  public static function buildElementHeader(array $element) {
    if (empty($element['#header'])) {
      return [
        [
          'data' => '',
          'colspan' => 4,
        ],
      ];
    }
    if (is_array($element['#header'])) {
      return $element['#header'];
    }
    $header = [];
    $header['_handle_'] = '';
    if ($element['#child_keys']) {
      foreach ($element['#child_keys'] as $child_key) {
        $header[$child_key] = !empty($element['#element'][$child_key]['#title']) ? $element['#element'][$child_key]['#title'] : '';
      }
    }
    else {
      $header['item'] = isset($element['#element']['#title']) ? $element['#element']['#title'] : '';
    }
    $header['weight'] = t('Weight');
    if (empty($element['#cardinality'])) {
      $header['_operations_'] = '';
    }
    return $header;
  }

  /**
   * Build a single element row.
   *
   * @param string $table_id
   *   The element's table id.
   * @param int $row_index
   *   The row index.
   * @param array $element
   *   The element.
   * @param string $default_value
   *   The default value.
   * @param int $weight
   *   The weight.
   * @param array $ajax_settings
   *   An array containing AJAX callback settings.
   *
   * @return array
   *   A render array containing inputs for an element's value and weight.
   */
  public static function buildElementRow($table_id, $row_index, array $element, $default_value, $weight, array $ajax_settings) {
    if ($element['#child_keys']) {
      foreach ($element['#child_keys'] as $child_key) {
        if (isset($default_value[$child_key])) {
          $element['#element'][$child_key]['#default_value'] = $default_value[$child_key];
        }
      }
    }
    else {
      $element['#element']['#default_value'] = $default_value;
    }
    $row = [];
    $row['_handle_'] = [];
    if ($element['#child_keys'] && !empty($element['#header'])) {
      foreach ($element['#child_keys'] as $child_key) {
        $row[$child_key] = $element['#element'][$child_key];
      }
    }
    else {
      $row['_item_'] = $element['#element'];
    }
    $row['weight'] = [
      '#type' => 'weight',
      '#delta' => 1000,
      '#title' => t('Item weight'),
      '#title_display' => 'invisible',
      '#attributes' => [
        'class' => [
          'yamlform-multiple-sort-weight',
        ],
      ],
      '#default_value' => $weight,
    ];

    // Allow users to add & remove rows if cardinality is not set.
    if (empty($element['#cardinality'])) {
      $row['_operations_'] = [];
      $row['_operations_']['add'] = [
        '#type' => 'image_button',
        '#src' => drupal_get_path('module', 'yamlform') . '/images/icons/plus.svg',
        '#limit_validation_errors' => [],
        '#submit' => [
          [
            get_called_class(),
            'addItemSubmit',
          ],
        ],
        '#ajax' => $ajax_settings,
        // Issue #1342066 Document that buttons with the same #value need a unique
        // #name for the form API to distinguish them, or change the form API to
        // assign unique #names automatically.
        '#row_index' => $row_index,
        '#name' => $table_id . '_add_' . $row_index,
      ];
      $row['_operations_']['remove'] = [
        '#type' => 'image_button',
        '#src' => drupal_get_path('module', 'yamlform') . '/images/icons/ex.svg',
        '#limit_validation_errors' => [],
        '#submit' => [
          [
            get_called_class(),
            'removeItemSubmit',
          ],
        ],
        '#ajax' => $ajax_settings,
        // Issue #1342066 Document that buttons with the same #value need a unique
        // #name for the form API to distinguish them, or change the form API to
        // assign unique #names automatically.
        '#row_index' => $row_index,
        '#name' => $table_id . '_remove_' . $row_index,
      ];
    }
    $row['#weight'] = $weight;
    $row['#attributes']['class'][] = 'draggable';
    return $row;
  }

  /****************************************************************************/

  // Callbacks.

  /****************************************************************************/

  /**
   * Form submission handler for adding more items.
   *
   * @param array $form
   *   An associative array containing the structure of the form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   */
  public static function addItemsSubmit(array &$form, FormStateInterface $form_state) {

    // Get the form list element by going up two levels.
    $button = $form_state
      ->getTriggeringElement();
    $element =& NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -2));

    // Add more items to the number of items.
    $number_of_items_storage_key = self::getStorageKey($element, 'number_of_items');
    $number_of_items = $form_state
      ->get($number_of_items_storage_key);
    $more_items = (int) $element['add']['more_items']['#value'];
    $form_state
      ->set($number_of_items_storage_key, $number_of_items + $more_items);

    // Reset values.
    $element['items']['#value'] = array_values($element['items']['#value']);
    $form_state
      ->setValueForElement($element['items'], $element['items']['#value']);
    NestedArray::setValue($form_state
      ->getUserInput(), $element['items']['#parents'], $element['items']['#value']);

    // Rebuild the form.
    $form_state
      ->setRebuild();
  }

  /**
   * Form submission handler for adding an item.
   *
   * @param array $form
   *   An associative array containing the structure of the form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   */
  public static function addItemSubmit(array &$form, FormStateInterface $form_state) {
    $button = $form_state
      ->getTriggeringElement();
    $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -4));

    // Add item.
    $values = [];
    foreach ($element['items']['#value'] as $row_index => $value) {
      $values[] = $value;
      if ($row_index == $button['#row_index']) {
        $values[] = [
          'item' => '',
          'text' => '',
        ];
      }
    }

    // Add one item to the 'number of items'.
    $number_of_items_storage_key = self::getStorageKey($element, 'number_of_items');
    $number_of_items = $form_state
      ->get($number_of_items_storage_key);
    $form_state
      ->set($number_of_items_storage_key, $number_of_items + 1);

    // Reset values.
    $form_state
      ->setValueForElement($element['items'], $values);
    NestedArray::setValue($form_state
      ->getUserInput(), $element['items']['#parents'], $values);

    // Rebuild the form.
    $form_state
      ->setRebuild();
  }

  /**
   * Form submission handler for removing an item.
   *
   * @param array $form
   *   An associative array containing the structure of the form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   */
  public static function removeItemSubmit(array &$form, FormStateInterface $form_state) {
    $button = $form_state
      ->getTriggeringElement();
    $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -4));
    $values = $element['items']['#value'];

    // Remove item.
    unset($values[$button['#row_index']]);
    $values = array_values($values);

    // Remove one item from the 'number of items'.
    $number_of_items_storage_key = self::getStorageKey($element, 'number_of_items');
    $number_of_items = $form_state
      ->get($number_of_items_storage_key);

    // Never allow the number of items to be less than 1.
    if ($number_of_items != 1) {
      $form_state
        ->set($number_of_items_storage_key, $number_of_items - 1);
    }

    // Reset values.
    $form_state
      ->setValueForElement($element['items'], $values);
    NestedArray::setValue($form_state
      ->getUserInput(), $element['items']['#parents'], $values);

    // Rebuild the form.
    $form_state
      ->setRebuild();
  }

  /**
   * Form submission AJAX callback the returns the list table.
   */
  public static function ajaxCallback(array &$form, FormStateInterface $form_state) {
    $button = $form_state
      ->getTriggeringElement();
    $parent_length = isset($button['#row_index']) ? -4 : -2;
    $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, $parent_length));
    return $element['items'];
  }

  /**
   * Validates form list element.
   */
  public static function validateYamlFormMultiple(&$element, FormStateInterface $form_state, &$complete_form) {

    // IMPORTANT: Must get values from the $form_states since sub-elements
    // may call $form_state->setValueForElement() via their validation hook.
    // @see \Drupal\yamlform\Element\YamlFormEmailConfirm::validateYamlFormEmailConfirm
    // @see \Drupal\yamlform\Element\YamlFormOtherBase::validateYamlFormOther
    $values = NestedArray::getValue($form_state
      ->getValues(), $element['#parents']);

    // Convert values to items.
    $items = self::convertValuesToItems($element, $values['items']);

    // Validate required items.
    if (!empty($element['#required']) && empty($items)) {
      if (isset($element['#required_error'])) {
        $form_state
          ->setError($element, $element['#required_error']);
      }
      elseif (isset($element['#title'])) {
        $form_state
          ->setError($element, t('@name field is required.', [
          '@name' => $element['#title'],
        ]));
      }
      else {
        $form_state
          ->setError($element);
      }
      return;
    }
    $form_state
      ->setValueForElement($element, $items);
  }

  /****************************************************************************/

  // Helper functions.

  /****************************************************************************/

  /**
   * Get unique key used to store the number of items for an element.
   *
   * @param array $element
   *   An element.
   *
   * @return string
   *   A unique key used to store the number of items for an element.
   */
  public static function getStorageKey(array $element, $name) {
    return 'yamlform_multiple__' . $element['#name'] . '__' . $name;
  }

  /**
   * Convert an array containing of values (elements or _item_ and weight) to an array of items.
   *
   * @param array $element
   *   The element.
   * @param array $values
   *   An array containing of item and weight.
   *
   * @return array
   *   An array of items.
   */
  public static function convertValuesToItems(array &$element, array $values = []) {

    // Sort the item values.
    uasort($values, [
      'Drupal\\Component\\Utility\\SortArray',
      'sortByWeightElement',
    ]);

    // Now build the associative array of items.
    $items = [];
    foreach ($values as $value) {
      if (isset($value['_item_'])) {
        if (!self::isEmpty($value['_item_'])) {
          $items[] = $value['_item_'];
        }
      }
      else {
        unset($value['weight'], $value['_operations_']);
        if (!self::isEmpty($value)) {
          $items[] = $value;
        }
      }
    }
    return $items;
  }

  /**
   * Check if array is empty.
   *
   * @param string|array $value
   *   An item.
   *
   * @return bool
   *   FALSE if item is an empty string or an empty array.
   */
  public static function isEmpty($value = NULL) {
    if (is_null($value)) {
      return TRUE;
    }
    elseif (is_string($value)) {
      return $value === '' ? TRUE : FALSE;
    }
    elseif (is_array($value)) {
      return !array_filter($value, function ($item) {
        return !self::isEmpty($item);
      });
    }
    else {
      return FALSE;
    }
  }

}

Members

Namesort descending Modifiers Type Description Overrides
DependencySerializationTrait::$_entityStorages protected property An array of entity type IDs keyed by the property name of their storages.
DependencySerializationTrait::$_serviceIds protected property An array of service IDs keyed by property name used for serialization.
DependencySerializationTrait::__sleep public function 1
DependencySerializationTrait::__wakeup public function 2
FormElement::processAutocomplete public static function Adds autocomplete functionality to elements.
FormElement::processPattern public static function #process callback for #pattern form element property.
FormElement::validatePattern public static function #element_validate callback for #pattern form element property.
MessengerTrait::$messenger protected property The messenger. 29
MessengerTrait::messenger public function Gets the messenger. 29
MessengerTrait::setMessenger public function Sets the messenger.
PluginBase::$configuration protected property Configuration information passed into the plugin. 1
PluginBase::$pluginDefinition protected property The plugin implementation definition. 1
PluginBase::$pluginId protected property The plugin_id.
PluginBase::DERIVATIVE_SEPARATOR constant A string which is used to separate base plugin IDs from the derivative ID.
PluginBase::getBaseId public function Gets the base_plugin_id of the plugin instance. Overrides DerivativeInspectionInterface::getBaseId
PluginBase::getDerivativeId public function Gets the derivative_id of the plugin instance. Overrides DerivativeInspectionInterface::getDerivativeId
PluginBase::getPluginDefinition public function Gets the definition of the plugin implementation. Overrides PluginInspectionInterface::getPluginDefinition 3
PluginBase::getPluginId public function Gets the plugin_id of the plugin instance. Overrides PluginInspectionInterface::getPluginId
PluginBase::isConfigurable public function Determines if the plugin is configurable.
PluginBase::__construct public function Constructs a \Drupal\Component\Plugin\PluginBase object. 92
RenderElement::preRenderAjaxForm public static function Adds Ajax information about an element to communicate with JavaScript.
RenderElement::preRenderGroup public static function Adds members of this group as actual elements for rendering.
RenderElement::processAjaxForm public static function Form element processing handler for the #ajax form property. 1
RenderElement::processGroup public static function Arranges elements into groups.
RenderElement::setAttributes public static function Sets a form element's class attribute. Overrides ElementInterface::setAttributes
StringTranslationTrait::$stringTranslation protected property The string translation service. 1
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.
YamlFormMultiple::addItemsSubmit public static function Form submission handler for adding more items.
YamlFormMultiple::addItemSubmit public static function Form submission handler for adding an item.
YamlFormMultiple::ajaxCallback public static function Form submission AJAX callback the returns the list table.
YamlFormMultiple::buildElementHeader public static function Build a single element header.
YamlFormMultiple::buildElementRow public static function Build a single element row.
YamlFormMultiple::convertValuesToItems public static function Convert an array containing of values (elements or _item_ and weight) to an array of items.
YamlFormMultiple::getInfo public function Returns the element properties for this element. Overrides ElementInterface::getInfo
YamlFormMultiple::getStorageKey public static function Get unique key used to store the number of items for an element.
YamlFormMultiple::isEmpty public static function Check if array is empty.
YamlFormMultiple::processYamlFormMultiple public static function Process items and build multiple elements widget.
YamlFormMultiple::removeItemSubmit public static function Form submission handler for removing an item.
YamlFormMultiple::validateYamlFormMultiple public static function Validates form list element.
YamlFormMultiple::valueCallback public static function Determines how user input is mapped to an element's #value property. Overrides FormElement::valueCallback