From 2a5c62deda015ed43c5a13748d8aca6b60ff31e1 Mon Sep 17 00:00:00 2001 From: catch <catch@35733.no-reply.drupal.org> Date: Mon, 7 Oct 2019 14:56:30 +0100 Subject: [PATCH] Issue #3000068 by andypost, claudiu.cristea, pingwin4eg, Mile23, voleger, johndevman: Deprecate drupal_process_states() --- core/includes/common.inc | 17 +- core/lib/Drupal/Core/Form/FormHelper.php | 150 +++++++++++++++++- .../Core/Render/Element/FormElement.php | 2 +- core/lib/Drupal/Core/Render/Renderer.php | 3 +- .../Drupal/Core/Render/RendererInterface.php | 4 +- .../Core/Render/RendererLegacyTest.php | 44 +++++ .../Drupal/Tests/Core/Form/FormHelperTest.php | 42 +++++ 7 files changed, 245 insertions(+), 17 deletions(-) diff --git a/core/includes/common.inc b/core/includes/common.inc index c2f74d9a8385..cc9677e750ab 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -9,13 +9,13 @@ */ use Drupal\Component\Gettext\PoItem; -use Drupal\Component\Serialization\Json; use Drupal\Component\Utility\Bytes; use Drupal\Component\Utility\Environment; use Drupal\Component\Utility\Html; use Drupal\Component\Utility\SortArray; use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Cache\Cache; +use Drupal\Core\Form\FormHelper; use Drupal\Core\Render\Element; use Drupal\Core\Render\Element\Link; use Drupal\Core\Render\HtmlResponseAttachmentsProcessor; @@ -588,16 +588,15 @@ function drupal_js_defaults($data = NULL) { * @param $elements * A renderable array element having a #states property as described above. * - * @see form_example_states_form() + * @deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use + * \Drupal\Core\Form\FormHelper::processStates() instead. + * + * @see https://www.drupal.org/node/3000069 + * @see \Drupal\Core\Form\FormHelper::processStates() */ function drupal_process_states(&$elements) { - $elements['#attached']['library'][] = 'core/drupal.states'; - // Elements of '#type' => 'item' are not actual form input elements, but we - // still want to be able to show/hide them. Since there's no actual HTML input - // element available, setting #attributes does not make sense, but a wrapper - // is available, so setting #wrapper_attributes makes it work. - $key = ($elements['#type'] == 'item') ? '#wrapper_attributes' : '#attributes'; - $elements[$key]['data-drupal-states'] = Json::encode($elements['#states']); + @trigger_error('drupal_process_states() is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use \Drupal\Core\Form\FormHelper::processStates() instead. See https://www.drupal.org/node/3000069', E_USER_DEPRECATED); + FormHelper::processStates($elements); } /** diff --git a/core/lib/Drupal/Core/Form/FormHelper.php b/core/lib/Drupal/Core/Form/FormHelper.php index 0ed3ade52ac7..ffced0468642 100644 --- a/core/lib/Drupal/Core/Form/FormHelper.php +++ b/core/lib/Drupal/Core/Form/FormHelper.php @@ -2,6 +2,7 @@ namespace Drupal\Core\Form; +use Drupal\Component\Serialization\Json; use Drupal\Core\Render\Element; /** @@ -12,16 +13,20 @@ class FormHelper { /** - * Rewrite #states selectors. + * Rewrites #states selectors in a render element. + * + * When a structure of elements is being altered, their HTML selectors may + * change. In such cases calling this method will check if there are any + * states in element and its children, and rewrite selectors in those states. * * @param array $elements - * A renderable array element having a #states property. + * A render array element having a #states property. * @param string $search * A partial or entire jQuery selector string to replace in #states. * @param string $replace * The string to replace all instances of $search with. * - * @see drupal_process_states() + * @see self::processStates() */ public static function rewriteStatesSelector(array &$elements, $search, $replace) { if (!empty($elements['#states'])) { @@ -35,7 +40,10 @@ public static function rewriteStatesSelector(array &$elements, $search, $replace } /** - * Helper function for self::rewriteStatesSelector(). + * Helps recursively rewrite #states selectors. + * + * Not to be confused with self::processStates(), which just prepares states + * for rendering. * * @param array $conditions * States conditions array. @@ -43,6 +51,8 @@ public static function rewriteStatesSelector(array &$elements, $search, $replace * A partial or entire jQuery selector string to replace in #states. * @param string $replace * The string to replace all instances of $search with. + * + * @see self::rewriteStatesSelector() */ protected static function processStatesArray(array &$conditions, $search, $replace) { // Retrieve the keys to make it easy to rename a key without changing the @@ -67,4 +77,136 @@ protected static function processStatesArray(array &$conditions, $search, $repla } } + /** + * Adds JavaScript to change the state of an element based on another element. + * + * A "state" means a certain property of a DOM element, such as "visible" or + * "checked", which depends on a state or value of another element on the + * page. In general, states are HTML attributes and DOM element properties, + * which are applied initially, when page is loaded, depending on elements' + * default values, and then may change due to user interaction. + * + * Since states are driven by JavaScript only, it is important to understand + * that all states are applied on presentation only, none of the states force + * any server-side logic, and that they will not be applied for site visitors + * without JavaScript support. All modules implementing states have to make + * sure that the intended logic also works without JavaScript being enabled. + * + * #states is an associative array in the form of: + * @code + * [ + * STATE1 => CONDITIONS_ARRAY1, + * STATE2 => CONDITIONS_ARRAY2, + * ... + * ] + * @endcode + * Each key is the name of a state to apply to the element, such as 'visible'. + * Each value is a list of conditions that denote when the state should be + * applied. + * + * Multiple different states may be specified to act on complex conditions: + * @code + * [ + * 'visible' => CONDITIONS, + * 'checked' => OTHER_CONDITIONS, + * ] + * @endcode + * + * Every condition is a key/value pair, whose key is a jQuery selector that + * denotes another element on the page, and whose value is an array of + * conditions, which must bet met on that element: + * @code + * [ + * 'visible' => [ + * JQUERY_SELECTOR => REMOTE_CONDITIONS, + * JQUERY_SELECTOR => REMOTE_CONDITIONS, + * ... + * ], + * ] + * @endcode + * All conditions must be met for the state to be applied. + * + * Each remote condition is a key/value pair specifying conditions on the + * other element that need to be met to apply the state to the element: + * @code + * [ + * 'visible' => [ + * ':input[name="remote_checkbox"]' => ['checked' => TRUE], + * ], + * ] + * @endcode + * + * For example, to show a textfield only when a checkbox is checked: + * @code + * $form['toggle_me'] = [ + * '#type' => 'checkbox', + * '#title' => t('Tick this box to type'), + * ]; + * $form['settings'] = [ + * '#type' => 'textfield', + * '#states' => [ + * // Only show this field when the 'toggle_me' checkbox is enabled. + * 'visible' => [ + * ':input[name="toggle_me"]' => ['checked' => TRUE], + * ], + * ], + * ]; + * @endcode + * + * The following states may be applied to an element: + * - enabled + * - disabled + * - required + * - optional + * - visible + * - invisible + * - checked + * - unchecked + * - expanded + * - collapsed + * + * The following states may be used in remote conditions: + * - empty + * - filled + * - checked + * - unchecked + * - expanded + * - collapsed + * - value + * + * The following states exist for both elements and remote conditions, but are + * not fully implemented and may not change anything on the element: + * - relevant + * - irrelevant + * - valid + * - invalid + * - touched + * - untouched + * - readwrite + * - readonly + * + * When referencing select lists and radio buttons in remote conditions, a + * 'value' condition must be used: + * @code + * '#states' => [ + * // Show the settings if 'bar' has been selected for 'foo'. + * 'visible' => [ + * ':input[name="foo"]' => ['value' => 'bar'], + * ], + * ], + * @endcode + * + * @param array $elements + * A render array element having a #states property as described above. + */ + public static function processStates(array &$elements) { + $elements['#attached']['library'][] = 'core/drupal.states'; + // Elements of '#type' => 'item' are not actual form input elements, but we + // still want to be able to show/hide them. Since there's no actual HTML + // input element available, setting #attributes does not make sense, but a + // wrapper is available, so setting #wrapper_attributes makes it work. + $key = ($elements['#type'] == 'item') ? '#wrapper_attributes' : '#attributes'; + $elements[$key]['data-drupal-states'] = Json::encode($elements['#states']); + } + } diff --git a/core/lib/Drupal/Core/Render/Element/FormElement.php b/core/lib/Drupal/Core/Render/Element/FormElement.php index 0168d2beb964..69d1e32e82a8 100644 --- a/core/lib/Drupal/Core/Render/Element/FormElement.php +++ b/core/lib/Drupal/Core/Render/Element/FormElement.php @@ -60,7 +60,7 @@ * - #required: (bool) Whether or not input is required on the element. * - #states: (array) Information about JavaScript states, such as when to * hide or show the element based on input on other elements. - * See drupal_process_states() for documentation. + * See \Drupal\Core\Form\FormHelper::processStates() for documentation. * - #title: (string) Title of the form element. Should be translated. * - #title_display: (string) Where and how to display the #title. Possible * values: diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php index 88189646b404..6f9ab71cc247 100644 --- a/core/lib/Drupal/Core/Render/Renderer.php +++ b/core/lib/Drupal/Core/Render/Renderer.php @@ -9,6 +9,7 @@ use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Controller\ControllerResolverInterface; +use Drupal\Core\Form\FormHelper; use Drupal\Core\Render\Element\RenderCallbackInterface; use Drupal\Core\Security\TrustedCallbackInterface; use Drupal\Core\Security\DoTrustedCallbackTrait; @@ -394,7 +395,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) { // Add any JavaScript state information associated with the element. if (!empty($elements['#states'])) { - drupal_process_states($elements); + FormHelper::processStates($elements); } // Get the children of the element, sorted by weight. diff --git a/core/lib/Drupal/Core/Render/RendererInterface.php b/core/lib/Drupal/Core/Render/RendererInterface.php index 5f8802d4c683..b74468010d06 100644 --- a/core/lib/Drupal/Core/Render/RendererInterface.php +++ b/core/lib/Drupal/Core/Render/RendererInterface.php @@ -219,7 +219,7 @@ public function renderPlaceholder($placeholder, array $elements); * prepended to #children. * - If this element has #states defined then JavaScript state information * is added to this element's #attached attribute by - * drupal_process_states(). + * \Drupal\Core\Form\FormHelper::processStates(). * - If this element has #attached defined then any required libraries, * JavaScript, CSS, or other custom data are added to the current page by * \Drupal\Core\Render\AttachmentsResponseProcessorInterface::processAttachments(). @@ -328,7 +328,7 @@ public function renderPlaceholder($placeholder, array $elements); * * @see \Drupal\Core\Render\ElementInfoManagerInterface::getInfo() * @see \Drupal\Core\Theme\ThemeManagerInterface::render() - * @see drupal_process_states() + * @see \Drupal\Core\Form\FormHelper::processStates() * @see \Drupal\Core\Render\AttachmentsResponseProcessorInterface::processAttachments() * @see \Drupal\Core\Render\RendererInterface::renderRoot() */ diff --git a/core/tests/Drupal/KernelTests/Core/Render/RendererLegacyTest.php b/core/tests/Drupal/KernelTests/Core/Render/RendererLegacyTest.php index 8cea7a1b90a9..9ea5d66c4095 100644 --- a/core/tests/Drupal/KernelTests/Core/Render/RendererLegacyTest.php +++ b/core/tests/Drupal/KernelTests/Core/Render/RendererLegacyTest.php @@ -2,6 +2,7 @@ namespace Drupal\KernelTests\Core\Render; +use Drupal\Core\Form\FormHelper; use Drupal\Core\Render\HtmlResponseAttachmentsProcessor; use Drupal\KernelTests\KernelTestBase; @@ -37,4 +38,47 @@ public function providerAttributes() { ]; } + /** + * Tests deprecation of the drupal_process_states() function. + * + * @dataProvider providerElements + * + * @expectedDeprecation drupal_process_states() is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use \Drupal\Core\Form\FormHelper::processStates() instead. See https://www.drupal.org/node/3000069 + */ + public function testDrupalProcessStates($elements) { + // Clone elements because processing changes array. + $expected = $elements; + drupal_process_states($expected); + FormHelper::processStates($elements); + $this->assertEquals($expected, $elements); + } + + /** + * Provides a list of elements to test. + */ + public function providerElements() { + return [ + [ + [ + '#type' => 'date', + '#states' => [ + 'visible' => [ + ':input[name="toggle_me"]' => ['checked' => TRUE], + ], + ], + ], + ], + [ + [ + '#type' => 'item', + '#states' => [ + 'visible' => [ + ':input[name="foo"]' => ['value' => 'bar'], + ], + ], + ], + ], + ]; + } + } diff --git a/core/tests/Drupal/Tests/Core/Form/FormHelperTest.php b/core/tests/Drupal/Tests/Core/Form/FormHelperTest.php index 556aab8f4c72..3fa9ab1b04ee 100644 --- a/core/tests/Drupal/Tests/Core/Form/FormHelperTest.php +++ b/core/tests/Drupal/Tests/Core/Form/FormHelperTest.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\Core\Form; +use Drupal\Component\Serialization\Json; use Drupal\Core\Form\FormHelper; use Drupal\Tests\UnitTestCase; @@ -82,4 +83,45 @@ public function testRewriteStatesSelector() { $this->assertSame($expected, $form, 'The #states selectors were properly rewritten.'); } + /** + * @covers ::processStates + * @dataProvider providerElements + */ + public function testProcessStates($elements, $key) { + $json = Json::encode($elements['#states']); + FormHelper::processStates($elements); + $this->assertEquals(['core/drupal.states'], $elements['#attached']['library']); + $this->assertEquals($json, $elements[$key]['data-drupal-states']); + } + + /** + * Provides a list of elements to test. + */ + public function providerElements() { + return [ + [ + [ + '#type' => 'date', + '#states' => [ + 'visible' => [ + ':input[name="toggle_me"]' => ['checked' => TRUE], + ], + ], + ], + '#attributes', + ], + [ + [ + '#type' => 'item', + '#states' => [ + 'visible' => [ + ':input[name="foo"]' => ['value' => 'bar'], + ], + ], + ], + '#wrapper_attributes', + ], + ]; + } + } -- GitLab