Skip to content

Commit

Permalink
fix(core): complete post-hydration cleanup in components that use Vie…
Browse files Browse the repository at this point in the history
…wContainerRef

Previously, if a component injects a `ViewContainerRef`, the post-hydration cleanup process doesn't visit inner views to cleanup dehydrated views in nested LContainers. This commit updates the logic to recognize this situation and enter host LView to complete cleanup.

Resolves angular#56989.
  • Loading branch information
AndrewKushnir committed Aug 8, 2024
1 parent 0761e9a commit 6d98c11
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 4 deletions.
13 changes: 9 additions & 4 deletions packages/core/src/hydration/cleanup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ function removeDehydratedView(dehydratedView: DehydratedContainerView, renderer:
*/
function cleanupLContainer(lContainer: LContainer) {
removeDehydratedViews(lContainer);

// The host could be an LView if this container is on a component node.
// In this case, descend into host LView for further cleanup. See also
// LContainer[HOST] docs for additional information.
const hostLView = lContainer[HOST];
if (isLView(hostLView)) {
cleanupLView(hostLView);
}

for (let i = CONTAINER_HEADER_OFFSET; i < lContainer.length; i++) {
cleanupLView(lContainer[i] as LView);
}
Expand Down Expand Up @@ -114,10 +123,6 @@ export function cleanupDehydratedViews(appRef: ApplicationRef) {
if (isLView(lNode)) {
cleanupLView(lNode);
} else {
// Cleanup in the root component view
const componentLView = lNode[HOST] as LView<unknown>;
cleanupLView(componentLView);

// Cleanup in all views within this view container
cleanupLContainer(lNode);
}
Expand Down
64 changes: 64 additions & 0 deletions packages/platform-server/test/hydration_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7771,6 +7771,70 @@ describe('platform-server hydration integration', () => {
verifyAllNodesClaimedForHydration(clientRootNode);
verifyClientAndSSRContentsMatch(ssrContents, clientRootNode);
});

it('should cleanup dehydrated views in routed components that use ViewContainerRef', async () => {
@Component({
standalone: true,
selector: 'cmp-a',
template: `
@if (isServer) {
<p>Server view</p>
} @else {
<p>Client view</p>
}
`,
})
class CmpA {
isServer = isPlatformServer(inject(PLATFORM_ID));
viewContainerRef = inject(ViewContainerRef);
}

const routes: Routes = [
{
path: '',
component: CmpA,
},
];

@Component({
standalone: true,
selector: 'app',
imports: [RouterOutlet],
template: `
<router-outlet />
`,
})
class SimpleComponent {}

const envProviders = [
{provide: PlatformLocation, useClass: MockPlatformLocation},
provideRouter(routes),
] as unknown as Provider[];
const html = await ssr(SimpleComponent, {envProviders});
const ssrContents = getAppContents(html);

expect(ssrContents).toContain(`<app ${NGH_ATTR_NAME}`);
expect(ssrContents).toContain('Server view');
expect(ssrContents).not.toContain('Client view');

resetTViewsFor(SimpleComponent, CmpA);

const appRef = await renderAndHydrate(doc, html, SimpleComponent, {envProviders});
const compRef = getComponentRef<SimpleComponent>(appRef);
appRef.tick();

await whenStable(appRef);

const clientRootNode = compRef.location.nativeElement;

// <p> tag is used in a view that is different on a server and
// on a client, so it gets re-created (not hydrated) on a client
const p = clientRootNode.querySelector('p');
verifyAllNodesClaimedForHydration(clientRootNode, [p]);

expect(clientRootNode.innerHTML).not.toContain('Server view');
expect(clientRootNode.innerHTML).toContain('Client view');
});
});
});
});

0 comments on commit 6d98c11

Please sign in to comment.