angular: Rehydration doesn't work with throw error `el.setAttribute is not a function` in Angular 16 rc.1

Which @angular/* package(s) are the source of the bug?

core

Is this a regression?

No

Description

When we enable provideClientHydration(withNoHttpTransferCache()) option in AppModule and check ssr hydration result, I got some error which is unhandled by NG0XXX error code.

I don’t know how to reproduce this error in stackblitz yet.

But hope it could be helpful if we provide callstack for error message first. Here is the error callstack.

Please provide a link to a minimal reproduction of the bug

https://github.com/leo6104/ng-rehydrate-error-repro

Please provide the exception or error you saw

re.mjs:10057 ERROR TypeError: el.setAttribute is not a function
    at EmulatedEncapsulationDomRenderer2.setAttribute (platform-browser.mjs:592:16)
    at BaseAnimationRenderer.setAttribute (animations.mjs:270:23)
    at setUpAttributes (core.mjs:1020:26)
    at setupStaticAttributes (core.mjs:7535:9)
    at ɵɵelementStart (core.mjs:15088:5)
    at _a5_ng_container_0_Template (_a5.js:1:1)
    at ReactiveLViewConsumer.runInContext (core.mjs:10311:13)
    at executeTemplate (core.mjs:11069:18)
    at renderView (core.mjs:10889:13)
    at TemplateRef2.createEmbeddedViewImpl (core.mjs:22766:9)
h

another error case (same but little more detail callstack)

handled Promise rejection: el.setAttribute is not a function ; Zone: <root> ; Task: Promise.then ; Value: TypeError: el.setAttribute is not a function
    at NoneEncapsulationDomRenderer.setAttribute (platform-browser.mjs:592:16)
    at BaseAnimationRenderer.setAttribute (animations.mjs:270:23)
    at setUpAttributes (core.mjs:1020:26)
    at setupStaticAttributes (core.mjs:7535:9)
    at ɵɵelementStart (core.mjs:15088:5)
    at _a5_ng_container_0_Template (template.html:6:5)
    at ReactiveLViewConsumer.runInContext (core.mjs:10311:13)
    at executeTemplate (core.mjs:11069:18)
    at renderView (core.mjs:10889:13)
    at TemplateRef2.createEmbeddedViewImpl (core.mjs:22766:9) 

TypeError: el.setAttribute is not a function
    at NoneEncapsulationDomRenderer.setAttribute (https://.../chunk-SET2APWZ.js:16109:14)
    at BaseAnimationRenderer.setAttribute (https://.../chunk-XDNB56EC.js:4246:23)
    at setUpAttributes (https://.../chunk-YTYK4I53.js:4664:18)
    at setupStaticAttributes (https://.../chunk-YTYK4I53.js:7713:5)
    at ɵɵelementStart (https://.../chunk-YTYK4I53.js:11031:3)
    at _a5_ng_container_0_Template (ng:///_a5.js:10:5)
    at ReactiveLViewConsumer.runInContext (https://.../chunk-YTYK4I53.js:17559:11)
    at executeTemplate (https://.../chunk-YTYK4I53.js:9029:14)
    at renderView (https://.../chunk-YTYK4I53.js:8884:7)
    at TemplateRef2.createEmbeddedViewImpl (https://.../chunk-YTYK4I53.js:18567:9)
a

Please provide the environment you discovered this bug in (run ng version)

Angular CLI: 16.0.0-rc.0
Node: 16.19.0
Package Manager: npm 8.19.3
OS: darwin x64

Angular: 16.0.0-rc.1
... animations, common, compiler, compiler-cli, core, elements
... forms, localize, platform-browser, platform-browser-dynamic
... platform-server, router, service-worker

Package                         Version
---------------------------------------------------------
@angular-devkit/architect       0.1600.0-rc.0
@angular-devkit/build-angular   16.0.0-rc.0
@angular-devkit/core            16.0.0-rc.0
@angular-devkit/schematics      16.0.0-rc.0
@angular/bazel                  15.2.6
@angular/cdk                    16.0.0-rc.0
@angular/cli                    16.0.0-rc.0
@angular/material               16.0.0-rc.0
@angular/youtube-player         16.0.0-next.2
@nguniversal/builders           16.0.0-next.0
@nguniversal/common             16.0.0-next.0
@nguniversal/express-engine     16.0.0-next.0
@schematics/angular             16.0.0-rc.0
ng-packagr                      16.0.0-rc.0
rxjs                            7.8.0
typescript                      5.0.3

Anything else?

the component html markup is here and got error in *ngIf="d.header" condition.

<ng-container *ngLet="{
  fullWidth: fullWidth$ | async,
  header: header$ | async
} as d">
  <div class="board-section" [class.full-width]="d.fullWidth" cdkScrollable>
    <global-navigation-bar *ngIf="d.header"></global-navigation-bar>
    <div class="body-container" [class.show-header]="d.header">
      <router-outlet></router-outlet>
    </div>
    <km-footer />
  </div>
</ng-container>
<ng-template #fabWrapperContainer></ng-template>

root-cmp decorator

@Component({
  selector: 'root-cmp',
  templateUrl: './root-cmp.component.html',
  styleUrls: [
    './root-cmp.component.scss'
  ],
  host: {
    toastContainer: 'true' // ToastContainerDirective,
  },
})

and header$ variable use ngrx route data

  header$ = this.store.select(selectRouteData).pipe(
    map(({ header }) => !header || header !== 'hide'),
    distinctUntilChanged(),
  );

ngLet directive


interface LetContext<T> {
  ngLet: T;
}

@Directive({
  selector: '[ngLet]',
  standalone: true,
})
export class LetDirective<T> {
  private _context: LetContext<T> = { ngLet: null };

  constructor(_viewContainer: ViewContainerRef, _templateRef: TemplateRef<LetContext<T>>) {
    _viewContainer.createEmbeddedView(_templateRef, this._context);
  }

  @Input()
  set ngLet(value: T) {
    this._context.ngLet = value;
  }
}

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Comments: 17 (16 by maintainers)

Commits related to this issue

Most upvoted comments

@AndrewKushnir Thanks for suggestion!

We made first-version test util and it works well. Its error message shows all child components hydration mismatch. It makes we find the root cause component of hydration error.

  beforeEach(() => {
    resetNgDevMode();
    if (getPlatform()) destroyPlatform();
  });

  fit('should pass `list-item[withImage]` tag', async () => {
    @Component({
      standalone: true,
      selector: 'app',
      template: '<list-item withImage></list-item>',
      imports: [
        ListItemWithImage,
      ],
    })
    class SimpleComponent {
    }

    const { verifyHydrate } = await assertionsForHydration(SimpleComponent, []);
    expect(verifyHydrate).not.toThrow();
  });

[Result] image

With more investigation, i will make new feature-request issue in next week and share brief concept for these assertionsForHydration utils.

@leo6104 thanks for the reply and for all your help to investigate the problems! 👍

However, its difference between two tsconfig files shows \n text node hydration error and Developer cannot expect the error came from preserveWhitespaces option. 😅 It could be the point angular can improve.

Yes, that is a quite tricky case. We will look into improving the detection for such cases.

BTW, it would be nice if angular team provide some test utils for hydration.

Thanks for the feedback.

Testing utilities for applications that use hydration is something that we’d like to explore and collect more feedback about. Could you please open a new ticket (a feature request) and describe a solution that would work for your application and test scenarios? We’ll also use the ticket to collect the feedback from the community to capture more use-cases that we’d need to take into account.

@AndrewKushnir Actually, there is no special reason we putting preserveWhitespaces: true option. It was preserved by legacy. We have been use angular for 6 years (migrated from angularjs in 2016) and at some moment tsconfig.json included preserveWhitespaces: true option. (maybe it was default option.)

when we setup initial SSR part few years ago, we want to reduce html size so we just turn off preserveWhitespaces in tsconfig.server.json to remove all whitespaces in prerendered html. (without changing preserveWhitespaces in tsconfig.app.json)

It was the reason we have mismatch value between tsconfig.app.json and tsconfig.server.json

At this moment, we can turn off preserveWhitespaces. it is no problem.

However, its difference between two tsconfig files shows \n text node hydration error and Developer cannot expect the error came from preserveWhitespaces option. 😅 It could be the point angular can improve.

BTW, it would be nice if angular team provide some test utils for hydration. We have lots of experience in Angular usage so we can build ssr-dev-server and can start hydration feature. But it was like end-to-end hydration testing process by human. We want to be more smarter and save times to testing hydration in the future. If the hydration testing util provided, there is no need to implement ssr-dev-server. I saw your PR and its hydration test util is awesome. we are trying to build similiar hydration test code in our project. we can open our hydration testing utils for public lib (if we succeed to make it) It can make our project sustainable and trustful. And i expect that easy to find unhandled case from angular internal logic.

@leo6104 thanks for the reply.

There is a chance that this issue was caused by a special logic to handle empty text nodes during hydration. I’ve put together some tests and can reproduce a similar (but not exactly the same) problem, see https://github.com/angular/angular/pull/49877. I’ve also put together a possible fix and I wanted to ask if you could use the following @angular/core package to test the behavior:

...
 "@angular/core": "https://output.circle-artifacts.com/output/job/90bae8de-aa9f-4cfa-aced-faf3895b07a0/artifacts/0/angular/core-pr49877-c21878ec3a.tgz",
...

Note: the PR does not include the other fix discussed above, please let me know if this is a problem for testing, I can create a temporary PR with a combination of both fixes.

If you can give us some hints to reproduce it, please let us know the information.

It’d be great if you could check the following use-cases and let us know the observed behavior:

  • What happens when you replace a text interpolation with just some static text in a template?
  • Does it make any difference if you do not have spaces before and after interpolation, i.e. <div>{{test}}</div>?
  • Do you see a different behavior when you wrap text interpolation with extra symbols like this:
<div>
  --{{test}}--
</div>

It’d still be very helpful to have a repro, so that we can perform further investigation.

Thank you.

@leo6104 thanks a lot for the reproduction! This helped to identify the root cause: the problem was triggered by an issue in the internal Angular logic that collects root nodes in a given view. The logic was not taking into account ViewContainerRef’s anchor (comment) nodes, thus returning incorrect output (less DOM nodes). Hydration logic relies on the number of root nodes in a view to properly split the DOM into segments (which belong to a particular view), thus the logic incorrectly identified the end of a segment.

The problem should be fixed by https://github.com/angular/angular/pull/49867. The PR would go through the usual review and test process before the merge, but you can test the fix by using the core package from the PR. You can update the package.json as shown below and run npm install (you may need to use --force to skip version conflicts):

  ...
  "@angular/core": "https://output.circle-artifacts.com/output/job/c6a99b5b-02c5-4094-9c28-be5eee45915c/artifacts/0/angular/core-pr49867-8af03dfa43.tgz",
  ...

Please let me know if this resolves the problem.

Thank you.