From 8995e3be0fbf0f2662e93b940919e9a4956800e8 Mon Sep 17 00:00:00 2001
From: just_like_good_vibes <mickael@meulle.com>
Date: Mon, 24 Mar 2025 12:35:00 +0100
Subject: [PATCH 1/3] updated

---
 src/Element/ComponentElementBuilder.php       | 36 ++++++++---
 .../UiPatterns/PropType/BooleanPropType.php   |  4 +-
 .../UiPatterns/PropType/StringPropType.php    |  2 +-
 .../test-component.component.yml              |  6 ++
 .../test-component/test-component.twig        | 12 ++++
 .../BooleanPropTypeTest.php                   | 64 +++++++++++++++++++
 .../Kernel/PropTypeNormalizationTestBase.php  | 42 ++++++++----
 7 files changed, 142 insertions(+), 24 deletions(-)

diff --git a/src/Element/ComponentElementBuilder.php b/src/Element/ComponentElementBuilder.php
index 23a3a1e8..67bf9e2d 100644
--- a/src/Element/ComponentElementBuilder.php
+++ b/src/Element/ComponentElementBuilder.php
@@ -98,19 +98,39 @@ class ComponentElementBuilder implements TrustedCallbackInterface {
       }
     }
     else {
-      if (!empty($data) || $prop_type->getPluginId() === 'attributes') {
-        // For JSON Schema validator, empty value is not the same as missing
-        // value, and we want to prevent some of the prop types rules to be
-        // applied on empty values: string pattern, string format,
-        // enum, number min/max...
-        // However, we don't remove empty attributes to avoid an error with
-        // Drupal\Core\Template\TwigExtension::createAttribute() when themers
-        // forget to use the default({}) filter.
+      if (!static::isPropValueEmpty($data, $prop_type)) {
         $build['#props'][$prop_or_slot_id] = $data;
       }
     }
   }
 
+  /**
+   * Determine if a prop value is empty.
+   *
+   * @param mixed $data
+   *   The prop value.
+   * @param \Drupal\ui_patterns\PropTypeInterface $prop_type
+   *   Target prop type.
+   *
+   * @return bool
+   *   TRUE if the prop value is empty, FALSE otherwise.
+   */
+  protected static function isPropValueEmpty(mixed $data, PropTypeInterface $prop_type): bool {
+    // For JSON Schema validator, empty value is not the same as missing
+    // value, and we want to prevent some of the prop types rules to be
+    // applied on empty values: string pattern, string format,
+    // enum, number min/max...
+    // However, we don't remove empty attributes to avoid an error with
+    // Drupal\Core\Template\TwigExtension::createAttribute() when themers
+    // forget to use the default({}) filter.
+    // For boolean values, we only remove NULL values.
+    return match ($prop_type->getPluginId()) {
+      'attributes' => FALSE,
+      'boolean' => ($data === NULL),
+      default => empty($data),
+    };
+  }
+
   /**
    * Update the build array for a configured source on a prop/slot.
    *
diff --git a/src/Plugin/UiPatterns/PropType/BooleanPropType.php b/src/Plugin/UiPatterns/PropType/BooleanPropType.php
index 2b9d2bba..c34ce92c 100644
--- a/src/Plugin/UiPatterns/PropType/BooleanPropType.php
+++ b/src/Plugin/UiPatterns/PropType/BooleanPropType.php
@@ -28,7 +28,7 @@ class BooleanPropType extends PropTypePluginBase {
   /**
    * {@inheritdoc}
    */
-  public static function normalize(mixed $value, ?array $definition = NULL): bool {
+  public static function normalize(mixed $value, ?array $definition = NULL): ?bool {
     if (is_bool($value)) {
       return $value;
     }
@@ -37,7 +37,7 @@ class BooleanPropType extends PropTypePluginBase {
       $value = (int) $value;
       return (bool) $value;
     }
-    return (bool) $value;
+    return ($value !== NULL) ? (bool) $value : NULL;
   }
 
 }
diff --git a/src/Plugin/UiPatterns/PropType/StringPropType.php b/src/Plugin/UiPatterns/PropType/StringPropType.php
index e3581382..6e83429d 100644
--- a/src/Plugin/UiPatterns/PropType/StringPropType.php
+++ b/src/Plugin/UiPatterns/PropType/StringPropType.php
@@ -37,7 +37,7 @@ class StringPropType extends PropTypePluginBase {
       'url' => $value,
       'identifier' => $value,
       'string' => $value,
-      'default' => (string) $value,
+      default => (string) $value,
     };
   }
 
diff --git a/tests/modules/ui_patterns_test/components/test-component/test-component.component.yml b/tests/modules/ui_patterns_test/components/test-component/test-component.component.yml
index 5315381b..66826dd4 100644
--- a/tests/modules/ui_patterns_test/components/test-component/test-component.component.yml
+++ b/tests/modules/ui_patterns_test/components/test-component/test-component.component.yml
@@ -29,6 +29,12 @@ props:
     boolean:
       title: "Boolean"
       $ref: "ui-patterns://boolean"
+    boolean_with_default_false:
+      title: "Boolean"
+      $ref: "ui-patterns://boolean"
+    boolean_with_default_true:
+      title: "Boolean"
+      $ref: "ui-patterns://boolean"
     links:
       title: "Links"
       $ref: "ui-patterns://links"
diff --git a/tests/modules/ui_patterns_test/components/test-component/test-component.twig b/tests/modules/ui_patterns_test/components/test-component/test-component.twig
index 6af3c38a..e1a8976c 100644
--- a/tests/modules/ui_patterns_test/components/test-component/test-component.twig
+++ b/tests/modules/ui_patterns_test/components/test-component/test-component.twig
@@ -25,6 +25,18 @@
   <div class="ui-patterns-props-boolean">
     {{ boolean }}
   </div>
+  <div class="ui-patterns-props-boolean_with_default_false">
+    {% if (boolean_with_default_false is not defined) or (boolean_with_default_false is null)  %}
+    {% set boolean_with_default_false = false %}
+    {% endif %}
+    {{ boolean_with_default_false }}
+  </div>
+  <div class="ui-patterns-props-boolean_with_default_true">
+    {% if (boolean_with_default_true is not defined) or (boolean_with_default_true is null)  %}
+    {% set boolean_with_default_true = true %}
+    {% endif %}
+    {{ boolean_with_default_true }}
+  </div>
   <div class="ui-patterns-props-links">
     {% for item in links %}
       {% if item.url %}
diff --git a/tests/src/Kernel/PropTypeNormalization/BooleanPropTypeTest.php b/tests/src/Kernel/PropTypeNormalization/BooleanPropTypeTest.php
index 63417c9a..f7384a63 100644
--- a/tests/src/Kernel/PropTypeNormalization/BooleanPropTypeTest.php
+++ b/tests/src/Kernel/PropTypeNormalization/BooleanPropTypeTest.php
@@ -34,6 +34,30 @@ class BooleanPropTypeTest extends PropTypeNormalizationTestBase {
     $this->runRenderPropTest('boolean', ["value" => $value, "rendered_value" => $rendered_value]);
   }
 
+  /**
+   * Test rendered component with prop default false.
+   *
+   * @dataProvider renderingTestsDefaultFalse
+   */
+  public function testRenderingDefaultFalse(mixed $value, mixed $rendered_value) : void {
+    $this->runRenderPropTest('boolean_with_default_false', ["value" => $value, "rendered_value" => $rendered_value]);
+    if ($value === TRUE) {
+      $this->runRenderPropTest(NULL, ["value" => $value, "rendered_value" => $rendered_value]);
+    }
+  }
+
+  /**
+   * Test rendered component with prop default true.
+   *
+   * @dataProvider renderingTestsDefaultTrue
+   */
+  public function testRenderingDefaultTrue(mixed $value, mixed $rendered_value) : void {
+    $this->runRenderPropTest('boolean_with_default_true', ["value" => $value, "rendered_value" => $rendered_value]);
+    if ($value === TRUE) {
+      $this->runRenderPropTest(NULL, ["value" => $value, "rendered_value" => $rendered_value]);
+    }
+  }
+
   /**
    * Provides data for testNormalization.
    */
@@ -81,4 +105,44 @@ class BooleanPropTypeTest extends PropTypeNormalizationTestBase {
     ];
   }
 
