diff --git a/core/core.services.yml b/core/core.services.yml
index 200972e73b3..2f04124df99 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -749,6 +749,9 @@ services:
plugin.manager.condition:
class: Drupal\Core\Condition\ConditionManager
parent: default_plugin_manager
+ plugin.manager.element_info:
+ class: Drupal\Core\Render\ElementInfoManager
+ parent: default_plugin_manager
kernel_destruct_subscriber:
class: Drupal\Core\EventSubscriber\KernelDestructionSubscriber
tags:
@@ -907,8 +910,7 @@ services:
info_parser:
class: Drupal\Core\Extension\InfoParser
element_info:
- class: Drupal\Core\Render\ElementInfo
- arguments: ['@module_handler']
+ alias: plugin.manager.element_info
file.mime_type.guesser:
class: Drupal\Core\File\MimeType\MimeTypeGuesser
tags:
diff --git a/core/includes/ajax.inc b/core/includes/ajax.inc
index 4a03c6869b6..3b5c2779b13 100644
--- a/core/includes/ajax.inc
+++ b/core/includes/ajax.inc
@@ -6,6 +6,7 @@
*/
use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\Element;
/**
* @defgroup ajax Ajax API
@@ -162,20 +163,10 @@ use Drupal\Core\Form\FormStateInterface;
/**
* Form element processing handler for the #ajax form property.
*
- * @param $element
- * An associative array containing the properties of the element.
- *
- * @return
- * The processed element.
- *
- * @see ajax_pre_render_element()
+ * @deprecated Use \Drupal\Core\Render\Element\FormElement::processAjaxForm().
*/
-function ajax_process_form($element, FormStateInterface $form_state) {
- $element = ajax_pre_render_element($element);
- if (!empty($element['#ajax_processed'])) {
- $form_state['cache'] = TRUE;
- }
- return $element;
+function ajax_process_form($element, FormStateInterface $form_state, &$complete_form) {
+ return Element\FormElement::processAjaxForm($element, $form_state, $complete_form);
}
/**
diff --git a/core/includes/common.inc b/core/includes/common.inc
index 6c6c4a3a88a..18ed1d51046 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -2670,65 +2670,10 @@ function drupal_pre_render_html_tag($element) {
/**
* Pre-render callback: Renders a link into #markup.
*
- * Doing so during pre_render gives modules a chance to alter the link parts.
- *
- * @param $elements
- * A structured array whose keys form the arguments to l():
- * - #title: The link text to pass as argument to l().
- * - One of the following
- * - #route_name and (optionally) and a #route_parameters array; The route
- * name and route parameters which will be passed into the link generator.
- * - #href: The system path or URL to pass as argument to l().
- * - #options: (optional) An array of options to pass to l() or the link
- * generator.
- *
- * @return
- * The passed-in elements containing a rendered link in '#markup'.
+ * @deprecated Use \Drupal\Core\Render\Element\Link::preRenderLink().
*/
function drupal_pre_render_link($element) {
- // By default, link options to pass to l() are normally set in #options.
- $element += array('#options' => array());
- // However, within the scope of renderable elements, #attributes is a valid
- // way to specify attributes, too. Take them into account, but do not override
- // attributes from #options.
- if (isset($element['#attributes'])) {
- $element['#options'] += array('attributes' => array());
- $element['#options']['attributes'] += $element['#attributes'];
- }
-
- // This #pre_render callback can be invoked from inside or outside of a Form
- // API context, and depending on that, a HTML ID may be already set in
- // different locations. #options should have precedence over Form API's #id.
- // #attributes have been taken over into #options above already.
- if (isset($element['#options']['attributes']['id'])) {
- $element['#id'] = $element['#options']['attributes']['id'];
- }
- elseif (isset($element['#id'])) {
- $element['#options']['attributes']['id'] = $element['#id'];
- }
-
- // Conditionally invoke ajax_pre_render_element(), if #ajax is set.
- if (isset($element['#ajax']) && !isset($element['#ajax_processed'])) {
- // If no HTML ID was found above, automatically create one.
- if (!isset($element['#id'])) {
- $element['#id'] = $element['#options']['attributes']['id'] = drupal_html_id('ajax-link');
- }
- // If #ajax['path] was not specified, use the href as Ajax request URL.
- if (!isset($element['#ajax']['path'])) {
- $element['#ajax']['path'] = $element['#href'];
- $element['#ajax']['options'] = $element['#options'];
- }
- $element = ajax_pre_render_element($element);
- }
-
- if (isset($element['#route_name'])) {
- $element['#route_parameters'] = empty($element['#route_parameters']) ? array() : $element['#route_parameters'];
- $element['#markup'] = \Drupal::linkGenerator()->generate($element['#title'], $element['#route_name'], $element['#route_parameters'], $element['#options']);
- }
- else {
- $element['#markup'] = l($element['#title'], $element['#href'], $element['#options']);
- }
- return $element;
+ return Element\Link::preRenderLink($element);
}
/**
diff --git a/core/includes/form.inc b/core/includes/form.inc
index f49dbd26fd1..fb9add490ad 100644
--- a/core/includes/form.inc
+++ b/core/includes/form.inc
@@ -566,22 +566,10 @@ function form_type_select_value($element, $input = FALSE) {
/**
* Determines the value for a textfield form element.
*
- * @param $element
- * The form element whose value is being populated.
- * @param $input
- * The incoming input to populate the form element. If this is FALSE,
- * the element's default value should be returned.
- *
- * @return
- * The data that will appear in the $element_state['values'] collection
- * for this element. Return nothing to use the default.
+ * @deprecated Use \Drupal\Core\Render\Element\Textfield::valueCallback().
*/
-function form_type_textfield_value($element, $input = FALSE) {
- if ($input !== FALSE && $input !== NULL) {
- // Equate $input to the form value to ensure it's marked for
- // validation.
- return str_replace(array("\r", "\n"), '', $input);
- }
+function form_type_textfield_value(&$element, $input, &$form_state) {
+ return Element\Textfield::valueCallback($element, $input, $form_state);
}
/**
@@ -1306,24 +1294,10 @@ function form_pre_render_actions_dropbutton(array $element) {
/**
* #process callback for #pattern form element property.
*
- * @param $element
- * An associative array containing the properties and children of the
- * generic input element.
- * @param $form_state
- * The current state of the form for the form this element belongs to.
- *
- * @return
- * The processed element.
- *
- * @see form_validate_pattern()
+ * @deprecated Use \Drupal\Core\Render\Element\FormElement::processPattern().
*/
-function form_process_pattern($element, FormStateInterface $form_state) {
- if (isset($element['#pattern']) && !isset($element['#attributes']['pattern'])) {
- $element['#attributes']['pattern'] = $element['#pattern'];
- $element['#element_validate'][] = 'form_validate_pattern';
- }
-
- return $element;
+function form_process_pattern($element, FormStateInterface $form_state, &$complete_form) {
+ return Element\FormElement::processPattern($element, $form_state, $complete_form);
}
/**
@@ -1922,61 +1896,10 @@ function form_pre_render_details($element) {
/**
* Adds members of this group as actual elements for rendering.
*
- * @param $element
- * An associative array containing the properties and children of the
- * element.
- *
- * @return
- * The modified element with all group members.
+ * @deprecated Use \Drupal\Core\Render\ElementElementBase::preRenderGroup().
*/
function form_pre_render_group($element) {
- // The element may be rendered outside of a Form API context.
- if (!isset($element['#parents']) || !isset($element['#groups'])) {
- return $element;
- }
-
- // Inject group member elements belonging to this group.
- $parents = implode('][', $element['#parents']);
- $children = Element::children($element['#groups'][$parents]);
- if (!empty($children)) {
- foreach ($children as $key) {
- // Break references and indicate that the element should be rendered as
- // group member.
- $child = (array) $element['#groups'][$parents][$key];
- $child['#group_details'] = TRUE;
- // Inject the element as new child element.
- $element[] = $child;
-
- $sort = TRUE;
- }
- // Re-sort the element's children if we injected group member elements.
- if (isset($sort)) {
- $element['#sorted'] = FALSE;
- }
- }
-
- if (isset($element['#group'])) {
- // Contains form element summary functionalities.
- $element['#attached']['library'][] = 'core/drupal.form';
-
- $group = $element['#group'];
- // If this element belongs to a group, but the group-holding element does
- // not exist, we need to render it (at its original location).
- if (!isset($element['#groups'][$group]['#group_exists'])) {
- // Intentionally empty to clarify the flow; we simply return $element.
- }
- // If we injected this element into the group, then we want to render it.
- elseif (!empty($element['#group_details'])) {
- // Intentionally empty to clarify the flow; we simply return $element.
- }
- // Otherwise, this element belongs to a group and the group exists, so we do
- // not render it.
- elseif (Element::children($element['#groups'][$group])) {
- $element['#printed'] = TRUE;
- }
- }
-
- return $element;
+ return Element\RenderElement::preRenderGroup($element);
}
/**
@@ -2064,43 +1987,10 @@ function template_preprocess_vertical_tabs(&$variables) {
* Adds autocomplete functionality to elements with a valid
* #autocomplete_route_name.
*
- * Suppose your autocomplete route name is 'mymodule.autocomplete' and its path
- * is: '/mymodule/autocomplete/{a}/{b}'
- * In your form you have:
- * @code
- * '#autocomplete_route_name' => 'mymodule.autocomplete',
- * '#autocomplete_route_parameters' => array('a' => $some_key, 'b' => $some_id),
- * @endcode
- * The user types in "keywords" so the full path called is:
- * 'mymodule_autocomplete/$some_key/$some_id?q=keywords'
- *
- * @param array $element
- * The form element to process. Properties used:
- * - #autocomplete_route_name: A route to be used as callback URL by the
- * autocomplete JavaScript library.
- * - #autocomplete_route_parameters: The parameters to be used in conjunction
- * with the route name.
- * @param \Drupal\Core\Form\FormStateInterface $form_state
- * The current state of the form.
- *
- * @return array
- * The form element.
+ * @deprecated Use \Drupal\Core\Render\Element\FormElement::processAutocomplete().
*/
-function form_process_autocomplete($element, FormStateInterface $form_state) {
- $access = FALSE;
- if (!empty($element['#autocomplete_route_name'])) {
- $parameters = isset($element['#autocomplete_route_parameters']) ? $element['#autocomplete_route_parameters'] : array();
-
- $path = \Drupal::urlGenerator()->generate($element['#autocomplete_route_name'], $parameters);
- $access = \Drupal::service('access_manager')->checkNamedRoute($element['#autocomplete_route_name'], $parameters, \Drupal::currentUser());
- }
- if ($access) {
- $element['#attributes']['class'][] = 'form-autocomplete';
- $element['#attached']['library'][] = 'core/drupal.autocomplete';
- // Provide a data attribute for the JavaScript behavior to bind to.
- $element['#attributes']['data-autocomplete-path'] = $path;
- }
- return $element;
+function form_process_autocomplete($element, FormStateInterface $form_state, &$complete_form) {
+ return Element\FormElement::processAutocomplete($element, $form_state, $complete_form);
}
/**
@@ -2206,20 +2096,10 @@ function form_pre_render_hidden($element) {
/**
* Prepares a #type 'textfield' render element for theme_input().
*
- * @param array $element
- * An associative array containing the properties of the element.
- * Properties used: #title, #value, #description, #size, #maxlength,
- * #placeholder, #required, #attributes.
- *
- * @return array
- * The $element with prepared variables ready for theme_input().
+ * @deprecated Use \Drupal\Core\Render\Element\Textfield::preRenderTextfield().
*/
function form_pre_render_textfield($element) {
- $element['#attributes']['type'] = 'text';
- Element::setAttributes($element, array('id', 'name', 'value', 'size', 'maxlength', 'placeholder'));
- _form_set_attributes($element, array('form-text'));
-
- return $element;
+ return Element\Textfield::preRenderTextfield($element);
}
/**
diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php
index adbb8f33568..d4170a41281 100644
--- a/core/lib/Drupal/Core/Form/FormBuilder.php
+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -938,7 +938,14 @@ class FormBuilder implements FormBuilderInterface, FormValidatorInterface, FormS
// Set the element's #value property.
if (!isset($element['#value']) && !array_key_exists('#value', $element)) {
+ // @todo Once all elements are converted to plugins in
+ // https://www.drupal.org/node/2311393, rely on
+ // $element['#value_callback'] directly.
$value_callable = !empty($element['#value_callback']) ? $element['#value_callback'] : 'form_type_' . $element['#type'] . '_value';
+ if (!is_callable($value_callable)) {
+ $value_callable = '\Drupal\Core\Render\Element\FormElement::valueCallback';
+ }
+
if ($process_input) {
// Get the input for the current element. NULL values in the input need
// to be explicitly distinguished from missing input. (see below)
@@ -962,9 +969,8 @@ class FormBuilder implements FormBuilderInterface, FormValidatorInterface, FormS
// If we have input for the current element, assign it to the #value
// property, optionally filtered through $value_callback.
if ($input_exists) {
- if (is_callable($value_callable)) {
- $element['#value'] = call_user_func_array($value_callable, array(&$element, $input, &$form_state));
- }
+ $element['#value'] = call_user_func_array($value_callable, array(&$element, $input, &$form_state));
+
if (!isset($element['#value']) && isset($input)) {
$element['#value'] = $input;
}
@@ -978,9 +984,8 @@ class FormBuilder implements FormBuilderInterface, FormValidatorInterface, FormS
if (!isset($element['#value'])) {
// Call #type_value without a second argument to request default_value
// handling.
- if (is_callable($value_callable)) {
- $element['#value'] = call_user_func_array($value_callable, array(&$element, FALSE, &$form_state));
- }
+ $element['#value'] = call_user_func_array($value_callable, array(&$element, FALSE, &$form_state));
+
// Final catch. If we haven't set a value yet, use the explicit default
// value. Avoid image buttons (which come with garbage value), so we
// only get value for the button actually clicked.
diff --git a/core/lib/Drupal/Core/Render/Annotation/FormElement.php b/core/lib/Drupal/Core/Render/Annotation/FormElement.php
new file mode 100644
index 00000000000..4d4b59339c7
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/Annotation/FormElement.php
@@ -0,0 +1,32 @@
+ 'attributename'). If both names are identical
- * except for the leading '#', then an attribute name value is sufficient and
- * no property name needs to be specified.
+ * An associative array whose keys are element property names and whose
+ * values are the HTML attribute names to set on the corresponding
+ * property; e.g., array('#propertyname' => 'attributename'). If both names
+ * are identical except for the leading '#', then an attribute name value is
+ * sufficient and no property name needs to be specified.
*/
public static function setAttributes(array &$element, array $map) {
foreach ($map as $property => $attribute) {
diff --git a/core/lib/Drupal/Core/Render/Element/ElementInterface.php b/core/lib/Drupal/Core/Render/Element/ElementInterface.php
new file mode 100644
index 00000000000..a1035f7b2b7
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/Element/ElementInterface.php
@@ -0,0 +1,45 @@
+ 'mymodule.autocomplete',
+ * '#autocomplete_route_parameters' => array('a' => $some_key, 'b' => $some_id),
+ * @endcode
+ * If the user types "keywords" in that field, the full path called would be:
+ * 'mymodule_autocomplete/$some_key/$some_id?q=keywords'
+ *
+ * @param array $element
+ * The form element to process. Properties used:
+ * - #autocomplete_route_name: A route to be used as callback URL by the
+ * autocomplete JavaScript library.
+ * - #autocomplete_route_parameters: The parameters to be used in
+ * conjunction with the route name.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ * @param array $complete_form
+ * The complete form structure.
+ *
+ * @return array
+ * The form element.
+ */
+ public static function processAutocomplete(&$element, FormStateInterface $form_state, &$complete_form) {
+ $access = FALSE;
+ if (!empty($element['#autocomplete_route_name'])) {
+ $parameters = isset($element['#autocomplete_route_parameters']) ? $element['#autocomplete_route_parameters'] : array();
+
+ $path = \Drupal::urlGenerator()->generate($element['#autocomplete_route_name'], $parameters);
+ $access = \Drupal::service('access_manager')->checkNamedRoute($element['#autocomplete_route_name'], $parameters, \Drupal::currentUser());
+ }
+ if ($access) {
+ $element['#attributes']['class'][] = 'form-autocomplete';
+ $element['#attached']['library'][] = 'core/drupal.autocomplete';
+ // Provide a data attribute for the JavaScript behavior to bind to.
+ $element['#attributes']['data-autocomplete-path'] = $path;
+ }
+
+ return $element;
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Render/Element/FormElementInterface.php b/core/lib/Drupal/Core/Render/Element/FormElementInterface.php
new file mode 100644
index 00000000000..2bbbde8ecbd
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/Element/FormElementInterface.php
@@ -0,0 +1,46 @@
+ array(
+ array($class, 'preRenderLink'),
+ ),
+ );
+ }
+
+ /**
+ * Pre-render callback: Renders a link into #markup.
+ *
+ * Doing so during pre_render gives modules a chance to alter the link parts.
+ *
+ * @param array $element
+ * A structured array whose keys form the arguments to l():
+ * - #title: The link text to pass as argument to l().
+ * - One of the following
+ * - #route_name and (optionally) a #route_parameters array; The route
+ * name and route parameters which will be passed into the link
+ * generator.
+ * - #href: The system path or URL to pass as argument to l().
+ * - #options: (optional) An array of options to pass to l() or the link
+ * generator.
+ *
+ * @return array
+ * The passed-in element containing a rendered link in '#markup'.
+ */
+ public static function preRenderLink($element) {
+ // By default, link options to pass to l() are normally set in #options.
+ $element += array('#options' => array());
+ // However, within the scope of renderable elements, #attributes is a valid
+ // way to specify attributes, too. Take them into account, but do not override
+ // attributes from #options.
+ if (isset($element['#attributes'])) {
+ $element['#options'] += array('attributes' => array());
+ $element['#options']['attributes'] += $element['#attributes'];
+ }
+
+ // This #pre_render callback can be invoked from inside or outside of a Form
+ // API context, and depending on that, a HTML ID may be already set in
+ // different locations. #options should have precedence over Form API's #id.
+ // #attributes have been taken over into #options above already.
+ if (isset($element['#options']['attributes']['id'])) {
+ $element['#id'] = $element['#options']['attributes']['id'];
+ }
+ elseif (isset($element['#id'])) {
+ $element['#options']['attributes']['id'] = $element['#id'];
+ }
+
+ // Conditionally invoke ajax_pre_render_element(), if #ajax is set.
+ if (isset($element['#ajax']) && !isset($element['#ajax_processed'])) {
+ // If no HTML ID was found above, automatically create one.
+ if (!isset($element['#id'])) {
+ $element['#id'] = $element['#options']['attributes']['id'] = drupal_html_id('ajax-link');
+ }
+ // If #ajax['path] was not specified, use the href as Ajax request URL.
+ if (!isset($element['#ajax']['path'])) {
+ $element['#ajax']['path'] = $element['#href'];
+ $element['#ajax']['options'] = $element['#options'];
+ }
+ $element = ajax_pre_render_element($element);
+ }
+
+ if (isset($element['#route_name'])) {
+ $element['#route_parameters'] = empty($element['#route_parameters']) ? array() : $element['#route_parameters'];
+ $element['#markup'] = \Drupal::linkGenerator()->generate($element['#title'], $element['#route_name'], $element['#route_parameters'], $element['#options']);
+ }
+ else {
+ $element['#markup'] = l($element['#title'], $element['#href'], $element['#options']);
+ }
+ return $element;
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Render/Element/MachineName.php b/core/lib/Drupal/Core/Render/Element/MachineName.php
new file mode 100644
index 00000000000..6301f3527bb
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/Element/MachineName.php
@@ -0,0 +1,226 @@
+ TRUE,
+ '#default_value' => NULL,
+ '#required' => TRUE,
+ '#maxlength' => 64,
+ '#size' => 60,
+ '#autocomplete_route_name' => FALSE,
+ '#process' => array(
+ array($class, 'processMachineName'),
+ array($class, 'processAutocomplete'),
+ array($class, 'processAjaxForm'),
+ ),
+ '#element_validate' => array(
+ array($class, 'validateMachineName'),
+ ),
+ '#pre_render' => array(
+ array($class, 'preRenderTextfield'),
+ ),
+ '#theme' => 'input__textfield',
+ '#theme_wrappers' => array('form_element'),
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
+ return NULL;
+ }
+
+ /**
+ * Processes a machine-readable name form element.
+ *
+ * @param array $element
+ * The form element to process. Properties used:
+ * - #machine_name: An associative array containing:
+ * - exists: A callable to invoke for checking whether a submitted machine
+ * name value already exists. The submitted value is passed as an
+ * argument. In most cases, an existing API or menu argument loader
+ * function can be re-used. The callback is only invoked if the
+ * submitted value differs from the element's #default_value.
+ * - source: (optional) The #array_parents of the form element containing
+ * the human-readable name (i.e., as contained in the $form structure)
+ * to use as source for the machine name. Defaults to array('label').
+ * - label: (optional) Text to display as label for the machine name value
+ * after the human-readable name form element. Defaults to "Machine
+ * name".
+ * - replace_pattern: (optional) A regular expression (without delimiters)
+ * matching disallowed characters in the machine name. Defaults to
+ * '[^a-z0-9_]+'.
+ * - replace: (optional) A character to replace disallowed characters in
+ * the machine name via JavaScript. Defaults to '_' (underscore). When
+ * using a different character, 'replace_pattern' needs to be set
+ * accordingly.
+ * - error: (optional) A custom form error message string to show, if the
+ * machine name contains disallowed characters.
+ * - standalone: (optional) Whether the live preview should stay in its
+ * own form element rather than in the suffix of the source
+ * element. Defaults to FALSE.
+ * - #maxlength: (optional) Maximum allowed length of the machine name.
+ * Defaults to 64.
+ * - #disabled: (optional) Should be set to TRUE if an existing machine
+ * name must not be changed after initial creation.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ * @param array $complete_form
+ * The complete form structure.
+ *
+ * @return array
+ * The processed element.
+ */
+ public static function processMachineName(&$element, FormStateInterface $form_state, &$complete_form) {
+ // We need to pass the langcode to the client.
+ $language = \Drupal::languageManager()->getCurrentLanguage();
+
+ // Apply default form element properties.
+ $element += array(
+ '#title' => t('Machine-readable name'),
+ '#description' => t('A unique machine-readable name. Can only contain lowercase letters, numbers, and underscores.'),
+ '#machine_name' => array(),
+ '#field_prefix' => '',
+ '#field_suffix' => '',
+ '#suffix' => '',
+ );
+ // A form element that only wants to set one #machine_name property (usually
+ // 'source' only) would leave all other properties undefined, if the defaults
+ // were defined in hook_element_info(). Therefore, we apply the defaults here.
+ $element['#machine_name'] += array(
+ 'source' => array('label'),
+ 'target' => '#' . $element['#id'],
+ 'label' => t('Machine name'),
+ 'replace_pattern' => '[^a-z0-9_]+',
+ 'replace' => '_',
+ 'standalone' => FALSE,
+ 'field_prefix' => $element['#field_prefix'],
+ 'field_suffix' => $element['#field_suffix'],
+ );
+
+ // By default, machine names are restricted to Latin alphanumeric characters.
+ // So, default to LTR directionality.
+ if (!isset($element['#attributes'])) {
+ $element['#attributes'] = array();
+ }
+ $element['#attributes'] += array('dir' => 'ltr');
+
+ // The source element defaults to array('name'), but may have been overidden.
+ if (empty($element['#machine_name']['source'])) {
+ return $element;
+ }
+
+ // Retrieve the form element containing the human-readable name from the
+ // complete form in $form_state. By reference, because we may need to append
+ // a #field_suffix that will hold the live preview.
+ $key_exists = NULL;
+ $source = NestedArray::getValue($form_state['complete_form'], $element['#machine_name']['source'], $key_exists);
+ if (!$key_exists) {
+ return $element;
+ }
+
+ $suffix_id = $source['#id'] . '-machine-name-suffix';
+ $element['#machine_name']['suffix'] = '#' . $suffix_id;
+
+ if ($element['#machine_name']['standalone']) {
+ $element['#suffix'] = SafeMarkup::set($element['#suffix'] . ' ');
+ }
+ else {
+ // Append a field suffix to the source form element, which will contain
+ // the live preview of the machine name.
+ $source += array('#field_suffix' => '');
+ $source['#field_suffix'] = SafeMarkup::set($source['#field_suffix'] . ' ');
+
+ $parents = array_merge($element['#machine_name']['source'], array('#field_suffix'));
+ NestedArray::setValue($form_state['complete_form'], $parents, $source['#field_suffix']);
+ }
+
+ $js_settings = array(
+ 'type' => 'setting',
+ 'data' => array(
+ 'machineName' => array(
+ '#' . $source['#id'] => $element['#machine_name'],
+ ),
+ 'langcode' => $language->id,
+ ),
+ );
+ $element['#attached']['library'][] = 'core/drupal.machine-name';
+ $element['#attached']['js'][] = $js_settings;
+
+ return $element;
+ }
+
+ /**
+ * Form element validation handler for machine_name elements.
+ *
+ * Note that #maxlength is validated by _form_validate() already.
+ *
+ * This checks that the submitted value:
+ * - Does not contain the replacement character only.
+ * - Does not contain disallowed characters.
+ * - Is unique; i.e., does not already exist.
+ * - Does not exceed the maximum length (via #maxlength).
+ * - Cannot be changed after creation (via #disabled).
+ */
+ public static function validateMachineName(&$element, FormStateInterface $form_state, &$complete_form) {
+ // Verify that the machine name not only consists of replacement tokens.
+ if (preg_match('@^' . $element['#machine_name']['replace'] . '+$@', $element['#value'])) {
+ form_error($element, $form_state, t('The machine-readable name must contain unique characters.'));
+ }
+
+ // Verify that the machine name contains no disallowed characters.
+ if (preg_match('@' . $element['#machine_name']['replace_pattern'] . '@', $element['#value'])) {
+ if (!isset($element['#machine_name']['error'])) {
+ // Since a hyphen is the most common alternative replacement character,
+ // a corresponding validation error message is supported here.
+ if ($element['#machine_name']['replace'] == '-') {
+ form_error($element, $form_state, t('The machine-readable name must contain only lowercase letters, numbers, and hyphens.'));
+ }
+ // Otherwise, we assume the default (underscore).
+ else {
+ form_error($element, $form_state, t('The machine-readable name must contain only lowercase letters, numbers, and underscores.'));
+ }
+ }
+ else {
+ form_error($element, $form_state, $element['#machine_name']['error']);
+ }
+ }
+
+ // Verify that the machine name is unique.
+ if ($element['#default_value'] !== $element['#value']) {
+ $function = $element['#machine_name']['exists'];
+ if (call_user_func($function, $element['#value'], $element, $form_state)) {
+ form_error($element, $form_state, t('The machine-readable name is already in use. It must be unique.'));
+ }
+ }
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Render/Element/RenderElement.php b/core/lib/Drupal/Core/Render/Element/RenderElement.php
new file mode 100644
index 00000000000..cf4d4546dd6
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/Element/RenderElement.php
@@ -0,0 +1,85 @@
+ TRUE,
+ '#size' => 60,
+ '#maxlength' => 128,
+ '#autocomplete_route_name' => FALSE,
+ '#process' => array(
+ array($class, 'processAutocomplete'),
+ array($class, 'processAjaxForm'),
+ array($class, 'processPattern'),
+ array($class, 'processGroup'),
+ ),
+ '#pre_render' => array(
+ array($class, 'preRenderTextfield'),
+ array($class, 'preRenderGroup'),
+ ),
+ '#theme' => 'input__textfield',
+ '#theme_wrappers' => array('form_element'),
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
+ if ($input !== FALSE && $input !== NULL) {
+ // Equate $input to the form value to ensure it's marked for
+ // validation.
+ return str_replace(array("\r", "\n"), '', $input);
+ }
+ }
+
+ /**
+ * Prepares a #type 'textfield' render element for theme_input().
+ *
+ * @param array $element
+ * An associative array containing the properties of the element.
+ * Properties used: #title, #value, #description, #size, #maxlength,
+ * #placeholder, #required, #attributes.
+ *
+ * @return array
+ * The $element with prepared variables ready for theme_input().
+ */
+ public static function preRenderTextfield($element) {
+ $element['#attributes']['type'] = 'text';
+ Element::setAttributes($element, array('id', 'name', 'value', 'size', 'maxlength', 'placeholder'));
+ _form_set_attributes($element, array('form-text'));
+
+ return $element;
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Render/ElementInfo.php b/core/lib/Drupal/Core/Render/ElementInfo.php
deleted file mode 100644
index 8d6a3f283c0..00000000000
--- a/core/lib/Drupal/Core/Render/ElementInfo.php
+++ /dev/null
@@ -1,65 +0,0 @@
-moduleHandler = $module_handler;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getInfo($type) {
- if (!isset($this->elementInfo)) {
- $this->elementInfo = $this->buildInfo();
- }
- return isset($this->elementInfo[$type]) ? $this->elementInfo[$type] : array();
- }
-
- /**
- * Builds up all element information.
- */
- protected function buildInfo() {
- $info = $this->moduleHandler->invokeAll('element_info');
- foreach ($info as $element_type => $element) {
- $info[$element_type]['#type'] = $element_type;
- }
- // Allow modules to alter the element type defaults.
- $this->moduleHandler->alter('element_info', $info);
-
- return $info;
- }
-
-}
diff --git a/core/lib/Drupal/Core/Render/ElementInfoInterface.php b/core/lib/Drupal/Core/Render/ElementInfoInterface.php
deleted file mode 100644
index ad05aebdfb3..00000000000
--- a/core/lib/Drupal/Core/Render/ElementInfoInterface.php
+++ /dev/null
@@ -1,51 +0,0 @@
-setCacheBackend($cache_backend, 'element_info');
+
+ parent::__construct('Element', $namespaces, $module_handler, 'Drupal\Core\Render\Annotation\RenderElement');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getInfo($type) {
+ if (!isset($this->elementInfo)) {
+ $this->elementInfo = $this->buildInfo();
+ }
+ return isset($this->elementInfo[$type]) ? $this->elementInfo[$type] : array();
+ }
+
+ /**
+ * Builds up all element information.
+ */
+ protected function buildInfo() {
+ // @todo Remove this hook once all elements are converted to plugins in
+ // https://www.drupal.org/node/2311393.
+ $info = $this->moduleHandler->invokeAll('element_info');
+
+ foreach ($this->getDefinitions() as $element_type => $definition) {
+ $element = $this->createInstance($element_type);
+ $element_info = $element->getInfo();
+
+ // If this is element is to be used exclusively in a form, denote that it
+ // will receive input, and assign the value callback.
+ if ($element instanceof FormElementInterface) {
+ $element_info['#input'] = TRUE;
+ $element_info['#value_callback'] = array($definition['class'], 'valueCallback');
+ }
+ $info[$element_type] = $element_info;
+ }
+ foreach ($info as $element_type => $element) {
+ $info[$element_type]['#type'] = $element_type;
+ }
+ // Allow modules to alter the element type defaults.
+ $this->moduleHandler->alter('element_info', $info);
+
+ return $info;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return \Drupal\Core\Render\Element\ElementInterface
+ */
+ public function createInstance($plugin_id, array $configuration = array()) {
+ return parent::createInstance($plugin_id, $configuration);
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Render/ElementInfoManagerInterface.php b/core/lib/Drupal/Core/Render/ElementInfoManagerInterface.php
new file mode 100644
index 00000000000..d32acd690f9
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/ElementInfoManagerInterface.php
@@ -0,0 +1,55 @@
+ array('form_pre_render_image_button'),
'#theme_wrappers' => array('input__image_button'),
);
- $types['textfield'] = array(
- '#input' => TRUE,
- '#size' => 60,
- '#maxlength' => 128,
- '#autocomplete_route_name' => FALSE,
- '#process' => array('form_process_autocomplete', 'ajax_process_form', 'form_process_pattern', 'form_process_group'),
- '#pre_render' => array('form_pre_render_textfield', 'form_pre_render_group'),
- '#theme' => 'input__textfield',
- '#theme_wrappers' => array('form_element'),
- );
$types['tel'] = array(
'#input' => TRUE,
'#size' => 30,
@@ -448,19 +438,6 @@ function system_element_info() {
'#theme' => 'input__color',
'#theme_wrappers' => array('form_element'),
);
- $types['machine_name'] = array(
- '#input' => TRUE,
- '#default_value' => NULL,
- '#required' => TRUE,
- '#maxlength' => 64,
- '#size' => 60,
- '#autocomplete_route_name' => FALSE,
- '#process' => array('form_process_machine_name', 'form_process_autocomplete', 'ajax_process_form'),
- '#element_validate' => array('form_validate_machine_name'),
- '#pre_render' => array('form_pre_render_textfield'),
- '#theme' => 'input__textfield',
- '#theme_wrappers' => array('form_element'),
- );
$types['password'] = array(
'#input' => TRUE,
'#size' => 60,
@@ -586,9 +563,6 @@ function system_element_info() {
$types['value'] = array(
'#input' => TRUE,
);
- $types['link'] = array(
- '#pre_render' => array('drupal_pre_render_link'),
- );
$types['fieldset'] = array(
'#value' => NULL,
'#process' => array('form_process_group', 'ajax_process_form'),
diff --git a/core/modules/system/theme.api.php b/core/modules/system/theme.api.php
index 85eb7283a7a..fabde88462a 100644
--- a/core/modules/system/theme.api.php
+++ b/core/modules/system/theme.api.php
@@ -163,11 +163,21 @@
* requests and the CSS files used to style that markup. In order to ensure that
* a theme can completely customize the markup, module developers should avoid
* directly writing HTML markup for pages, blocks, and other user-visible output
- * in their modules, and instead return structured "render arrays" (described
- * below). Doing this also increases usability, by ensuring that the markup used
- * for similar functionality on different areas of the site is the same, which
- * gives users fewer user interface patterns to learn.
+ * in their modules, and instead return structured "render arrays" (see @ref
+ * arrays below). Doing this also increases usability, by ensuring that the
+ * markup used for similar functionality on different areas of the site is the
+ * same, which gives users fewer user interface patterns to learn.
*
+ * For further information on the Theme and Render APIs, see:
+ * - https://drupal.org/documentation/theme
+ * - https://drupal.org/node/722174
+ * - https://drupal.org/node/933976
+ * - https://drupal.org/node/930760
+ *
+ * @todo Check these links. Some are for Drupal 7, and might need updates for
+ * Drupal 8.
+ *
+ * @section arrays Render arrays
* The core structure of the Render API is the render array, which is a
* hierarchical associative array containing data to be rendered and properties
* describing how the data should be rendered. A render array that is returned
@@ -194,11 +204,8 @@
* - #type: Specifies that the array contains data and options for a particular
* type of "render element" (examples: 'form', for an HTML form; 'textfield',
* 'submit', and other HTML form element types; 'table', for a table with
- * rows, columns, and headers). Modules define render elements by implementing
- * hook_element_info(), which specifies the properties that are used in render
- * arrays to provide the data and options, and default values for these
- * properties. Look through implementations of hook_element_info() to discover
- * what render elements are available.
+ * rows, columns, and headers). See @ref elements below for more on render
+ * element types.
* - #theme: Specifies that the array contains data to be themed by a particular
* theme hook. Modules define theme hooks by implementing hook_theme(), which
* specifies the input "variables" used to provide data and options; if a
@@ -214,15 +221,29 @@
* normally preferable to use #theme or #type instead, so that the theme can
* customize the markup.
*
- * For further information on the Theme and Render APIs, see:
- * - https://drupal.org/documentation/theme
- * - https://drupal.org/developing/modules/8
- * - https://drupal.org/node/722174
- * - https://drupal.org/node/933976
- * - https://drupal.org/node/930760
+ * @section elements Render elements
+ * Render elements are defined by Drupal core and modules. The primary way to
+ * define a render element is to create a render element plugin. There are
+ * two types of render element plugins:
+ * - Generic elements: Generic render element plugins implement
+ * \Drupal\Core\Render\Element\ElementInterface, are annotated with
+ * \Drupal\Core\Render\Annotation\RenderElement annotation, go in plugin
+ * namespace Element, and generally extend the
+ * \Drupal\Core\Render\Element\RenderElement base class.
+ * - Form input elements: Render elements representing form input elements
+ * implement \Drupal\Core\Render\Element\FormElementInterface, are annotated
+ * with \Drupal\Core\Render\Annotation\FormElement annotation, go in plugin
+ * namespace Element, and generally extend the
+ * \Drupal\Core\Render\Element\FormElement base class.
+ * See the @link plugin_api Plugin API topic @endlink for general information
+ * on plugins, and look for classes with the RenderElement or FormElement
+ * annotation to discover what render elements are available.
+ *
+ * Modules can also currently define render elements by implementing
+ * hook_element_info(), although defining a plugin is preferred.
+ * properties. Look through implementations of hook_element_info() to discover
+ * elements defined this way.
*
- * @todo Check these links. Some are for Drupal 7, and might need updates for
- * Drupal 8.
* @see themeable
*
* @}
diff --git a/core/tests/Drupal/Tests/Core/Render/ElementInfoManagerTest.php b/core/tests/Drupal/Tests/Core/Render/ElementInfoManagerTest.php
new file mode 100644
index 00000000000..fa9cbb3a23f
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Render/ElementInfoManagerTest.php
@@ -0,0 +1,197 @@
+cache = $this->getMock('Drupal\Core\Cache\CacheBackendInterface');
+ $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
+
+ $this->elementInfo = new ElementInfoManager(new \ArrayObject(), $this->cache, $this->moduleHandler);
+ }
+
+ /**
+ * Tests the getInfo method.
+ *
+ * @covers ::getInfo
+ * @covers ::buildInfo
+ *
+ * @dataProvider providerTestGetInfo
+ */
+ public function testGetInfo($type, $expected_info, $element_info, callable $alter_callback = NULL) {
+ $this->moduleHandler->expects($this->once())
+ ->method('invokeAll')
+ ->with('element_info')
+ ->will($this->returnValue($element_info));
+ $this->moduleHandler->expects($this->once())
+ ->method('alter')
+ ->with('element_info', $this->anything())
+ ->will($this->returnCallback($alter_callback ?: function($info) {
+ return $info;
+ }));
+
+ $this->assertEquals($expected_info, $this->elementInfo->getInfo($type));
+ }
+
+ /**
+ * Provides tests data for getInfo.
+ *
+ * @return array
+ */
+ public function providerTestGetInfo() {
+ $data = array();
+ // Provide an element and expect it is returned.
+ $data[] = array(
+ 'page',
+ array(
+ '#type' => 'page',
+ '#show_messages' => TRUE,
+ '#theme' => 'page',
+ ),
+ array('page' => array(
+ '#show_messages' => TRUE,
+ '#theme' => 'page',
+ )),
+ );
+ // Provide an element but request an non existent one.
+ $data[] = array(
+ 'form',
+ array(
+ ),
+ array('page' => array(
+ '#show_messages' => TRUE,
+ '#theme' => 'page',
+ )),
+ );
+ // Provide an element and alter it to ensure it is altered.
+ $data[] = array(
+ 'page',
+ array(
+ '#type' => 'page',
+ '#show_messages' => TRUE,
+ '#theme' => 'page',
+ '#number' => 597219,
+ ),
+ array('page' => array(
+ '#show_messages' => TRUE,
+ '#theme' => 'page',
+ )),
+ function ($alter_name, array &$info) {
+ $info['page']['#number'] = 597219;
+ }
+ );
+ return $data;
+ }
+
+ /**
+ * Tests the getInfo() method when render element plugins are used.
+ *
+ * @covers ::getInfo
+ * @covers ::buildInfo
+ *
+ * @dataProvider providerTestGetInfoElementPlugin
+ */
+ public function testGetInfoElementPlugin($plugin_class, $expected_info) {
+ $this->moduleHandler->expects($this->once())
+ ->method('invokeAll')
+ ->with('element_info')
+ ->willReturn(array());
+ $this->moduleHandler->expects($this->once())
+ ->method('alter')
+ ->with('element_info', $this->anything())
+ ->will($this->returnArgument(0));
+
+ $plugin = $this->getMock($plugin_class);
+ $plugin->expects($this->once())
+ ->method('getInfo')
+ ->willReturn(array(
+ '#show_messages' => TRUE,
+ '#theme' => 'page',
+ ));
+
+ $element_info = $this->getMockBuilder('Drupal\Core\Render\ElementInfoManager')
+ ->setConstructorArgs(array(new \ArrayObject(), $this->cache, $this->moduleHandler))
+ ->setMethods(array('getDefinitions', 'createInstance'))
+ ->getMock();
+ $element_info->expects($this->once())
+ ->method('createInstance')
+ ->with('page')
+ ->willReturn($plugin);
+ $element_info->expects($this->once())
+ ->method('getDefinitions')
+ ->willReturn(array(
+ 'page' => array('class' => 'TestElementPlugin'),
+ ));
+
+ $this->assertEquals($expected_info, $element_info->getInfo('page'));
+ }
+
+ /**
+ * Provides tests data for testGetInfoElementPlugin().
+ *
+ * @return array
+ */
+ public function providerTestGetInfoElementPlugin() {
+ $data = array();
+ $data[] = array(
+ 'Drupal\Core\Render\Element\ElementInterface',
+ array(
+ '#type' => 'page',
+ '#show_messages' => TRUE,
+ '#theme' => 'page',
+ ),
+ );
+
+ $data[] = array(
+ 'Drupal\Core\Render\Element\FormElementInterface',
+ array(
+ '#type' => 'page',
+ '#show_messages' => TRUE,
+ '#theme' => 'page',
+ '#input' => TRUE,
+ '#value_callback' => array('TestElementPlugin', 'valueCallback'),
+ ),
+ );
+ return $data;
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Render/ElementInfoTest.php b/core/tests/Drupal/Tests/Core/Render/ElementInfoTest.php
deleted file mode 100644
index 86878f4322a..00000000000
--- a/core/tests/Drupal/Tests/Core/Render/ElementInfoTest.php
+++ /dev/null
@@ -1,118 +0,0 @@
-moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
-
- $this->elementInfo = new ElementInfo($this->moduleHandler);
- }
-
- /**
- * Tests the getInfo method.
- *
- * @covers ::getInfo
- * @covers ::buildInfo
- *
- * @dataProvider providerTestGetInfo
- */
- public function testGetInfo($type, $expected_info, $element_info, callable $alter_callback = NULL) {
- $this->moduleHandler->expects($this->once())
- ->method('invokeAll')
- ->with('element_info')
- ->will($this->returnValue($element_info));
- $this->moduleHandler->expects($this->once())
- ->method('alter')
- ->with('element_info', $this->anything())
- ->will($this->returnCallback($alter_callback ?: function($info) {
- return $info;
- }));
-
- $this->assertEquals($expected_info, $this->elementInfo->getInfo($type));
- }
-
- /**
- * Provides tests data for getInfo.
- *
- * @return array
- */
- public function providerTestGetInfo() {
- $data = array();
- // Provide an element and expect it is returned.
- $data[] = array(
- 'page',
- array(
- '#type' => 'page',
- '#show_messages' => TRUE,
- '#theme' => 'page',
- ),
- array('page' => array(
- '#show_messages' => TRUE,
- '#theme' => 'page',
- )),
- );
- // Provide an element but request an non existent one.
- $data[] = array(
- 'form',
- array(
- ),
- array('page' => array(
- '#show_messages' => TRUE,
- '#theme' => 'page',
- )),
- );
- // Provide an element and alter it to ensure it is altered.
- $data[] = array(
- 'page',
- array(
- '#type' => 'page',
- '#show_messages' => TRUE,
- '#theme' => 'page',
- '#number' => 597219,
- ),
- array('page' => array(
- '#show_messages' => TRUE,
- '#theme' => 'page',
- )),
- function ($alter_name, array &$info) {
- $info['page']['#number'] = 597219;
- }
- );
- return $data;
- }
-
-}