From ac0554422a3d49c6081f939f4b009a8d533ed70f Mon Sep 17 00:00:00 2001 From: WarLikeLaux Date: Sat, 13 Jun 2026 21:14:32 +0600 Subject: [PATCH] fix(view): preserve zero attribute bindings --- .../src/Attributes/ExpressionAttribute.php | 10 ++++-- .../src/Elements/ViewComponentElement.php | 4 +-- .../TempestViewRendererDataPassingTest.php | 35 ++++++++++++++++++- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/packages/view/src/Attributes/ExpressionAttribute.php b/packages/view/src/Attributes/ExpressionAttribute.php index 43f8b57d91..0fd8209d8e 100644 --- a/packages/view/src/Attributes/ExpressionAttribute.php +++ b/packages/view/src/Attributes/ExpressionAttribute.php @@ -66,14 +66,20 @@ public static function render(string $name, mixed $value): string return str($name)->kebab()->toString(); } - if (! $value) { + if ($value === false || $value === null) { + return ''; + } + + $resolvedValue = self::resolveValue($value); + + if ($resolvedValue === '') { return ''; } return sprintf( '%s="%s"', str($name)->kebab(), - ExpressionAttribute::resolveValue($value), + $resolvedValue, ); } diff --git a/packages/view/src/Elements/ViewComponentElement.php b/packages/view/src/Elements/ViewComponentElement.php index bf6f124fe1..539700e9b3 100644 --- a/packages/view/src/Elements/ViewComponentElement.php +++ b/packages/view/src/Elements/ViewComponentElement.php @@ -60,7 +60,7 @@ public function __construct( $this->expressionAttributes = arr($attributes) ->filter(fn (string $_, string $key) => str_starts_with($key, ':')) ->filter(fn (string $_, string $key) => ! in_array($key, [':if', ':else', ':elseif', ':foreach', ':forelse'], strict: true)) - ->mapWithKeys(fn (string $value, string $key) => yield str($key)->camel()->ltrim(':')->toString() => $value ?: 'true'); + ->mapWithKeys(fn (string $value, string $key) => yield str($key)->camel()->ltrim(':')->toString() => $value === '' ? 'true' : $value); $this->scopedVariables = arr(); } @@ -448,7 +448,7 @@ private function exportAttributesArray(): string $isExpression = isset($this->expressionAttributes[$camelKey]); $entries[] = $isExpression - ? sprintf("'%s' => %s", $key, $value ?: 'true') + ? sprintf("'%s' => %s", $key, $value === '' ? 'true' : $value) : sprintf("'%s' => %s", $key, ViewObjectExporter::exportValue($value)); } diff --git a/tests/Integration/View/TempestViewRendererDataPassingTest.php b/tests/Integration/View/TempestViewRendererDataPassingTest.php index 1bd3d479b5..6212f296b7 100644 --- a/tests/Integration/View/TempestViewRendererDataPassingTest.php +++ b/tests/Integration/View/TempestViewRendererDataPassingTest.php @@ -193,6 +193,25 @@ public function test_expression_attribute_on_view_component(): void ); } + public function test_zero_expression_attribute_literal_on_view_component(): void + { + $this->view->registerViewComponent( + 'x-link', + <<<'HTML' + + HTML, + ); + + $this->assertSame( + 'a', + $this->view->render( + <<<'HTML' + a + HTML, + ), + ); + } + public function test_normal_attribute_on_view_component(): void { // 💯 always pass as variable, never set directly as attribute @@ -270,7 +289,7 @@ public function test_boolean_attributes(): void #[TestWith(['false'])] #[TestWith(['null'])] - #[TestWith(['0'])] + #[TestWith(["''"])] #[TestWith(['$show'])] public function test_falsy_bool_attribute(mixed $value): void { @@ -283,6 +302,20 @@ public function test_falsy_bool_attribute(mixed $value): void HTML, $html); } + #[TestWith([0])] + #[TestWith([0.0])] + #[TestWith(['0'])] + public function test_zero_expression_attribute_value_is_rendered(mixed $value): void + { + $html = $this->view->render(<<<'HTML' +
+ HTML, value: $value); + + $this->assertStringEqualsStringIgnoringLineEndings(<<<'HTML' +
+ HTML, $html); + } + #[TestWith(['true'])] #[TestWith(['$show'])] public function test_truthy_bool_attribute(mixed $value): void