+  /**
+   * Provides data for testNormalization.
+   */
+  public static function renderingTestsDefaultFalse() : array {
+    return [
+      "null value" => [
+        NULL,
+        '<div class="ui-patterns-props-boolean_with_default_false"></div>',
+      ],
+      "false value" => [
+        FALSE,
+        '<div class="ui-patterns-props-boolean_with_default_false"></div>',
+      ],
+      "true value" => [
+        TRUE,
+        '<div class="ui-patterns-props-boolean_with_default_false">1</div>',
+      ],
+    ];
+  }
+
+  /**
+   * Provides data for testNormalization.
+   */
+  public static function renderingTestsDefaultTrue() : array {
+    return [
+      "null value" => [
+        NULL,
+        '<div class="ui-patterns-props-boolean_with_default_true">1</div>',
+      ],
+      "false value" => [
+        FALSE,
+        '<div class="ui-patterns-props-boolean_with_default_true"></div>',
+      ],
+      "true value" => [
+        TRUE,
+        '<div class="ui-patterns-props-boolean_with_default_true">1</div>',
+      ],
+    ];
+  }
+
 }
diff --git a/tests/src/Kernel/PropTypeNormalizationTestBase.php b/tests/src/Kernel/PropTypeNormalizationTestBase.php
index 1d1cfebd..90d8f31a 100644
--- a/tests/src/Kernel/PropTypeNormalizationTestBase.php
+++ b/tests/src/Kernel/PropTypeNormalizationTestBase.php
@@ -54,12 +54,12 @@ abstract class PropTypeNormalizationTestBase extends KernelTestBase {
   /**
    * Run tests with a given prop.
    *
-   * @param string $prop
-   *   The prop to test.
+   * @param string|null $prop
+   *   The prop to test or NULL for no prop.
    * @param array $tested_value
    *   The tested value.
    */
-  protected function runRenderPropTest(string $prop, array $tested_value) : void {
+  protected function runRenderPropTest(?string $prop, array $tested_value) : void {
     $expectedOutput = [
       "rendered_value" => $tested_value["rendered_value"],
       "assert" => $tested_value["assert"] ?? "assertStringContainsString",
@@ -68,19 +68,35 @@ abstract class PropTypeNormalizationTestBase extends KernelTestBase {
     if (!empty($exception_class)) {
       $this->expectException($exception_class);
     }
-    $this->assertExpectedOutput($expectedOutput, [
-      "#type" => "inline_template",
-      "#template" => sprintf("{{ include('ui_patterns_test:test-component', {%s: injected}) }}", $prop),
-      "#context" => ["injected" => $tested_value["value"]],
-    ]);
+    if (!empty($prop)) {
+      $this->assertExpectedOutput($expectedOutput, [
+        "#type" => "inline_template",
+        "#template" => sprintf("{{ include('ui_patterns_test:test-component', {%s: injected}) }}", $prop),
+        "#context" => ["injected" => $tested_value["value"]],
+      ]);
+    }
+    else {
+      $this->assertExpectedOutput($expectedOutput, [
+        "#type" => "inline_template",
+        "#template" => "{{ include('ui_patterns_test:test-component', {}) }}",
+      ]);
+    }
     if (!empty($exception_class)) {
       $this->expectException($exception_class);
     }
-    $this->assertExpectedOutput($expectedOutput, [
-      "#type" => "component",
-      '#component' => 'ui_patterns_test:test-component',
-      "#props" => [$prop => $tested_value["value"]],
-    ]);
+    if (!empty($prop)) {
+      $this->assertExpectedOutput($expectedOutput, [
+        "#type" => "component",
+        '#component' => 'ui_patterns_test:test-component',
+        "#props" => [$prop => $tested_value["value"]],
+      ]);
+    }
+    else {
+      $this->assertExpectedOutput($expectedOutput, [
+        "#type" => "component",
+        '#component' => 'ui_patterns_test:test-component',
+      ]);
+    }
   }
 
 }
-- 
GitLab


From 072dcad942803496b5593da5e6caef26d43dc589 Mon Sep 17 00:00:00 2001
From: just_like_good_vibes <mickael@meulle.com>
Date: Mon, 24 Mar 2025 13:57:58 +0100
Subject: [PATCH 2/3] fixed bad test

---
 .../src/Kernel/PropTypeNormalization/BooleanPropTypeTest.php  | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/tests/src/Kernel/PropTypeNormalization/BooleanPropTypeTest.php b/tests/src/Kernel/PropTypeNormalization/BooleanPropTypeTest.php
index f7384a63..8449ea9b 100644
--- a/tests/src/Kernel/PropTypeNormalization/BooleanPropTypeTest.php
+++ b/tests/src/Kernel/PropTypeNormalization/BooleanPropTypeTest.php
@@ -41,7 +41,7 @@ class BooleanPropTypeTest extends PropTypeNormalizationTestBase {
    */
   public function testRenderingDefaultFalse(mixed $value, mixed $rendered_value) : void {
     $this->runRenderPropTest('boolean_with_default_false', ["value" => $value, "rendered_value" => $rendered_value]);
-    if ($value === TRUE) {
+    if ($value === NULL) {
       $this->runRenderPropTest(NULL, ["value" => $value, "rendered_value" => $rendered_value]);
     }
   }
@@ -53,7 +53,7 @@ class BooleanPropTypeTest extends PropTypeNormalizationTestBase {
    */
   public function testRenderingDefaultTrue(mixed $value, mixed $rendered_value) : void {
     $this->runRenderPropTest('boolean_with_default_true', ["value" => $value, "rendered_value" => $rendered_value]);
-    if ($value === TRUE) {
+    if ($value === NULL) {
       $this->runRenderPropTest(NULL, ["value" => $value, "rendered_value" => $rendered_value]);
     }
   }
-- 
GitLab


From f853f13695d50c5c209fda802dd215afc45ac576 Mon Sep 17 00:00:00 2001
From: just_like_good_vibes <mickael@meulle.com>
Date: Mon, 24 Mar 2025 14:00:59 +0100
Subject: [PATCH 3/3] improved tests with NULL value

---
 .../Kernel/PropTypeNormalization/BooleanPropTypeTest.php    | 6 ------
 tests/src/Kernel/PropTypeNormalizationTestBase.php          | 5 +++++
 2 files changed, 5 insertions(+), 6 deletions(-)

diff --git a/tests/src/Kernel/PropTypeNormalization/BooleanPropTypeTest.php b/tests/src/Kernel/PropTypeNormalization/BooleanPropTypeTest.php
index 8449ea9b..7c5eec9c 100644
--- a/tests/src/Kernel/PropTypeNormalization/BooleanPropTypeTest.php
+++ b/tests/src/Kernel/PropTypeNormalization/BooleanPropTypeTest.php
@@ -41,9 +41,6 @@ class BooleanPropTypeTest extends PropTypeNormalizationTestBase {
    */
   public function testRenderingDefaultFalse(mixed $value, mixed $rendered_value) : void {
     $this->runRenderPropTest('boolean_with_default_false', ["value" => $value, "rendered_value" => $rendered_value]);
-    if ($value === NULL) {
-      $this->runRenderPropTest(NULL, ["value" => $value, "rendered_value" => $rendered_value]);
-    }
   }
 
   /**
@@ -53,9 +50,6 @@ class BooleanPropTypeTest extends PropTypeNormalizationTestBase {
    */
   public function testRenderingDefaultTrue(mixed $value, mixed $rendered_value) : void {
     $this->runRenderPropTest('boolean_with_default_true', ["value" => $value, "rendered_value" => $rendered_value]);
-    if ($value === NULL) {
-      $this->runRenderPropTest(NULL, ["value" => $value, "rendered_value" => $rendered_value]);
-    }
   }
 
   /**
diff --git a/tests/src/Kernel/PropTypeNormalizationTestBase.php b/tests/src/Kernel/PropTypeNormalizationTestBase.php
index 90d8f31a..a73450ee 100644
--- a/tests/src/Kernel/PropTypeNormalizationTestBase.php
+++ b/tests/src/Kernel/PropTypeNormalizationTestBase.php
@@ -60,6 +60,11 @@ abstract class PropTypeNormalizationTestBase extends KernelTestBase {
    *   The tested value.
    */
   protected function runRenderPropTest(?string $prop, array $tested_value) : void {
+    if ($tested_value["value"] === NULL && !empty($prop)) {
+      // If the value injected is NULL
+      // we will also try with removing the prop.
+      $this->runRenderPropTest(NULL, $tested_value);
+    }
     $expectedOutput = [
       "rendered_value" => $tested_value["rendered_value"],
       "assert" => $tested_value["assert"] ?? "assertStringContainsString",
-- 
GitLab