Engine: Introduce SlotSubtree to render the markup a slot covers
- #2283
Merged
Conversation
|
View your CI Pipeline Execution ↗ for commit 626c080
💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗ ☁️ Nx Cloud last updated this comment at |
🌿 Interactive Playground and Documentation PreviewA preview deployment has been built for this pull request. Try out the changes live in the interactive playground: 🌱 Grown from commit ✅ Preview deployment has been cleaned up. |
commit: |
marcoroth
force-pushed
the
slots/subtrees
branch
from
August 18, 2026 16:17
c207527 to
626c080
Compare
marcoroth
force-pushed
the
slots/subtrees
branch
from
August 18, 2026 16:22
626c080 to
7f8444b
Compare
marcoroth
added a commit
that referenced
this pull request
Aug 19, 2026
This pull request teaches the dependency analysis that a block's
parameters carry whatever that block iterates, in one template and
across a `render` call, and in all three languages that implement it.
```erb
<ul>
<% @items.each do |item| %>
<li><%= item.name %></li>
<% end %>
</ul>
```
`item` is `@items`, one at a time. Matching is by name, so nothing
connected the two, and `item.name` was attributed to nothing.
Ruby learned this within a single template in #2280. This finishes it:
the two ports learn the same thing, and the Ruby trace learns it across
a render boundary, which is where it was answering a user-facing
question incorrectly.
`affected_templates` and `state_flow` back `herb dependencies` and `herb
actionview flow`, and their whole product is the list of templates a
piece of state reaches. One shape was missing from it.
traced:
```erb
<%= render partial: "posts/card", collection: @posts %>
```
not traced:
```erb
<% @posts.each do |post| %>
<%= render "posts/card", card: post %>
<% end %>
```
For the second, `affected_templates(entry, "@posts")` returned
`["index.html.erb"]` and omitted `_card.html.erb`, and `state_flow`
returned a tree with no child. `trace_state` asks whether a render
call's local carries the state, and `card: post` names a block parameter
that nothing tied to the collection that bound it.
The collector now records the blocks open around a render call, with the
names each binds, and a local whose value is one of those names carries
what its block iterates.
`analyze` now parses with `iteration_nodes`, so a block running per item
and a block running once are different node types. Without that,
`form_with model: @post do |f|` looks like a collection and `f` carries
`@post` into every partial rendered inside the form.
Once the trace reaches `_card.html.erb`, its slots become addressable,
and they came back as an `identity`, which is a client writing one value
into every card. A render inside an iterating block is now per item the
same way `collection:` is, and the modes below a block that runs once
are left alone.
Follow up on #2279, #2280, #2281, #2282 and #2283.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This pull request introduces
Herb::Engine::SlotSubtree, which compiles the markup behind one slot, for the two cases a payload of values cannot carry.SlotIndex#applyreports a deferred entry when the page is asked for something values cannot express, which is a conditional taking a branch that never rendered, and a collection gaining an item there is no row to copy. Both name a slot, and nothing on the server answered one.SubtreeCompilerrenders the node at anode_pathandSlotVisitorrecords thenode_pathof every slot, so a slot index is all that is needed to reach its markup. The two agreeing on what anode_pathmeans is what makes this a lookup.An attribute is refused
A path indexes an element's body and does not descend into its open tag, so an attribute slot's path names the element holding it. Compiling that would hand back
<div class="card">x</div>where the caller asked forcard. Those slots are values, and values is how they come back.Asking for only what changed
subtree_slotssays which slots a change needs markup for, so a request naming what changed can be answered without asking the page.Only structural slots appear, because that is the whole of what values cannot say. Changing something a branch merely displays asks for nothing.
What this does not do
It does not make the render cheaper.
SubtreeCompilerruns the whole template and keeps one node's output, which its own documentation says is the answer that is correct without knowing which expressions the target depends on:That analysis now exists, so the question can be asked. It is not asked here, because answering it means changing what the compiler emits and taking on the side effects that come with skipping work.