3 namespace Drupal\Core\Render\Element;
5 use Drupal\Core\Form\FormStateInterface;
6 use Drupal\Core\Render\Element;
7 use Drupal\Component\Utility\Html as HtmlUtility;
10 * Provides a render element for a table.
12 * Note: Although this extends FormElement, it can be used outside the
16 * - #header: An array of table header labels.
17 * - #rows: An array of the rows to be displayed. Each row is either an array
18 * of cell contents or an array of properties as described in table.html.twig
19 * Alternatively specify the data for the table as child elements of the table
20 * element. Table elements would contain rows elements that would in turn
21 * contain column elements.
22 * - #empty: Text to display when no rows are present.
23 * - #responsive: Indicates whether to add the drupal.responsive_table library
24 * providing responsive tables. Defaults to TRUE.
25 * - #sticky: Indicates whether to add the drupal.tableheader library that makes
26 * table headers always visible at the top of the page. Defaults to FALSE.
27 * - #size: The size of the input element in characters.
31 * $form['contacts'] = array(
33 * '#caption' => $this->t('Sample Table'),
34 * '#header' => array($this->t('Name'), $this->t('Phone')),
37 * for ($i = 1; $i <= 4; $i++) {
38 * $form['contacts'][$i]['#attributes'] = array('class' => array('foo', 'baz'));
39 * $form['contacts'][$i]['name'] = array(
40 * '#type' => 'textfield',
41 * '#title' => $this->t('Name'),
42 * '#title_display' => 'invisible',
45 * $form['contacts'][$i]['phone'] = array(
47 * '#title' => $this->t('Phone'),
48 * '#title_display' => 'invisible',
52 * $form['contacts'][]['colspan_example'] = array(
53 * '#plain_text' => 'Colspan Example',
54 * '#wrapper_attributes' => array('colspan' => 2, 'class' => array('foo', 'bar')),
57 * @see \Drupal\Core\Render\Element\Tableselect
59 * @FormElement("table")
61 class Table extends FormElement {
66 public function getInfo() {
67 $class = get_class($this);
72 // Properties for tableselect support.
75 '#tableselect' => FALSE,
77 '#responsive' => TRUE,
81 [$class, 'processTable'],
83 '#element_validate' => [
84 [$class, 'validateTable'],
86 // Properties for tabledrag support.
87 // The value is a list of arrays that are passed to
88 // drupal_attach_tabledrag(). Table::preRenderTable() prepends the HTML ID
89 // of the table to each set of options.
90 // @see drupal_attach_tabledrag()
94 [$class, 'preRenderTable'],
103 public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
104 // If #multiple is FALSE, the regular default value of radio buttons is used.
105 if (!empty($element['#tableselect']) && !empty($element['#multiple'])) {
106 // Contrary to #type 'checkboxes', the default value of checkboxes in a
107 // table is built from the array keys (instead of array values) of the
108 // #default_value property.
109 // @todo D8: Remove this inconsistency.
110 if ($input === FALSE) {
111 $element += ['#default_value' => []];
112 $value = array_keys(array_filter($element['#default_value']));
113 return array_combine($value, $value);
116 return is_array($input) ? array_combine($input, $input) : [];
122 * #process callback for #type 'table' to add tableselect support.
124 * @param array $element
125 * An associative array containing the properties and children of the
127 * @param \Drupal\Core\Form\FormStateInterface $form_state
128 * The current state of the form.
129 * @param array $complete_form
130 * The complete form structure.
133 * The processed element.
135 public static function processTable(&$element, FormStateInterface $form_state, &$complete_form) {
136 if ($element['#tableselect']) {
137 if ($element['#multiple']) {
138 $value = is_array($element['#value']) ? $element['#value'] : [];
140 // Advanced selection behavior makes no sense for radios.
142 $element['#js_select'] = FALSE;
144 // Add a "Select all" checkbox column to the header.
145 // @todo D8: Rename into #select_all?
146 if ($element['#js_select']) {
147 $element['#attached']['library'][] = 'core/drupal.tableselect';
148 array_unshift($element['#header'], ['class' => ['select-all']]);
150 // Add an empty header column for radio buttons or when a "Select all"
151 // checkbox is not desired.
153 array_unshift($element['#header'], '');
156 if (!isset($element['#default_value']) || $element['#default_value'] === 0) {
157 $element['#default_value'] = [];
159 // Create a checkbox or radio for each row in a way that the value of the
160 // tableselect element behaves as if it had been of #type checkboxes or
162 foreach (Element::children($element) as $key) {
163 $row = &$element[$key];
164 // Prepare the element #parents for the tableselect form element.
165 // Their values have to be located in child keys (#tree is ignored),
166 // since Table::validateTable() has to be able to validate whether input
167 // (for the parent #type 'table' element) has been submitted.
168 $element_parents = array_merge($element['#parents'], [$key]);
170 // Since the #parents of the tableselect form element will equal the
171 // #parents of the row element, prevent FormBuilder from auto-generating
172 // an #id for the row element, since
173 // \Drupal\Component\Utility\Html::getUniqueId() would automatically
174 // append a suffix to the tableselect form element's #id otherwise.
175 $row['#id'] = HtmlUtility::getUniqueId('edit-' . implode('-', $element_parents) . '-row');
177 // Do not overwrite manually created children.
178 if (!isset($row['select'])) {
179 // Determine option label; either an assumed 'title' column, or the
180 // first available column containing a #title or #markup.
181 // @todo Consider to add an optional $element[$key]['#title_key']
182 // defaulting to 'title'?
183 unset($label_element);
185 if (isset($row['title']['#type']) && $row['title']['#type'] == 'label') {
186 $label_element = &$row['title'];
189 if (!empty($row['title']['#title'])) {
190 $title = $row['title']['#title'];
193 foreach (Element::children($row) as $column) {
194 if (isset($row[$column]['#title'])) {
195 $title = $row[$column]['#title'];
198 if (isset($row[$column]['#markup'])) {
199 $title = $row[$column]['#markup'];
204 if (isset($title) && $title !== '') {
205 $title = t('Update @title', ['@title' => $title]);
209 // Prepend the select column to existing columns.
210 $row = ['select' => []] + $row;
212 '#type' => $element['#multiple'] ? 'checkbox' : 'radio',
213 '#id' => HtmlUtility::getUniqueId('edit-' . implode('-', $element_parents)),
214 // @todo If rows happen to use numeric indexes instead of string keys,
215 // this results in a first row with $key === 0, which is always FALSE.
216 '#return_value' => $key,
217 '#attributes' => $element['#attributes'],
218 '#wrapper_attributes' => [
219 'class' => ['table-select'],
222 if ($element['#multiple']) {
223 $row['select']['#default_value'] = isset($value[$key]) ? $key : NULL;
224 $row['select']['#parents'] = $element_parents;
227 $row['select']['#default_value'] = ($element['#default_value'] == $key ? $key : NULL);
228 $row['select']['#parents'] = $element['#parents'];
230 if (isset($label_element)) {
231 $label_element['#id'] = $row['select']['#id'] . '--label';
232 $label_element['#for'] = $row['select']['#id'];
233 $row['select']['#attributes']['aria-labelledby'] = $label_element['#id'];
234 $row['select']['#title_display'] = 'none';
237 $row['select']['#title'] = $title;
238 $row['select']['#title_display'] = 'invisible';
248 * #element_validate callback for #type 'table'.
250 * @param array $element
251 * An associative array containing the properties and children of the
253 * @param \Drupal\Core\Form\FormStateInterface $form_state
254 * The current state of the form.
255 * @param array $complete_form
256 * The complete form structure.
258 public static function validateTable(&$element, FormStateInterface $form_state, &$complete_form) {
259 // Skip this validation if the button to submit the form does not require
260 // selected table row data.
261 $triggering_element = $form_state->getTriggeringElement();
262 if (empty($triggering_element['#tableselect'])) {
265 if ($element['#multiple']) {
266 if (!is_array($element['#value']) || !count(array_filter($element['#value']))) {
267 $form_state->setError($element, t('No items selected.'));
270 elseif (!isset($element['#value']) || $element['#value'] === '') {
271 $form_state->setError($element, t('No item selected.'));
276 * #pre_render callback to transform children of an element of #type 'table'.
278 * This function converts sub-elements of an element of #type 'table' to be
279 * suitable for table.html.twig:
280 * - The first level of sub-elements are table rows. Only the #attributes
281 * property is taken into account.
282 * - The second level of sub-elements is converted into columns for the
283 * corresponding first-level table row.
285 * Simple example usage:
287 * $form['table'] = array(
288 * '#type' => 'table',
289 * '#header' => array($this->t('Title'), array('data' => $this->t('Operations'), 'colspan' => '1')),
290 * // Optionally, to add tableDrag support:
291 * '#tabledrag' => array(
293 * 'action' => 'order',
294 * 'relationship' => 'sibling',
295 * 'group' => 'thing-weight',
299 * foreach ($things as $row => $thing) {
300 * $form['table'][$row]['#weight'] = $thing['weight'];
302 * $form['table'][$row]['title'] = array(
303 * '#type' => 'textfield',
304 * '#default_value' => $thing['title'],
307 * // Optionally, to add tableDrag support:
308 * $form['table'][$row]['#attributes']['class'][] = 'draggable';
309 * $form['table'][$row]['weight'] = array(
310 * '#type' => 'textfield',
311 * '#title' => $this->t('Weight for @title', array('@title' => $thing['title'])),
312 * '#title_display' => 'invisible',
314 * '#default_value' => $thing['weight'],
315 * '#attributes' => array('class' => array('thing-weight')),
318 * // The amount of link columns should be identical to the 'colspan'
319 * // attribute in #header above.
320 * $form['table'][$row]['edit'] = array(
322 * '#title' => $this->t('Edit'),
323 * '#url' => Url::fromRoute('entity.test_entity.edit_form', ['test_entity' => $row]),
328 * @param array $element
329 * A structured array containing two sub-levels of elements. Properties used:
330 * - #tabledrag: The value is a list of $options arrays that are passed to
331 * drupal_attach_tabledrag(). The HTML ID of the table is added to each
336 * @see template_preprocess_table()
337 * @see \Drupal\Core\Render\AttachmentsResponseProcessorInterface::processAttachments()
338 * @see drupal_attach_tabledrag()
340 public static function preRenderTable($element) {
341 foreach (Element::children($element) as $first) {
342 $row = ['data' => []];
343 // Apply attributes of first-level elements as table row attributes.
344 if (isset($element[$first]['#attributes'])) {
345 $row += $element[$first]['#attributes'];
347 // Turn second-level elements into table row columns.
348 // @todo Do not render a cell for children of #type 'value'.
349 // @see https://www.drupal.org/node/1248940
350 foreach (Element::children($element[$first]) as $second) {
351 // Assign the element by reference, so any potential changes to the
352 // original element are taken over.
353 $column = ['data' => &$element[$first][$second]];
355 // Apply wrapper attributes of second-level elements as table cell
357 if (isset($element[$first][$second]['#wrapper_attributes'])) {
358 $column += $element[$first][$second]['#wrapper_attributes'];
361 $row['data'][] = $column;
363 $element['#rows'][] = $row;
366 // Take over $element['#id'] as HTML ID attribute, if not already set.
367 Element::setAttributes($element, ['id']);
369 // Add sticky headers, if applicable.
370 if (count($element['#header']) && $element['#sticky']) {
371 $element['#attached']['library'][] = 'core/drupal.tableheader';
372 // Add 'sticky-enabled' class to the table to identify it for JS.
373 // This is needed to target tables constructed by this function.
374 $element['#attributes']['class'][] = 'sticky-enabled';
376 // If the table has headers and it should react responsively to columns hidden
377 // with the classes represented by the constants RESPONSIVE_PRIORITY_MEDIUM
378 // and RESPONSIVE_PRIORITY_LOW, add the tableresponsive behaviors.
379 if (count($element['#header']) && $element['#responsive']) {
380 $element['#attached']['library'][] = 'core/drupal.tableresponsive';
381 // Add 'responsive-enabled' class to the table to identify it for JS.
382 // This is needed to target tables constructed by this function.
383 $element['#attributes']['class'][] = 'responsive-enabled';
386 // If the custom #tabledrag is set and there is a HTML ID, add the table's
387 // HTML ID to the options and attach the behavior.
388 if (!empty($element['#tabledrag']) && isset($element['#attributes']['id'])) {
389 foreach ($element['#tabledrag'] as $options) {
390 $options['table_id'] = $element['#attributes']['id'];
391 drupal_attach_tabledrag($element, $options);