Skip to content

Fold: reach inherited @aware values through the data stack - #200

Closed
oneleggedswede wants to merge 1 commit into
livewire:mainfrom
oneleggedswede:fold-aware-through-data-stack
Closed

Fold: reach inherited @aware values through the data stack#200
oneleggedswede wants to merge 1 commit into
livewire:mainfrom
oneleggedswede:fold-aware-through-data-stack

Conversation

@oneleggedswede

Copy link
Copy Markdown
Contributor

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.

For most components that difference is invisible: both routes hand back the same value. It matters for a component that asks where a value came from rather than what it is.

The case that surfaced it

A form control that inherits its field name from a wrapper, and uses "was this written on my tag?" to decide whether it owns its own validation message:

<x-field name="tags">
    <x-checkbox value="php" label="PHP" />
    <x-checkbox value="laravel" label="Laravel" />
</x-field>

Unfolded, the checkbox's bag has no name, so it can tell it is one of a group and leaves the message to the field. Folded, name="tags" is merged into its bag, it reads as a control that named itself, and the group's message is drawn again under every box. A wire:model control picks up a name attribute its binding meant to replace in the same way.

Neither shows up as broken markup — it is well formed, it just says something else. @aware unsetting the key afterwards doesn't help a component that has to snapshot and restore the bag around the directive, which is what a control does when the value also has to reach the rendered element.

The change

mergeAwareProps() already knows the answer — it merges only where the call site did not write the key:

if (isset($this->renderable->attributes[$prop])) {
    continue;
}

…and then discards it. 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.

Tests

Two cases added to ComparisonTest, which renders the same template through both pipelines and asserts byte equality.

The first fails on main and passes here:

- <input type="number" data-source="own" >
+ <input type="number" 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. The existing suite is unchanged: 266 passing before, 268 after.

Note

InvalidBlazeFoldUsageException::forAware() is defined and never called. With this, @aware and fold compose rather than being something to warn about — happy to wire the exception up instead if you'd rather go the other way, but this seemed the better end of it.

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.
@github-actions

Copy link
Copy Markdown
Contributor

Benchmark Result: Default

Attempt Blade Blaze Change
#1 364.93ms 15.80ms 95.7%
#2 365.12ms 16.11ms 95.6%
#3 360.15ms 15.88ms 95.6%
#4 364.91ms 15.90ms 95.6%
#5 359.07ms 15.89ms 95.6%
#6 * 363.75ms 16.44ms 95.5%
#7 * 374.20ms 15.89ms 95.8%
#8 361.87ms 16.31ms 95.5%
#9 364.51ms 16.07ms 95.6%
#10 364.85ms 15.94ms 95.6%
Snapshot 362.16ms 15.96ms 95.6%
Result 364.68ms (~) 15.92ms (~) 95.6% (~)

Median of 10 attempts (* = outlier, excluded from result), 5000 iterations x 10 rounds, 47.84s total

To run a specific benchmark, comment /benchmark <name>
attributes, aware, class, default, forwarding, merge, named-slots, no-attributes, slot, compilation

@ganyicz ganyicz self-assigned this Aug 10, 2026
@ganyicz

ganyicz commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Hi @oneleggedswede,

thanks for contributing and reporting the issue!

I have addressed this in #201, fixing the issue more broadly and adding few focused tests.

I'll close this PR but I’ve added you as a co-author to the other one.

In your example and test, you capture the attribute bag before @aware, re-assigning it later. However, this wouldn't be required in standard Blade and uncovers the real issue, which is that Blaze mutates the attribute bag in @aware.

When the above fix is released you can remove this workaround as the attribute bag will remain untouched.

@ganyicz ganyicz closed this Aug 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants