Skip to content

Commit

Permalink
refactor(core): add hydration support for built-in if and switch
Browse files Browse the repository at this point in the history
This commit updates the `if` and `switch` logic to support hydration. On the server, we annotate a `template` instruction that is used to host and render selected branch. On the client, we lookup dehydrated views in case we detect that the flag is present.
  • Loading branch information
AndrewKushnir committed Sep 24, 2023
1 parent 3cbb2a8 commit 7f1ba6c
Show file tree
Hide file tree
Showing 4 changed files with 337 additions and 28 deletions.
76 changes: 53 additions & 23 deletions packages/compiler/src/render3/view/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ const GLOBAL_TARGET_RESOLVERS = new Map<string, o.ExternalReference>(

export const LEADING_TRIVIA_CHARS = [' ', '\n', '\r', '\t'];

// Special symbol that is used as a tag name in the `template` instruction in case
// an underlying LContainer is used by the runtime code to render different branches.
// This is needed to indicate that this particular LContainer needs to be hydrated
// (if an application uses SSR).
const BUILT_IN_CONTROL_FLOW_CONTAINER = '@';

// if (rf & flags) { .. }
export function renderFlagCheckIfStmt(
flags: core.RenderFlags, statements: o.Statement[]): o.IfStmt {
Expand Down Expand Up @@ -1144,29 +1150,42 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
// We have to process the block in two steps: once here and again in the update instruction
// callback in order to generate the correct expressions when pipes or pure functions are
// used inside the branch expressions.
const branchData = block.branches.map(({expression, expressionAlias, children, sourceSpan}) => {
let processedExpression: AST|null = null;
const branchData =
block.branches.map(({expression, expressionAlias, children, sourceSpan}, index) => {
let processedExpression: AST|null = null;

if (expression !== null) {
processedExpression = expression.visit(this._valueConverter);
this.allocateBindingSlots(processedExpression);
}
if (expression !== null) {
processedExpression = expression.visit(this._valueConverter);
this.allocateBindingSlots(processedExpression);
}

// If the branch has an alias, it'll be assigned directly to the container's context.
// We define a variable referring directly to the context so that any nested usages can be
// rewritten to refer to it.
const variables = expressionAlias !== null ?
[new t.Variable(
expressionAlias.name, DIRECT_CONTEXT_REFERENCE, expressionAlias.sourceSpan,
expressionAlias.keySpan)] :
undefined;

return {
index: this.createEmbeddedTemplateFn(null, children, '_Conditional', sourceSpan, variables),
expression: processedExpression,
alias: expressionAlias
};
});
// If the branch has an alias, it'll be assigned directly to the container's context.
// We define a variable referring directly to the context so that any nested usages can be
// rewritten to refer to it.
const variables = expressionAlias !== null ?
[new t.Variable(
expressionAlias.name, DIRECT_CONTEXT_REFERENCE, expressionAlias.sourceSpan,
expressionAlias.keySpan)] :
undefined;

// Since there is no creation mode instruction for {#if}, we need to annotate
// a template instruction, which creates an LContainer that is later used by
// the runtime code to render different branches. This is needed to indicate
// that this particular LContainer needs to be hydrated (if an application uses
// SSR). To avoid introducing an extra flag and consuming an extra argument,
// use a special symbol in the tag name.
//
// NOTE: this code will be removed once we split the `template` instruction logic
// into a `block` and `container` instructions and start generating them for control
// flow instead.
const tagName = index === 0 ? BUILT_IN_CONTROL_FLOW_CONTAINER : null;
return {
index: this.createEmbeddedTemplateFn(
tagName, children, '_Conditional', sourceSpan, variables),
expression: processedExpression,
alias: expressionAlias
};
});

// Use the index of the first block as the index for the entire container.
const containerIndex = branchData[0].index;
Expand Down Expand Up @@ -1228,9 +1247,20 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver

// We have to process the block in two steps: once here and again in the update instruction
// callback in order to generate the correct expressions when pipes or pure functions are used.
const caseData = block.cases.map(currentCase => {
const caseData = block.cases.map((currentCase, caseIndex) => {
// Since there is no creation mode instruction for {#switch}, we need to annotate
// a template instruction, which creates an LContainer that is later used by
// the runtime code to render different branches. This is needed to indicate
// that this particular LContainer needs to be hydrated (if an application uses
// SSR). To avoid introducing an extra flag and consuming an extra argument,
// use a special symbol in the tag name.
//
// NOTE: this code will be removed once we split the `template` instruction logic
// into a `block` and `container` instructions and start generating them for control
// flow instead.
const tagName = caseIndex === 0 ? BUILT_IN_CONTROL_FLOW_CONTAINER : null;
const index = this.createEmbeddedTemplateFn(
null, currentCase.children, '_Case', currentCase.sourceSpan);
tagName, currentCase.children, '_Case', currentCase.sourceSpan);
let expression: AST|null = null;

if (currentCase.expression !== null) {
Expand Down
11 changes: 8 additions & 3 deletions packages/core/src/render3/instructions/control_flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import {DefaultIterableDiffer, IterableChangeRecord, TrackByFunction} from '../../change_detection';
import {findMatchingDehydratedView} from '../../hydration/views';
import {assertDefined} from '../../util/assert';
import {assertLContainer, assertLView, assertTNode} from '../assert';
import {bindingUpdated} from '../bindings';
Expand All @@ -17,7 +18,7 @@ import {CONTEXT, DECLARATION_COMPONENT_VIEW, HEADER_OFFSET, LView, TVIEW, TView}
import {detachView} from '../node_manipulation';
import {getLView, nextBindingIndex} from '../state';
import {getTNode} from '../util/view_utils';
import {addLViewToLContainer, createAndRenderEmbeddedLView, getLViewFromLContainer, removeLViewFromLContainer} from '../view_manipulation';
import {addLViewToLContainer, createAndRenderEmbeddedLView, getLViewFromLContainer, removeLViewFromLContainer, shouldAddViewToDom} from '../view_manipulation';

import {ɵɵtemplate} from './template';

Expand Down Expand Up @@ -47,9 +48,13 @@ export function ɵɵconditional<T>(containerIndex: number, matchingTemplateIndex
// a truthy value and as the consequence we've got no view to show.
if (matchingTemplateIndex !== -1) {
const templateTNode = getExistingTNode(hostLView[TVIEW], matchingTemplateIndex);
const embeddedLView = createAndRenderEmbeddedLView(hostLView, templateTNode, value);
const dehydratedView = findMatchingDehydratedView(lContainer, templateTNode.tView!.ssrId);
const embeddedLView =
createAndRenderEmbeddedLView(hostLView, templateTNode, value, {dehydratedView});

addLViewToLContainer(lContainer, embeddedLView, viewInContainerIdx);
addLViewToLContainer(
lContainer, embeddedLView, viewInContainerIdx,
shouldAddViewToDom(templateTNode, dehydratedView));
}
} else {
// We might keep displaying the same template but the actual value of the expression could have
Expand Down
26 changes: 25 additions & 1 deletion packages/core/src/render3/instructions/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {validateMatchingNode, validateNodeExists} from '../../hydration/error_ha
import {TEMPLATES} from '../../hydration/interfaces';
import {locateNextRNode, siblingAfter} from '../../hydration/node_lookup_utils';
import {calcSerializedContainerSize, isDisconnectedNode, markRNodeAsClaimedByHydration, setSegmentHead} from '../../hydration/utils';
import {populateDehydratedViewsInContainer} from '../../linker/view_container_ref';
import {assertEqual} from '../../util/assert';
import {assertFirstCreatePass} from '../assert';
import {attachPatchData} from '../context_discovery';
Expand All @@ -24,6 +25,12 @@ import {getConstant} from '../util/view_utils';

import {addToViewTree, createDirectivesInstances, createLContainer, createTView, getOrCreateTNode, resolveDirectives, saveResolvedLocalsInData} from './shared';

// Special symbol that is used as a tag name in the `template` instruction in case
// an underlying LContainer is used by the runtime code to render different branches.
// This is needed to indicate that this particular LContainer needs to be hydrated
// (if an application uses SSR).
const BUILT_IN_CONTROL_FLOW_CONTAINER = '@';

function templateFirstCreatePass(
index: number, tView: TView, lView: LView, templateFn: ComponentTemplate<any>|null,
decls: number, vars: number, tagName?: string|null, attrsIndex?: number|null,
Expand Down Expand Up @@ -92,7 +99,24 @@ export function ɵɵtemplate(
}
attachPatchData(comment, lView);

addToViewTree(lView, lView[adjustedIndex] = createLContainer(comment, lView, comment, tNode));
const lContainer = createLContainer(comment, lView, comment, tNode);
lView[adjustedIndex] = lContainer;
addToViewTree(lView, lContainer);

// Since there is no creation mode instruction for `if` and `for`, we annotate
// a `template` instruction, which creates an LContainer that is later used by
// the runtime code to render different branches. This is needed to indicate
// that this particular LContainer needs to be hydrated (if an application uses
// SSR). To avoid introducing an extra flag and consuming an extra argument,
// we use a special symbol in the tag name.
//
// NOTE: this code will be removed once we split the `template` instruction logic
// into a `block` and `container` instructions and start generating them for control
// flow instead. In this case, when we call `container` from the `template` function,
// we'll just pass a flag to skip hydration.
if (tagName === BUILT_IN_CONTROL_FLOW_CONTAINER) {
populateDehydratedViewsInContainer(lContainer);
}

if (isDirectiveHost(tNode)) {
createDirectivesInstances(tView, lView, tNode);
Expand Down
Loading

0 comments on commit 7f1ba6c

Please sign in to comment.