refactor(core): prevent annotating in hydrate never blocks (#58328)

This fixes an issue with hydrate never where jsaction annotations would still hydrate even when they should not.'

PR Close #58328
This commit is contained in:
Jessica Janiuk 2024-10-23 09:41:56 -04:00 committed by Alex Rickabaugh
parent c79b62ced2
commit 28516fd852
2 changed files with 83 additions and 35 deletions

View file

@ -134,19 +134,6 @@ function getSsrId(tView: TView): string {
return tView.ssrId;
}
/**
* Global counter that is used to generate a unique id for Defer Blocks
* during the serialization process.
*/
let deferBlockSsrId = 0;
/**
* Generates a unique id for a Defer Block.
*/
function getDeferBlockId(): string {
return `d${deferBlockSsrId++}`;
}
/**
* Describes a context available during the serialization
* process. The context is used to share and collect information
@ -407,6 +394,8 @@ function serializeLContainer(
[NUM_ROOT_NODES]: numRootNodes,
};
let isHydrateNeverBlock = false;
// If this is a defer block, serialize extra info.
if (isDeferBlock(lView[TVIEW], tNode)) {
const lDetails = getLDeferBlockDetails(lView, tNode);
@ -414,11 +403,14 @@ function serializeLContainer(
if (context.isIncrementalHydrationEnabled) {
const deferBlockId = `d${context.deferBlocks.size}`;
const tDetails = getTDeferBlockDetails(lView[TVIEW], tNode);
if (tDetails.hydrateTriggers?.has(DeferBlockTrigger.Never)) {
isHydrateNeverBlock = true;
}
let rootNodes: any[] = [];
collectNativeNodesInLContainer(lContainer, rootNodes);
const tDetails = getTDeferBlockDetails(lView[TVIEW], tNode);
// Add defer block into info context.deferBlocks
const deferBlockInfo: SerializedDeferBlock = {
[DEFER_PARENT_BLOCK_ID]: parentDeferBlockId,
@ -440,7 +432,10 @@ function serializeLContainer(
for (let et of actionList) {
context.eventTypesToReplay.regular.add(et);
}
annotateDeferBlockRootNodesWithJsAction(actionList, rootNodes, deferBlockId);
if (!isHydrateNeverBlock) {
annotateDeferBlockRootNodesWithJsAction(actionList, rootNodes, deferBlockId);
}
// Use current block id as parent for nested routes.
parentDeferBlockId = deferBlockId;
@ -453,11 +448,13 @@ function serializeLContainer(
serializedView[DEFER_BLOCK_STATE] = lDetails[CURRENT_DEFER_BLOCK_STATE];
}
// TODO(incremental-hydration): avoid copying of an object here
serializedView = {
...serializedView,
...serializeLView(lContainer[i] as LView, parentDeferBlockId, context, injector),
};
if (!isHydrateNeverBlock) {
// TODO(incremental-hydration): avoid copying of an object here
serializedView = {
...serializedView,
...serializeLView(lContainer[i] as LView, parentDeferBlockId, context, injector),
};
}
}
// Check if the previous view has the same shape (for example, it was
@ -610,19 +607,6 @@ function serializeLView(
continue;
}
// Attach `jsaction` attribute to elements that have registered listeners,
// thus potentially having a need to do an event replay.
if (nativeElementsToEventTypes && tNode.type & TNodeType.Element) {
const nativeElement = unwrapRNode(lView[i]) as Element;
if (nativeElementsToEventTypes.has(nativeElement)) {
setJSActionAttributes(
nativeElement,
nativeElementsToEventTypes.get(nativeElement)!,
parentDeferBlockId,
);
}
}
if (Array.isArray(tNode.projection)) {
for (const projectionHeadTNode of tNode.projection) {
// We may have `null`s in slots with no projected content.
@ -663,7 +647,6 @@ function serializeLView(
}
conditionallyAnnotateNodePath(ngh, tNode, lView, i18nChildren);
if (isLContainer(lView[i])) {
// Serialize information about a template.
const embeddedTView = tNode.tView;
@ -744,6 +727,19 @@ function serializeLView(
processTextNodeBeforeSerialization(context, rNode);
}
}
// Attach `jsaction` attribute to elements that have registered listeners,
// thus potentially having a need to do an event replay.
if (nativeElementsToEventTypes && tNode.type & TNodeType.Element) {
const nativeElement = unwrapRNode(lView[i]) as Element;
if (nativeElementsToEventTypes.has(nativeElement)) {
setJSActionAttributes(
nativeElement,
nativeElementsToEventTypes.get(nativeElement)!,
parentDeferBlockId,
);
}
}
}
return ngh;
}

View file

@ -1339,6 +1339,58 @@ describe('platform-server partial hydration integration', () => {
expect(appHostNode.outerHTML).not.toContain('Outer block placeholder');
}, 100_000);
it('should not annotate jsaction events for events inside a hydrate never block', async () => {
@Component({
standalone: true,
selector: 'app',
template: `
<main (click)="fnA()">
@defer (on timer(1s); hydrate never) {
<article>
defer block rendered!
<span id="test" (click)="fnB()">{{value()}}</span>
@defer(on immediate; hydrate on idle) {
<p id="test2" (click)="fnB()">shouldn't be annotated</p>
} @placeholder {
<p>blah de blah</p>
}
</article>
} @placeholder {
<span>Outer block placeholder</span>
}
@defer (on timer(1s); hydrate on viewport) {
<div>
viewport section
<p (click)="fnA()">has a binding</p>
</div>
} @placeholder {
<span>another placeholder</span>
}
</main>
`,
})
class SimpleComponent {
value = signal('start');
fnA() {}
fnB() {
this.value.set('end');
}
}
const appId = 'custom-app-id';
const providers = [{provide: APP_ID, useValue: appId}];
const hydrationFeatures = [withIncrementalHydration()];
const html = await ssr(SimpleComponent, {envProviders: providers, hydrationFeatures});
const ssrContents = getAppContents(html);
expect(ssrContents).not.toContain('<span id="test" jsaction="click:;');
expect(ssrContents).toContain('<span id="test">start</span>');
expect(ssrContents).toContain('<p jsaction="click:;" ngb="d1">has a binding</p>');
expect(ssrContents).not.toContain('<p id="test2" jsaction="click:;');
expect(ssrContents).toContain('<p id="test2">shouldn\'t be annotated</p>');
}, 100_000);
});
describe('cleanup', () => {