Skip to content

Commit 77f1ca0

Browse files
splincodeatscott
authored andcommitted
fix(core): handle missing serialized container hydration data
Simplify the hydration regression test by removing conditional early-return branches and relying on direct Jasmine expectations while keeping strict typing and OnPush configuration.
1 parent 2ff9db3 commit 77f1ca0

2 files changed

Lines changed: 69 additions & 7 deletions

File tree

packages/core/src/linker/view_container_ref.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -773,16 +773,23 @@ function populateDehydratedViewsInLContainerImpl(
773773
const currentRNode: RNode | null = getSegmentHead(hydrationInfo, noOffsetIndex);
774774

775775
const serializedViews = hydrationInfo.data[CONTAINERS]?.[noOffsetIndex];
776-
ngDevMode &&
777-
assertDefined(
778-
serializedViews,
779-
'Unexpected state: no hydration info available for a given TNode, ' +
780-
'which represents a view container.',
781-
);
776+
if (serializedViews === undefined) {
777+
ngDevMode &&
778+
console.warn(
779+
'Unexpected state: no hydration info available for a given TNode, ' +
780+
'which represents a view container.',
781+
);
782+
783+
// This ViewContainerRef was created for an element through a query
784+
// (for example `viewChild(..., {read: ViewContainerRef})`) and there
785+
// is no corresponding serialized container data in hydration metadata.
786+
// Fall back to creation mode and insert an anchor on demand.
787+
return false;
788+
}
782789

783790
const [commentNode, dehydratedViews] = locateDehydratedViewsInContainer(
784791
currentRNode!,
785-
serializedViews!,
792+
serializedViews,
786793
);
787794

788795
if (ngDevMode) {

packages/core/test/acceptance/query_spec.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,19 @@ import {
3030
} from '../../src/core';
3131
import {TestBed} from '../../testing';
3232
import {By} from '@angular/platform-browser';
33+
import {
34+
createContainerRef,
35+
enableLocateOrCreateContainerRefImpl,
36+
} from '../../src/linker/view_container_ref';
37+
import {getLContext} from '../../src/render3/context_discovery';
38+
import {DehydratedView} from '../../src/hydration/interfaces';
39+
import {
40+
TElementContainerNode,
41+
TElementNode,
42+
TContainerNode,
43+
TNodeType,
44+
} from '../../src/render3/interfaces/node';
45+
import {HYDRATION, TVIEW} from '../../src/render3/interfaces/view';
3346

3447
describe('query logic', () => {
3548
beforeEach(() => {
@@ -1762,6 +1775,48 @@ describe('query logic', () => {
17621775
expect(qList.first).toBeInstanceOf(ViewContainerRef);
17631776
});
17641777

1778+
it('should not throw when hydration metadata has no serialized container data', () => {
1779+
@Component({
1780+
template: `<div #foo></div>`,
1781+
changeDetection: ChangeDetectionStrategy.OnPush,
1782+
})
1783+
class TestCmp {}
1784+
1785+
const fixture = TestBed.createComponent(TestCmp);
1786+
fixture.detectChanges();
1787+
1788+
const element = fixture.nativeElement.querySelector('div')!;
1789+
const context = getLContext(element)!;
1790+
const hostLView = context.lView!;
1791+
const candidateTNode = hostLView[TVIEW].data[context.nodeIndex] as unknown;
1792+
1793+
expect(candidateTNode).toBeDefined();
1794+
expect(!!candidateTNode && typeof candidateTNode === 'object' && 'type' in candidateTNode)
1795+
.withContext('Expected a TNode with a type field.')
1796+
.toBeTrue();
1797+
1798+
const hostTNode = candidateTNode as TElementNode | TContainerNode | TElementContainerNode;
1799+
expect(!!(hostTNode.type & (TNodeType.AnyContainer | TNodeType.AnyRNode)))
1800+
.withContext('Expected a host TNode that can create a ViewContainerRef.')
1801+
.toBeTrue();
1802+
1803+
hostLView[HYDRATION] = {data: {}, firstChild: null} as DehydratedView;
1804+
enableLocateOrCreateContainerRefImpl();
1805+
1806+
const warnSpy = spyOn(console, 'warn');
1807+
1808+
expect(() => createContainerRef(hostTNode, hostLView)).not.toThrow();
1809+
1810+
const warningMessages = warnSpy.calls
1811+
.allArgs()
1812+
.map(([message]) => String(message))
1813+
.join('\n');
1814+
1815+
expect(warningMessages).toContain(
1816+
'Unexpected state: no hydration info available for a given TNode, which represents a view container.',
1817+
);
1818+
});
1819+
17651820
it('should read ElementRef with a native element pointing to comment DOM node from ng-template', () => {
17661821
@Component({
17671822
template: `<ng-template #foo></ng-template>`,

0 commit comments

Comments
 (0)