Skip to content

feat: Shared Render Tree - #1910

Open
linkdotnet wants to merge 1 commit into
mainfrom
feat/shared-dom-tree
Open

feat: Shared Render Tree#1910
linkdotnet wants to merge 1 commit into
mainfrom
feat/shared-dom-tree

Conversation

@linkdotnet

Copy link
Copy Markdown
Collaborator

Shared Render Tree

This went a bit bigger than initially anticipated. Basically this PR would solve and close:

So it is kind of a heavy-hitter code-wise but also feature-wise.

And has a major breaking behavioural change.

## The state before
Every IRenderedComponent produced its own markup and parsed it into its own AngleSharp document: Markup was Htmlizer.GetHtml(ComponentId, renderer), Nodes was
htmlParser.Parse(Markup), and BunitHtmlParser.Parse opens a fresh IDocument per call.

So parent hat its DOM including all children, but child-elements only have theirs (plus eventually their children). So this PR will allow all 3 attached scenarios / or fix bugs.

The new now

One markup string and one document per render tree, generated by its root.
Htmlizer records the [start, end) character range each component occupies while it
builds the string, in post-order. We had this once on another feature branch. So nothing new.

Markup then is sliced regarding the components position in the overall tree.
Nodes is a view over a run of sibling nodes in the shared document.

Edge cases

Imagine a component hat has only text

<p id="parent-p">Hello <TextOnlyChild /> world</p>

where TextOnlyChild does only contain text, no HTML element. The system then can't really tell what is the child here. The text merged with the parent. So they keep the current behavior more or less, but can't be used with the new GetOwingComponent - there is nothing to latch on here.

Other things

  • OnMarkupUpdated triggers now more often - basically because of going up and down the tree
  • We should be faster - given less work (hopefully - didn't measure it tbh)

v3?

Now we have a breaking change in behaviour. We might want go ahead with v3 where it is fine to have a breaking change behavioural wise. Breaking change being if a user "clicks" on a child element, it does in fact now bubble up! I do feel that is the better approach and more obvious, but breaks anyway. Depending on how we go ahead, we can make that clear in the docs.

@linkdotnet
linkdotnet requested review from egil and a lite review from Copilot September 5, 2026 13:47

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

ConditionalWeakTable<,>.AddOrUpdate is not available on the net8.0 target, causing a compile-time break for one of the supported TFMs.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR reworks bUnit’s rendered-component DOM model so all components under the same render-tree root share a single markup snapshot and AngleSharp document, enabling correct event bubbling across component boundaries and supporting “find owning component” and component-tree navigation APIs.

Changes:

  • Introduces a root-owned RootMarkupSnapshot (shared markup + document) and per-component “slice” views for Markup/Nodes.
  • Adds public extension APIs for component tree navigation (Parent/Root/GetAncestors/GetChildren) and for mapping DOM nodes back to their rendering component (GetOwningComponent).
  • Adds tests, docs, and changelog entries covering shared-document behavior, bubbling behavior changes, and the new APIs.
File summaries
File Description
tests/bunit.tests/Rendering/SharedDocumentTest.cs New tests validating shared-document DOM behavior and fallback cases
tests/bunit.tests/Rendering/ComponentTreeNavigationTest.cs New tests for parent/root/children/ancestor navigation APIs
tests/bunit.tests/Extensions/ElementOwningComponentTest.cs New tests for INode.GetOwningComponent* resolution
tests/bunit.tests/EventDispatchExtensions/ChildComponentEventBubblingTest.cs New tests confirming cross-component DOM bubbling behavior
tests/bunit.testassets/SampleComponents/TextOnlyChild.razor New test asset for text-only child rendering edge case
tests/bunit.testassets/SampleComponents/TableWithChildRows.razor New test asset for table/row nesting behavior
tests/bunit.testassets/SampleComponents/SiblingChildren.razor New test asset for sibling re-render scenarios
tests/bunit.testassets/SampleComponents/SelectWithChildOptions.razor New test asset for <select>/<option> ownership/selection behavior
tests/bunit.testassets/SampleComponents/ParentWithTextOnlyChild.razor New test asset covering inlined text-only child output
tests/bunit.testassets/SampleComponents/ParentWithLeadingTextChild.razor New test asset for “leading text prevents shared-node view” fallback
tests/bunit.testassets/SampleComponents/ParentWithEmptyChild.razor New test asset for empty child rendering behavior
tests/bunit.testassets/SampleComponents/ParentFormWithChildButton.razor New test asset for submit bubbling into parent form
tests/bunit.testassets/SampleComponents/ParentDivWithChildButton.razor New test asset for click bubbling + stopPropagation
tests/bunit.testassets/SampleComponents/LeadingTextChild.razor New test asset rendering markup with leading text
tests/bunit.testassets/SampleComponents/GrandParentDivWithChildButton.razor New test asset for 3-level bubbling / ancestor navigation
tests/bunit.testassets/SampleComponents/EmptyChild.razor New test asset for “renders nothing” scenario
tests/bunit.testassets/SampleComponents/CounterChild.razor New test asset used for sibling re-render and filtering tests
tests/bunit.testassets/SampleComponents/ChildWithButton.razor New test asset for basic child button interactions
tests/bunit.testassets/SampleComponents/ChildTableRow.razor New test asset for <tr> rendered by child component
tests/bunit.testassets/SampleComponents/ChildOption.razor New test asset for <option> rendered by child component
src/bunit/Rendering/RootRenderedComponent.cs Adds root rendered component that owns the shared snapshot
src/bunit/Rendering/RenderedComponent.cs Reworks per-component markup/nodes to be snapshot “slices” + invalidation signaling
src/bunit/Rendering/IRenderedComponent.cs Extends internal contracts for root access + markup-updated signaling
src/bunit/Rendering/Internal/RootMarkupSnapshot.cs Implements shared markup/document storage and per-component node-range views
src/bunit/Rendering/Internal/NodeRangeList.cs Adds INodeList view over sibling node ranges
src/bunit/Rendering/Internal/Htmlizer.cs Records component markup ranges while generating shared markup
src/bunit/Rendering/Internal/ComponentMarkupRange.cs Adds range struct + Htmlizer result record type
src/bunit/Rendering/BunitRenderer.cs Adds root snapshot regeneration + markup-updated propagation + private-document tracking
src/bunit/Extensions/RenderedComponentTreeExtensions.cs Adds public component tree navigation extensions
src/bunit/Extensions/ElementOwningComponentExtensions.cs Adds public DOM-node-to-rendered-component lookup extensions
docs/site/docs/verification/index.md Adds verification docs entry for “find owning component”
docs/site/docs/verification/find-owning-component.md New documentation for GetOwningComponent* and behavior details
docs/site/docs/toc.md Adds doc TOC link for new verification topic
docs/site/docs/interaction/trigger-event-handlers.md Documents cross-component event bubbling behavior
CHANGELOG.md Notes new APIs and breaking behavior change under Unreleased
Review details
  • Files reviewed: 35/35 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/bunit/Rendering/BunitRenderer.cs
Comment on lines +5 to +7
/// <summary>
/// Extension methods for navigating the component tree an
/// <see cref="IRenderedComponent{TComponent}"/> is part of.
@linkdotnet

Copy link
Copy Markdown
Collaborator Author

@egil I started to have a v3 branch - I am much inclined to have this in v3 with all the issues flagged for that milestone in our github issue tracker. So we can release roughly with net11.

For sure we need a release party somewhere :D

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