Skip to content

fix(angular-server): fallback to scoped+annotations when SSR DOM lacks attachShadow#30967

Open
SebastianKohler wants to merge 1 commit into
ionic-team:mainfrom
SebastianKohler:fix/angular-server-domino-shadow-fallback
Open

fix(angular-server): fallback to scoped+annotations when SSR DOM lacks attachShadow#30967
SebastianKohler wants to merge 1 commit into
ionic-team:mainfrom
SebastianKohler:fix/angular-server-domino-shadow-fallback

Conversation

@SebastianKohler

Copy link
Copy Markdown

Issue number: resolves #29751

What is the current behavior?

In Angular SSR environments using a server DOM that does not implement native attachShadow (for example Domino), Ionic server hydration can fail with:

TypeError: this.attachShadow is not a function

This occurs in the server hydrate path used by @ionic/angular-server when calling hydrateDocument(...) with the current default behavior.

What is the new behavior?

This PR adds a capability check in packages/angular-server/src/ionic-server-module.ts and applies a conditional fallback only when attachShadow is unavailable:

  • if attachShadow is supported:
    • keep existing behavior (clientHydrateAnnotations: false)
  • if attachShadow is not supported:
    • use serializeShadowRoot: 'scoped'
    • use clientHydrateAnnotations: true

This keeps the current behavior for environments with native attachShadow, while providing a compatible SSR path for Domino-like environments.

Does this introduce a breaking change?

  • Yes
  • No

Other information

The change is intentionally scoped to @ionic/angular-server integration logic and does not modify component behavior or the existing excludeComponents list.

I could not run the full Ionic test/lint suite locally in my current Windows environment setup, so this PR relies on CI for full validation.

Note: this PR addresses the attachShadow SSR crash path in @ionic/angular-server by applying a conditional fallback.

From upstream investigation, there is also a Stencil runtime fix in >= 4.40.0 related to non-shadow component patching (children/childNodes) that may affect Angular client hydration behavior. So full end-to-end resolution of all symptoms may also depend on the Stencil bump tracked separately.

@SebastianKohler
SebastianKohler requested a review from a team as a code owner February 23, 2026 13:40
@SebastianKohler
SebastianKohler requested a review from gnbm February 23, 2026 13:40
@vercel

vercel Bot commented Feb 23, 2026

Copy link
Copy Markdown

@SebastianKohler is attempting to deploy a commit to the Ionic Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added the package: angular @ionic/angular package label Feb 23, 2026
@SebastianKohler

Copy link
Copy Markdown
Author

@gnbm Are there any plans from the Ionic team to take a look at this PR? I see you are hard at work developing Angular 21 och 22 support for Ionic v9 due in Q3 2026, but does that include Angular SSR support? Angular SSR has not worked in Ionic since v8.2.2, which this PR attempts to fix.

Btw, I love the fact that your focus for v9 is to "keep up" rather than implement new features 👍👍

@ShaneK

ShaneK commented Jul 9, 2026

Copy link
Copy Markdown
Member

Hey @SebastianKohler! Thanks for the PR. It looks fine, but a couple of concerns:

@SebastianKohler

Copy link
Copy Markdown
Author

I tested the PR fallback against Ionic 8.8.13, which bundles Stencil 4.43.5. The server-side attachShadow error is gone and Stencil produces rendered SSR output. However, the browser test exposed a remaining Angular hydration failure:

TypeError: e.hasAttribute is not a function

The minified function maps to Angular’s hasSkipHydrationAttrOnRElement. Stencil’s clientHydrateAnnotations: true inserts a <!--r.*--> marker before <ion-button>, which Angular expects to hydrate as the next element. Angular encounters the comment instead.

Stencil 4.40+ contains the fix for the related non-shadow DOM traversal issue (the current test confirms ion-app.children.length === 1), but it does not resolve this Angular/Stencil hydration-marker conflict.

A useful regression test would need to load the SSR result with Angular client hydration, assert that there are no page/console errors, and verify that the <ion-button> host is no longer hidden and contains exactly one native control in its shadow root. That test currently fails with the PR.

I published the exact verification setup here: https://github.com/SebastianKohler/ionic-angular-ssr-attachShadow-typeError/tree/codex/verify-pr-30967. It pins Ionic 8.8.13, which bundles Stencil 4.43.5, and applies the PR implementation locally. SSR completes successfully, but Angular client hydration still fails because Stencil’s annotations alter the expected node order.

I’m sharing this reproduction and diagnosis so the Ionic team has a concrete starting point. I won’t be able to continue investigating the Angular/Stencil hydration interaction or develop the framework-level fix. Given the remaining client-side failure, I don’t think this PR can currently be considered a complete resolution of #29751. I’ll leave it to the maintainers to decide whether the server-side fallback has value independently or whether the PR should be closed.

@SebastianKohler

Copy link
Copy Markdown
Author

One important clarification to my previous comment: Angular client hydration is optional.

