Skip to content

fix(flow-client): set text node content before it is attached - #25683

Open
totally-not-ai[bot] wants to merge 2 commits into
mainfrom
fix/text-node-content-before-attach
Open

fix(flow-client): set text node content before it is attached#25683
totally-not-ai[bot] wants to merge 2 commits into
mainfrom
fix/text-node-content-before-attach

Conversation

@totally-not-ai

@totally-not-ai totally-not-ai Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Summary

A text node was created empty and only got its text later, when the reactive flush ran — after the node was already in the DOM. Now the text is set while the node is bound, so it is never inserted empty.

What changed

  • TextBindingStrategy (both the Java/GWT and the TypeScript client) now calls computation.recompute() right after registering the reactive computation, so the text is applied during bind() instead of during the later flush. This matches how element attributes, properties and styles are already handled.
  • Behavior change: the DOM text of a text node is now set before the node is attached, instead of after. This fixes web components that read their slotted content while they render (for example vaadin-badge since 25.2.7), which used to see an empty text node. Changing the data of a text node that is already assigned to a slot fires no slotchange, so there was no later chance to correct it. No public API changes; only the timing of the text update moves earlier.

Fixes #25679

Test summary

# Status What the test verifies Why it matters
1 A text-node child already carries its text at the moment it is inserted into its parent (checked by intercepting appendChild), in both the TS and the GWT client This is the actual fix; a later assert would still pass if the text arrived after insertion
2 The text is in place right after bind() returns, without any Reactive.flush() Pins that the binding itself applies the value, not the pending flush
3 Later text changes still reach the DOM on flush, and stop after the node is unregistered The eager apply must not break the normal reactive updates or the unbind
  • SimpleElementBindingStrategyChildrenTests → "inserts a text-node child with its text already in place": rows 1, 2
  • GwtBasicElementBinderTest.testAddTextNodeBeforeBind: rows 1, 2
  • TextBindingStrategyTests → "reactively binds the text property to the DOM node and unbinds on unregister" (extra assert added): rows 2, 3

Not tested on purpose: the real vaadin-badge slot behaviour, since it lives outside this repository; the client tests reproduce the same insertion order instead.

A text node was created empty and got its content from the reactive
flush, which runs after the node has been inserted into the DOM and
after its parent element has been attached. A web component that
resolves the state of its slotted content while it renders, such as
vaadin-badge since 25.2.7, then sees an empty text node, and changing
the data of a text node that is already assigned to a slot fires no
slotchange to correct that.

The content is now applied when the text node is bound, the same way
element attributes, properties and styles already are.

Fixes #25679
The GWT case only checked the text after the bind returned, which would
still pass if the content were applied after the insertion. Both cases
now record the text of every inserted child as it is at the moment of
the insertion.
@knoobie

knoobie commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

I'm wondering if this is a trivial bug fix or a fundamental behavior change in how flow and the client side interacts which has to be done at the next major.. locking at a comment from @web-padawan -- this might could affect a lot of components for better or worst 😅

@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

Test Results

 1 440 files  ±0   1 524 suites  ±0   1h 36m 53s ⏱️ + 2m 16s
12 062 tests +2  11 994 ✅ +2  68 💤 ±0  0 ❌ ±0 
12 380 runs  +2  12 312 ✅ +2  68 💤 ±0  0 ❌ ±0 

Results for commit d84f544. ± Comparison against base commit 980ac5b.

@totally-not-ai

Copy link
Copy Markdown
Contributor Author

@knoobie Fair question. Scope first, then why it matters less than it looks.

What changes at runtime. Only the moment the initial text of a text node is written: from "the next reactive flush" to "when the node is bound". Both happen inside the same synchronous handling of the same server message, so no component can see the text later than it does today — only earlier, or at the same point. Later text updates still go through the flush. No protocol change, no API change, no new configuration.

Why it is a consistency fix rather than a new rule. Attributes, properties, styles and class names are already applied while the element is bound (bindMap -> bindProperty(...).recompute(), bindClassList), which is why an element reaches the DOM with its attributes in place. Text was the only content applied after the element was already attached; now both happen at the same point.

The regression is already fixed on the web-components side. @vaadin/badge@25.2.11 and 25.3.0-beta3 add a characterData observer, and its code comment names the Flow pattern explicitly ("it appends an empty text node and sets its data later"). Measured against 25.2.11:

append empty text node -> connect -> write data   has-content=false at connect, true one microtask later
text node carries its data before connect        has-content=true at connect

So this is not on the critical path for the reported bug and does not need to ride a patch release. What it still buys is that a component which measures itself while it renders sees the final text — the reason syncInitial was introduced in the first place (vaadin/web-components#11891, grid auto-width) — instead of laying out empty and being corrected a microtask later.

I have left the code as it is, since I think the ordering belongs in the client engine rather than in every component. But it is a client-side timing change that touches every application, so if you would rather not move that in a patch line, holding this for the next major is fine by me — nothing in it needs to be split up to land later.

One triage note: #25679 was closed as a duplicate of vaadin/web-components#12663, so the link on this PR should be "Related to" rather than "Fixes".

@Artur-

Artur- commented Sep 11, 2026

Copy link
Copy Markdown
Member

This might be a good change or might not, I will let @Legioth decide that. It is not a fix for the linked issue though. A web component should not have timing issues so the web-components fix is probably the good fix for badge

@totally-not-ai

Copy link
Copy Markdown
Contributor Author

@Artur- Agreed — the web-components fix is the right fix for the badge, and this is not a fix for the linked issue. #25679 was closed as a duplicate of vaadin/web-components#12663, and the badge fix shipped in @vaadin/badge@25.2.11 / 25.3.0-beta3, so the link on this PR should read "Related to" rather than "Fixes". The badge observer is also worth keeping regardless of this change, since it covers callers other than Flow.

What is left here should be judged on its own: attributes, properties, styles and class names are already applied while an element is bound, so an element reaches the DOM with them in place. The initial text of a text node was the only content written afterwards, in the reactive flush that follows. This makes both happen at the same point, so a component that measures itself while it renders sees the final text instead of an empty text node.

@Legioth over to you. Happy to close this if moving that timing in the client engine is not something you want.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Badge Text not shown

2 participants