Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Dec 3, 2025

Fix data-permanent to preserve element attributes, not just descendants

  • You've read the Contributor Guide and Code of Conduct.
  • You've included unit or integration tests for your change, where applicable.
  • You've included inline docs for your change, where applicable.
  • There's an open issue for the PR that you are making. If you'd like to propose a new feature or change, please open an issue to discuss the change or find an existing issue.

Description

The data-permanent attribute was only preventing child content synchronization during enhanced navigation, but still synchronized attributes on the element itself. This caused JS-modified attributes (like dynamically added classes) to be lost.

<!-- Before: expand class lost after enhanced nav -->
<div id="myelem" data-permanent></div>
<script>document.getElementById('myelem').classList.add('expand')</script>

<!-- Workaround required wrapping in another element -->
<div data-permanent><div id="myelem"></div></div>

Changes:

  • Move the isDataPermanentElement check in treatAsMatch() to occur before attribute synchronization, skipping both attribute and content sync for permanent elements
  • Add unit tests for attribute preservation on data-permanent elements, including dynamically added attributes
  • Add E2E test (ElementsWithDataPermanentAttribute_HavePreservedAttributes) to verify dynamically added CSS classes are preserved after enhanced navigation
  • Extend PageThatPreservesContent.razor test component to include attribute preservation scenario

Fixes #51021

Original prompt

This section details on the original issue you should resolve

<issue_title>data-permanent doesn't apply to the element with that attribute</issue_title>
<issue_description>I was trying out data-permanent and was confused for a while because it seemed not to work. Then I tried moving it to an ancestor element and it did work.

Repro

  • Start with <div id='myelem' data-permanent></div>
  • Use JS to mutate it in some way (e.g., document.getElementById('myelem').classList.toggle('expand'))
  • Perform an enhanced nav that returns equivalent content (i.e., <div id='myelem' data-permanent></div>)

Expected: The expand class should be retained
Actual: It is not

However if you replace the markup with <div data-permanent><div id='myelem'></div></div> then the class is retained

I guess diffing only uses data-permanent to decide whether to recurse into the element, but I think it should also use it to decide whether to synchronize the attributes on that element.</issue_description>

Comments on the Issue (you are @copilot in this section)

@SteveSandersonMS It is just about entire elements (and all their descendants), i.e., ensuring they are not modified across enhance page loads.

I know that doesn't cover the specific scenario you mention (preserving specific attributes). That would require broader, nontrivial design to give developers ways to express everything possible there. Maybe it would be something more like a callback to let developers intercept the before/after states during DOM merging, so they could use any logic they wanted to capture "before" state and re-apply it to the "after" state. It would be a very reasonable feature request for .NET 9.</comment_new>
<comment_new>@danroth27

I'm confused about this feature in general. Where is the specification?

@marinasundstrom Take a look at #50437 for a description of data-permanent

@ Thanks for contacting us.

We're moving this issue to the .NET 9 Planning milestone for future evaluation / consideration. We would like to keep this around to collect more feedback, which can help us with prioritizing this work. We will re-evaluate this issue, during our next planning meeting(s).
If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues.
To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.</comment_new>
<comment_new>@
Thanks for contacting us.

We're moving this issue to the .NET 9 Planning milestone for future evaluation / consideration. We would like to keep this around to collect more feedback, which can help us with prioritizing this work. We will re-evaluate this issue, during our next planning meeting(s).
If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues.
To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.</comment_new>
<comment_new>@SteveSandersonMS
Another vote in favour of fixing this: #54658 (need to preserve content in <head> without having multiple heads).</comment_new>


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix data-permanent not applying to element with that attribute Fix data-permanent to preserve element attributes, not just descendants Dec 3, 2025
Copilot AI requested a review from javiercn December 3, 2025 09:12
Copilot finished work on behalf of javiercn December 3, 2025 09:12
@javiercn
Copy link
Member

javiercn commented Dec 3, 2025

@copilot Have you followed the steps in src/Components/AGENTS.MD?

Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Dec 3, 2025

@copilot Have you followed the steps in src/Components/AGENTS.MD?