I repeated the test after removing provideClientHydration() from app.module.ts. With the same PR fallback on Ionic 8.8.13/Stencil 4.43.5, the application then works end-to-end without server or browser errors.

Without Angular client hydration, Angular replaces the server-rendered DOM during client bootstrap instead of reconciling it. Consequently, Angular does not encounter the Stencil hydration markers that caused the failure described in my previous comment.

This means the PR appears to be a working fix for Angular SSR applications that do not enable provideClientHydration(). The remaining marker conflict is specific to applications using Angular client hydration.

The original reproduction has always included provideClientHydration(), but my earlier testing with Ionic 8.2.2 only verified the absence of the server-side attachShadow error. I did not verify whether Angular client hydration worked in that version, so I cannot characterize the browser failure as a regression from 8.2.2.

Therefore, if #29751 is scoped to the server-side error and SSR without Angular client hydration, the PR appears to resolve it. If Ionic intends to support Angular client hydration as well, that path still requires additional work.

@DavidBowdoin

DavidBowdoin commented Jul 15, 2026

Copy link
Copy Markdown

We derived a patch from this PR, that we use in production. Including it here in case it helps move this along. I really hope SSR works out of the box in Ionic v9 👍

package.json

  "overrides": {
    "@stencil/core": "4.43.5"
  }
// import { IonicServerModule } from '@ionic/angular-server';
import { IonicServerScopedModule } from './patch/ionic-server-scoped.module';

in Angular 21, we had

    provideClientHydration(
      withEventReplay(),
      withIncrementalHydration(),
    ),

But in Angular 22, those are defaulted, so we simplified it to

    provideClientHydration(),

The page does flicker between ssr to the hydrated version, but I suspect that is something in our own code that I need to dig into.

@SebastianKohler

Copy link
Copy Markdown
Author

@DavidBowdoin Is your browser console clean when Angular client hydration runs? In particular, do you see a TypeError: e.hasAttribute is not a function error?

@DavidBowdoin

DavidBowdoin commented Jul 20, 2026

Copy link
Copy Markdown

@SebastianKohler Yes, both the browser console, and the node express console, are clean.
https://athletic.tv/ Angular 22, SSR, Ionic

@ShaneK ShaneK left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I tested this manually and I think I found an issue, please let me know if I'm off base here

? { clientHydrateAnnotations: false }
: {
serializeShadowRoot: 'scoped',
clientHydrateAnnotations: true,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
clientHydrateAnnotations: true,
clientHydrateAnnotations: false,

I think this should be false rather than true. I ran renderToString on Stencil 4.43.5 (what main ships) with both values: with true the serializer injects <!--r.1-->/<!--r.2--> root hydrate markers as siblings right before <ion-button>, which are the exact nodes traced to Angular client hydration throwing hasAttribute is not a function on Angular 18. With false those markers are gone, and there's still only one native <button>, so the duplicate-internals symptom you originally needed true to avoid doesn't come back on 4.43.5 (that was a pre-4.40 Stencil bug that's since been fixed).

So false gives no server crash, no duplicate internals, and no hydration-breaking markers, which should work with provideClientHydration() across Angular versions rather than only without it. One caveat: I verified this at the Stencil serialization layer in Node, not with a live Angular hydration runtime, so it'd be worth confirming in the ng18 repro with provideClientHydration() enabled before merging.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks — I tested the suggested clientHydrateAnnotations: false change in a live Angular 18 client-hydration runtime, rather than only at the Stencil serialization layer.

The setup uses Angular 18.2.14 with provideClientHydration(), Ionic 8.8.13/Stencil 4.43.5, serializeShadowRoot: 'scoped', and no ngSkipHydration.

At the serialization layer, my result agrees with yours:

  • SSR completes without the attachShadow error.
  • The <!--r.*--> markers are gone.
  • The serialized HTML contains exactly one native <button>.

However, the end-to-end browser result still fails:

  • Angular throws TypeError: s.hasAttribute is not a function.
  • An empty comment node still precedes <ion-button> inside <ion-app>.
  • <ion-button> remains hidden.
  • After client bootstrap, there is one button.button-native in the light DOM and another in the shadow root.

So the duplicate internals are not visible in the serialized HTML; the second native control is created during Stencil client bootstrap. A serializer-only button count does not catch that.

The exact verification setup is here: https://github.com/SebastianKohler/ionic-angular-ssr-attachShadow-typeError/tree/codex/verify-annotations-false-ng18.

I also investigated the Angular 22 application linked by @DavidBowdoin. Its current SSR HTML contains <ion-app ngskiphydration> while retaining Stencil’s <!--r.*--> markers. Angular therefore skips hydration for the entire Ionic subtree and recreates it on the client. That explains why its console can remain clean and may also explain the reported flicker; it does not exercise Angular hydration of the Stencil-annotated Ionic subtree.

Based on the Angular 18 runtime test, changing the value to false does not appear to resolve Angular client hydration end-to-end.

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

Labels

package: angular @ionic/angular package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: elm.attachShadow is not a function TypeError in Angular SSR app since Ionic v8.2.4

3 participants