From 1fbe01f37a77c5e62a7f78853c60ccddae66a040 Mon Sep 17 00:00:00 2001 From: Henrik Persson Date: Mon, 10 Aug 2026 21:54:30 +0200 Subject: [PATCH] Fold: reach inherited @aware values through the data stack Folding a component that declares @aware merges the inherited value into that component's own attribute bag. At render time the value never goes there -- it reaches the component through the data stack, and the bag holds only what the call site wrote -- so the two pipelines disagree for any component that asks where a value came from rather than what it is. The case that surfaced it is a form control inheriting its field name from a wrapper. Unfolded, its bag has no `name`, so it can tell it is one of a group; folded, `name` is merged in and it reads as a control that named itself, and draws the group's validation message a second time under every box. A wire:model control picks up a `name` its binding meant to replace the same way. Neither shows up as broken markup -- it is well formed, it just says something else. mergeAwareProps() already knows the answer: it merges only where the call site did not write the key. This keeps those values in a frame of their own and pushes it under the component's, so @aware resolves in the order it does at render -- own first, then ancestors -- while the bag stays clean. The two comparison tests render the same template through both pipelines. The first fails without this change (`data-source="own"` against `data-source="inherited"`); the second covers the other direction, so a value genuinely written on the tag is still read as the component's own. --- src/BladeRenderer.php | 12 +++++++++++- src/Folder/Foldable.php | 11 +++++++---- tests/ComparisonTest.php | 14 ++++++++++++++ .../foldable/input-aware-bag.blade.php | 18 ++++++++++++++++++ 4 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 tests/fixtures/views/components/foldable/input-aware-bag.blade.php diff --git a/src/BladeRenderer.php b/src/BladeRenderer.php index 89301e0e..ae0caa29 100644 --- a/src/BladeRenderer.php +++ b/src/BladeRenderer.php @@ -39,7 +39,7 @@ public function getTemporaryCachePath(): string /** * Render a Blade template string in isolation by freezing and restoring compiler state. */ - public function render(ComponentNode $component, ComponentSource $source): string + public function render(ComponentNode $component, ComponentSource $source, array $aware = []): string { $temporaryCachePath = $this->getTemporaryCachePath(); @@ -112,6 +112,15 @@ function ($input) { return [$slot->name => new ComponentSlot($slot->content())]; }); + $awareData = Arr::mapWithKeys($aware, function (Attribute $attribute) { + return [$attribute->name => $attribute->getStaticValue()]; + }); + + // Inherited @aware values reach the component the way they do at render -- + // through the data stack, under the component's own frame -- rather than by + // being merged into its attribute bag. A component that asks whether a key + // was written on its tag then gets the same answer folded and unfolded. + $this->runtime->pushData($awareData); $this->runtime->pushData($attributes); $this->runtime->pushSlots($slots); @@ -131,6 +140,7 @@ function ($input) { ob_end_clean(); } + $this->runtime->popData(); $this->runtime->popData(); $this->manager->stopFolding(); diff --git a/src/Folder/Foldable.php b/src/Folder/Foldable.php index ec03b8db..599442e9 100644 --- a/src/Folder/Foldable.php +++ b/src/Folder/Foldable.php @@ -23,6 +23,9 @@ class Foldable protected array $slotByPlaceholder = []; protected int $placeholderIndex = 0; + /** @var array Inherited @aware values, kept out of the component's own bag. */ + protected array $awareAttributes = []; + protected ComponentNode $renderable; protected string $html; @@ -52,7 +55,7 @@ public function fold(): string $this->setupSlots(); $this->mergeAwareProps(); - $this->html = $this->renderer->render($this->renderable, $this->source); + $this->html = $this->renderer->render($this->renderable, $this->source, $this->awareAttributes); $this->processUncompiledAttributes(); $this->restorePlaceholders(); @@ -165,14 +168,14 @@ protected function mergeAwareProps(): void $this->attributeByPlaceholder[$placeholder] = $attribute; - $this->renderable->attributes[$prop] = new Attribute( + $this->awareAttributes[$prop] = new Attribute( name: $prop, value: $placeholder, propName: $prop, dynamic: false, ); } else { - $this->renderable->attributes[$prop] = new Attribute( + $this->awareAttributes[$prop] = new Attribute( name: $attribute->name, value: $attribute->value, propName: $attribute->propName, @@ -187,7 +190,7 @@ protected function mergeAwareProps(): void // skip adding the attribute. This lets @aware and @props handle defaults // at runtime, matching the non-folded behavior. Adding an attribute with // null value would render as prop="" in HTML, corrupting null to empty string. - $this->renderable->attributes[$prop] = new Attribute( + $this->awareAttributes[$prop] = new Attribute( name: $prop, value: $default, propName: $prop, diff --git a/tests/ComparisonTest.php b/tests/ComparisonTest.php index dedb80cf..0cd26c2b 100644 --- a/tests/ComparisonTest.php +++ b/tests/ComparisonTest.php @@ -52,6 +52,20 @@ BLADE )); +test('foldable aware read back off the attribute bag', fn () => compare(<<<'BLADE' + + + + BLADE +)); + +test('foldable aware written on the tag itself', fn () => compare(<<<'BLADE' + + + + BLADE +)); + test('foldable boolean attributes', fn () => compare(<<<'BLADE' BLADE, diff --git a/tests/fixtures/views/components/foldable/input-aware-bag.blade.php b/tests/fixtures/views/components/foldable/input-aware-bag.blade.php new file mode 100644 index 00000000..4bdae58f --- /dev/null +++ b/tests/fixtures/views/components/foldable/input-aware-bag.blade.php @@ -0,0 +1,18 @@ +@blaze(fold: true) + +{{-- A component that has to tell an attribute written on its own tag from one it + inherited. The field name on a form control is the usual case: it decides + whether the control owns its own validation message or whether the field + around it does. The bag is snapshotted and restored because @aware consumes + the key, and the value still has to reach the element. --}} +@php + $__bag = $attributes->getAttributes(); +@endphp + +@aware(['type' => 'text']) + +@php + $attributes->setAttributes($__bag); +@endphp + +