Yes, I've now followed the steps in AGENTS.MD. In commit 68284e42, I:

  1. Extended the existing test component PageThatPreservesContent.razor to include an attribute preservation scenario (adding a dynamically added class to a data-permanent element)
  2. Added an E2E test ElementsWithDataPermanentAttribute_HavePreservedAttributes that verifies dynamically added attributes are preserved after enhanced navigation
  3. Built and ran the E2E tests - all pass including the new test and existing data-permanent tests

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • accounts.google.com
    • Triggering command: /proc/self/exe /proc/self/exe --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=none --no-sandbox --ignore-certificate-errors --use-angle=swiftshader-webgl --ignore-certificate-errors --crashpad-handler-pid=9373 --enable-crash-reporter=, --noerrdialogs --user-data-dir=/tmp/BrowserFixtureUserProfiles/streaming --change-stack-guard-on-fork=enable --shared-files=v8_context_snapshot_data:100 --field-trial-handle=3,i,1075644145285363203,7250468106450785236,262144 --disable-features=IgnoreDuplicateNavs,PaintHolding,Prewarm --variations-seed-version --trace-process-track-uuid=3190708989122997041 --enable-logging=stderr (dns block)
    • Triggering command: /opt/google/chrome/chrome /opt/google/chrome/chrome --allow-pre-commit-input --disable-REDACTED-networking --disable-client-side-phishing-detection --disable-default-apps --disable-features=IgnoreDuplicateNavs,Prewarm --disable-hang-monitor --disable-popup-blocking --disable-prompt-on-repost --disable-sync --enable-automation --enable-logging=stderr --headless=new --ignore-certificate-errors --log-level=0 --no-first-run --no-sandbox --no-service-autorun --password-store=basic --remote-debugging-port=0 (dns block)
    • Triggering command: /proc/self/exe /proc/self/exe --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=none --no-sandbox --ignore-certificate-errors --use-angle=swiftshader-webgl --ignore-certificate-errors --crashpad-handler-pid=9597 --enable-crash-reporter=, --noerrdialogs --user-data-dir=/tmp/BrowserFixtureUserProfiles/streaming --change-stack-guard-on-fork=enable --shared-files=v8_context_snapshot_data:100 --field-trial-handle=3,i,3234065746608267733,13207032037754184699,262144 --disable-features=IgnoreDuplicateNavs,PaintHolding,Prewarm --variations-seed-version --trace-process-track-uuid=3190708989122997041 --enable-logging=stderr (dns block)
  • clients2.google.com
    • Triggering command: /proc/self/exe /proc/self/exe --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=none --no-sandbox --ignore-certificate-errors --use-angle=swiftshader-webgl --ignore-certificate-errors --crashpad-handler-pid=9373 --enable-crash-reporter=, --noerrdialogs --user-data-dir=/tmp/BrowserFixtureUserProfiles/streaming --change-stack-guard-on-fork=enable --shared-files=v8_context_snapshot_data:100 --field-trial-handle=3,i,1075644145285363203,7250468106450785236,262144 --disable-features=IgnoreDuplicateNavs,PaintHolding,Prewarm --variations-seed-version --trace-process-track-uuid=3190708989122997041 --enable-logging=stderr (dns block)
    • Triggering command: /opt/google/chrome/chrome /opt/google/chrome/chrome --allow-pre-commit-input --disable-REDACTED-networking --disable-client-side-phishing-detection --disable-default-apps --disable-features=IgnoreDuplicateNavs,Prewarm --disable-hang-monitor --disable-popup-blocking --disable-prompt-on-repost --disable-sync --enable-automation --enable-logging=stderr --headless=new --ignore-certificate-errors --log-level=0 --no-first-run --no-sandbox --no-service-autorun --password-store=basic --remote-debugging-port=0 (dns block)
    • Triggering command: /proc/self/exe /proc/self/exe --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=none --no-sandbox --ignore-certificate-errors --use-angle=swiftshader-webgl --ignore-certificate-errors --crashpad-handler-pid=9597 --enable-crash-reporter=, --noerrdialogs --user-data-dir=/tmp/BrowserFixtureUserProfiles/streaming --change-stack-guard-on-fork=enable --shared-files=v8_context_snapshot_data:100 --field-trial-handle=3,i,3234065746608267733,13207032037754184699,262144 --disable-features=IgnoreDuplicateNavs,PaintHolding,Prewarm --variations-seed-version --trace-process-track-uuid=3190708989122997041 --enable-logging=stderr (dns block)
  • www.google.com
    • Triggering command: /proc/self/exe /proc/self/exe --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=none --no-sandbox --ignore-certificate-errors --use-angle=swiftshader-webgl --ignore-certificate-errors --crashpad-handler-pid=9373 --enable-crash-reporter=, --noerrdialogs --user-data-dir=/tmp/BrowserFixtureUserProfiles/streaming --change-stack-guard-on-fork=enable --shared-files=v8_context_snapshot_data:100 --field-trial-handle=3,i,1075644145285363203,7250468106450785236,262144 --disable-features=IgnoreDuplicateNavs,PaintHolding,Prewarm --variations-seed-version --trace-process-track-uuid=3190708989122997041 --enable-logging=stderr (dns block)
    • Triggering command: /opt/google/chrome/chrome /opt/google/chrome/chrome --allow-pre-commit-input --disable-REDACTED-networking --disable-client-side-phishing-detection --disable-default-apps --disable-features=IgnoreDuplicateNavs,Prewarm --disable-hang-monitor --disable-popup-blocking --disable-prompt-on-repost --disable-sync --enable-automation --enable-logging=stderr --headless=new --ignore-certificate-errors --log-level=0 --no-first-run --no-sandbox --no-service-autorun --password-store=basic --remote-debugging-port=0 (dns block)
    • Triggering command: /proc/self/exe /proc/self/exe --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=none --no-sandbox --ignore-certificate-errors --use-angle=swiftshader-webgl --ignore-certificate-errors --crashpad-handler-pid=9597 --enable-crash-reporter=, --noerrdialogs --user-data-dir=/tmp/BrowserFixtureUserProfiles/streaming --change-stack-guard-on-fork=enable --shared-files=v8_context_snapshot_data:100 --field-trial-handle=3,i,3234065746608267733,13207032037754184699,262144 --disable-features=IgnoreDuplicateNavs,PaintHolding,Prewarm --variations-seed-version --trace-process-track-uuid=3190708989122997041 --enable-logging=stderr (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot finished work on behalf of javiercn December 3, 2025 10:09
@javiercn javiercn added the area-blazor Includes: Blazor, Razor Components label Dec 3, 2025
@javiercn javiercn marked this pull request as ready for review December 3, 2025 15:58
@javiercn javiercn requested a review from a team as a code owner December 3, 2025 15:58
Copilot AI review requested due to automatic review settings December 3, 2025 15:58
Copilot finished reviewing on behalf of javiercn December 3, 2025 16:02
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a bug where the data-permanent attribute only prevented child content synchronization during enhanced navigation, but still synchronized attributes on the element itself. The fix moves the isDataPermanentElement check to occur before attribute synchronization, ensuring both attributes and content are preserved for elements marked with data-permanent.

Key Changes:

  • Modified TypeScript logic to skip all synchronization (attributes and content) for data-permanent elements
  • Added unit tests verifying attribute preservation on data-permanent elements
  • Added E2E test confirming dynamically added CSS classes survive enhanced navigation

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
src/Components/Web.JS/src/Rendering/DomMerging/DomSync.ts Moved isDataPermanentElement check before attribute synchronization to preserve both attributes and content for permanent elements
src/Components/Web.JS/test/DomSync.test.ts Added two unit tests verifying attributes are preserved: one for existing attributes and one for dynamically added attributes
src/Components/test/E2ETest/ServerRenderingTests/EnhancedNavigationTest.cs Added E2E test verifying dynamically added CSS class is preserved across multiple enhanced navigation events
src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/EnhancedNav/PageThatPreservesContent.razor Extended test page to include element for attribute preservation testing with dynamically added CSS class

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

Labels

area-blazor Includes: Blazor, Razor Components Attention: Shared Code Modified

Projects

None yet

Development

Successfully merging this pull request may close these issues.

data-permanent doesn't apply to the element with that attribute

2 participants