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