...`.
- // In this case, there are nodes *after* this element and we need to skip
- // all of them to reach an element that we are looking for.
- const noOffsetPrevSiblingIndex = getNoOffsetIndex(previousTNode);
- const segmentHead = getSegmentHead(hydrationInfo, noOffsetPrevSiblingIndex);
- if (previousTNode.type === TNodeType.Element && segmentHead) {
- const numRootNodesToSkip =
- calcSerializedContainerSize(hydrationInfo, noOffsetPrevSiblingIndex);
- // `+1` stands for an anchor comment node after all the views in this container.
- const nodesToSkip = numRootNodesToSkip + 1;
- // First node after this segment.
- native = siblingAfter(nodesToSkip, segmentHead);
+ let previousRElement = getNativeByTNode(previousTNode, lView);
+ if (previousTNodeParent) {
+ native = (previousRElement as RElement).firstChild;
} else {
- native = previousRElement.nextSibling;
+ // If the previous node is an element, but it also has container info,
+ // this means that we are processing a node like `
`, which is
+ // represented in the DOM as `
...`.
+ // In this case, there are nodes *after* this element and we need to skip
+ // all of them to reach an element that we are looking for.
+ const noOffsetPrevSiblingIndex = getNoOffsetIndex(previousTNode);
+ const segmentHead = getSegmentHead(hydrationInfo, noOffsetPrevSiblingIndex);
+ if (previousTNode.type === TNodeType.Element && segmentHead) {
+ const numRootNodesToSkip =
+ calcSerializedContainerSize(hydrationInfo, noOffsetPrevSiblingIndex);
+ // `+1` stands for an anchor comment node after all the views in this container.
+ const nodesToSkip = numRootNodesToSkip + 1;
+ // First node after this segment.
+ native = siblingAfter(nodesToSkip, segmentHead);
+ } else {
+ native = previousRElement.nextSibling;
+ }
}
}
}
diff --git a/packages/core/src/render3/i18n/i18n_apply.ts b/packages/core/src/render3/i18n/i18n_apply.ts
index 27d376d0d59c09..91853ef30d59a7 100644
--- a/packages/core/src/render3/i18n/i18n_apply.ts
+++ b/packages/core/src/render3/i18n/i18n_apply.ts
@@ -7,8 +7,10 @@
*/
import {RuntimeError, RuntimeErrorCode} from '../../errors';
+import { locateRNodeByIndex } from '../../hydration/node_lookup_utils';
+import { isDisconnectedNode, markRNodeAsClaimedByHydration } from '../../hydration/utils';
import {getPluralCase} from '../../i18n/localization';
-import {assertDefined, assertDomNode, assertEqual, assertGreaterThan, assertIndexInRange, throwError} from '../../util/assert';
+import {assertDefined, assertDomNode, assertEqual, assertGreaterThan, assertIndexInRange, assertNotDefined, throwError} from '../../util/assert';
import {assertIndexInExpandoRange, assertTIcu} from '../assert';
import {attachPatchData} from '../context_discovery';
import {elementPropertyInternal, setElementAttribute} from '../instructions/shared';
@@ -16,9 +18,9 @@ import {ELEMENT_MARKER, I18nCreateOpCode, I18nCreateOpCodes, I18nUpdateOpCode, I
import {TNode} from '../interfaces/node';
import {RElement, RNode, RText} from '../interfaces/renderer_dom';
import {SanitizerFn} from '../interfaces/sanitization';
-import {HEADER_OFFSET, LView, RENDERER, TView} from '../interfaces/view';
+import {HEADER_OFFSET, HYDRATION, LView, RENDERER, TVIEW, TView} from '../interfaces/view';
import {createCommentNode, createElementNode, createTextNode, nativeInsertBefore, nativeParentNode, nativeRemoveNode, updateTextNode} from '../node_manipulation';
-import {getBindingIndex} from '../state';
+import {getBindingIndex, isInSkipHydrationBlock, lastNodeWasCreated, wasLastNodeCreated} from '../state';
import {renderStringify} from '../util/stringify_utils';
import {getNativeByIndex, unwrapRNode} from '../util/view_utils';
@@ -101,19 +103,50 @@ export function applyCreateOpCodes(
const appendNow =
(opCode & I18nCreateOpCode.APPEND_EAGERLY) === I18nCreateOpCode.APPEND_EAGERLY;
const index = opCode >>> I18nCreateOpCode.SHIFT;
- let rNode = lView[index];
- if (rNode === null) {
- // We only create new DOM nodes if they don't already exist: If ICU switches case back to a
- // case which was already instantiated, no need to create new DOM nodes.
- rNode = lView[index] =
- isComment ? renderer.createComment(text) : createTextNode(renderer, text);
- }
- if (appendNow && parentRNode !== null) {
+
+ ngDevMode && assertNotDefined(lView[index], 'view should not exist');
+
+ let rNode = lView[index] = locateOrCreateNode(lView, index, text, isComment ? Node.COMMENT_NODE : Node.TEXT_NODE);
+
+ if ((appendNow && parentRNode !== null) && wasLastNodeCreated()) {
nativeInsertBefore(renderer, parentRNode, rNode, insertInFrontOf, false);
}
}
}
+function locateOrCreateNode(lView: LView, index: number, textOrName: string, nodeType: number) {
+ const hydrationInfo = lView[HYDRATION];
+ const isNodeCreationMode = !hydrationInfo || isInSkipHydrationBlock() || isDisconnectedNode(hydrationInfo, index);
+
+ lastNodeWasCreated(isNodeCreationMode);
+
+ if (isNodeCreationMode) {
+ const renderer = lView[RENDERER];
+ switch (nodeType) {
+ case Node.COMMENT_NODE:
+ return createCommentNode(renderer, textOrName);
+
+ case Node.TEXT_NODE:
+ return createTextNode(renderer, textOrName);
+
+ case Node.ELEMENT_NODE:
+ return createElementNode(renderer, textOrName, null);
+ }
+ }
+
+ const native = locateRNodeByIndex(hydrationInfo!, index - HEADER_OFFSET) as RNode;
+
+ if (ngDevMode) {
+ // TODO: Ideally we'd reuse `validateMatchingNode` here, but not all i18n nodes have
+ // a valid TNode.
+ assertEqual((native as Node).nodeType, nodeType, 'expected matching node type');
+ nodeType === Node.ELEMENT_NODE && assertEqual((native as Element).tagName.toLowerCase(), textOrName.toLowerCase(), 'expected matching tag name');
+ markRNodeAsClaimedByHydration(native);
+ }
+
+ return native;
+}
+
/**
* Apply `I18nMutateOpCodes` OpCodes.
*
@@ -141,7 +174,7 @@ export function applyMutableOpCodes(
if (lView[textNodeIndex] === null) {
ngDevMode && ngDevMode.rendererCreateTextNode++;
ngDevMode && assertIndexInRange(lView, textNodeIndex);
- lView[textNodeIndex] = createTextNode(renderer, opCode);
+ lView[textNodeIndex] = locateOrCreateNode(lView, textNodeIndex, opCode, Node.TEXT_NODE);
}
} else if (typeof opCode == 'number') {
switch (opCode & IcuCreateOpCode.MASK_INSTRUCTION) {
@@ -218,8 +251,7 @@ export function applyMutableOpCodes(
`Expected "${commentValue}" to be a comment node value`);
ngDevMode && ngDevMode.rendererCreateComment++;
ngDevMode && assertIndexInExpandoRange(lView, commentNodeIndex);
- const commentRNode = lView[commentNodeIndex] =
- createCommentNode(renderer, commentValue);
+ const commentRNode = lView[commentNodeIndex] = locateOrCreateNode(lView, commentNodeIndex, commentValue, Node.COMMENT_NODE);
// FIXME(misko): Attaching patch data is only needed for the root (Also add tests)
attachPatchData(commentRNode, lView);
}
@@ -236,7 +268,7 @@ export function applyMutableOpCodes(
ngDevMode && ngDevMode.rendererCreateElement++;
ngDevMode && assertIndexInExpandoRange(lView, elementNodeIndex);
const elementRNode = lView[elementNodeIndex] =
- createElementNode(renderer, tagName, null);
+ locateOrCreateNode(lView, elementNodeIndex, tagName, Node.ELEMENT_NODE);
// FIXME(misko): Attaching patch data is only needed for the root (Also add tests)
attachPatchData(elementRNode, lView);
}
diff --git a/packages/core/src/render3/i18n/i18n_hydration.ts b/packages/core/src/render3/i18n/i18n_hydration.ts
new file mode 100644
index 00000000000000..6965e1044170db
--- /dev/null
+++ b/packages/core/src/render3/i18n/i18n_hydration.ts
@@ -0,0 +1,207 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import { DehydratedView, I18N_ICU_DATA } from "../../hydration/interfaces";
+import { locateNextRNode, siblingAfter } from "../../hydration/node_lookup_utils";
+import { getNgContainerSize } from "../../hydration/utils";
+import { assertDefined, assertNotEqual } from "../../util/assert";
+import { I18nNode, I18nNodeKind, I18nPlaceholderType, TI18n } from "../interfaces/i18n";
+import { TNode } from "../interfaces/node";
+import { RNode } from "../interfaces/renderer_dom";
+import { HEADER_OFFSET, HYDRATION, LView, TVIEW } from "../interfaces/view";
+
+/**
+ * TODO: Compute the i18n data (i.e. ICU cases, if any) to serialize for the given LView.
+ */
+export function computeI18nSerialization(lView: LView, index: number): Array
|null {
+ const tView = lView[TVIEW];
+ const tI18n = tView.data[index] as TI18n | undefined;
+
+ if (!tI18n || !tI18n.ast) {
+ return null;
+ }
+
+ const decisions: Array = [];
+ tI18n.ast.forEach(node => computeCaseQueue(node, lView, decisions));
+ return decisions.length > 0 ? decisions : null;
+}
+
+function computeCaseQueue(node: I18nNode, lView: LView, decisions: Array) {
+ switch (node.kind) {
+ case I18nNodeKind.ELEMENT:
+ case I18nNodeKind.PLACEHOLDER:
+ node.children.forEach(node => computeCaseQueue(node, lView, decisions));
+ break;
+
+ case I18nNodeKind.ICU:
+ const currentCase = lView[node.currentCaseLViewIndex] as number;
+ if (currentCase < 0) {
+ const caseIdx = ~currentCase;
+ decisions.push(caseIdx);
+ node.cases[caseIdx].forEach(node => computeCaseQueue(node, lView, decisions));
+ }
+ break;
+ }
+}
+
+interface DeserializationContext {
+ lView: LView;
+ disconnectedNodes: Set;
+ nodeMap: Record;
+ caseQueue: number[];
+ hydrationInfo: DehydratedView;
+}
+
+/**
+ * TODO: Updates the hydration info for the given LView, based on the serialized
+ * i18n data, if available.
+ *
+ * In particular, this uses the serialized i18n data to walk over the AST and map
+ * each LView to a given DOM element. This is used to update the `nodeMap` and
+ * `disconnectedNodes` values in the LView's hydration info, so that Angular can
+ * hydrate as usual.
+ */
+export function computeI18nDeserialization(lView: LView, index: number): void {
+ const hydrationInfo = lView[HYDRATION];
+ if (!hydrationInfo) {
+ return;
+ }
+
+ const tView = lView[TVIEW];
+ const tI18n = tView.data[index] as TI18n;
+ const caseQueue = hydrationInfo?.data[I18N_ICU_DATA]?.[index - HEADER_OFFSET] ?? [];
+
+ ngDevMode && assertDefined(tI18n, 'Expected i18n data');
+ ngDevMode && assertDefined(tI18n.ast, 'Expected valid i18n data');
+
+ const firstChildIndex = tI18n.ast[0].index;
+ const tNode = tView.data[firstChildIndex] as TNode;
+ const rootNode: Node = locateNextRNode(hydrationInfo, tView, lView, tNode) as Node;
+
+ ngDevMode && assertDefined(rootNode, 'expected root node');
+
+ const nodeMap = hydrationInfo.nodeMap ??= {};
+ const disconnectedNodes = hydrationInfo.disconnectedNodes ??= new Set();
+
+ const context: DeserializationContext = {
+ lView,
+ disconnectedNodes,
+ nodeMap,
+ caseQueue: [...caseQueue],
+ hydrationInfo,
+ };
+ const state: HydrationState = {
+ currentNode: rootNode,
+ };
+ computeDeserializationData(context, tI18n.ast, state);
+}
+
+interface HydrationState {
+ currentNode: Node | null;
+}
+
+const enum MarkOptions {
+ NONE = 0,
+ CLAIM
+}
+
+function markHydrationRoot(context: DeserializationContext, astNode: I18nNode, state: HydrationState | null, options: MarkOptions): void {
+ const noOffsetIndex = astNode.index - HEADER_OFFSET;
+ const domNode = state?.currentNode;
+
+ if (domNode != null) {
+ context.nodeMap[noOffsetIndex] = domNode;
+ if (options === MarkOptions.CLAIM) {
+ state!.currentNode = domNode.nextSibling;
+ }
+ } else {
+ context.disconnectedNodes.add(noOffsetIndex);
+ }
+}
+
+function computeDeserializationData(context: DeserializationContext, astNodeOrNodes: I18nNode | I18nNode[], state: HydrationState | null) {
+ if (Array.isArray(astNodeOrNodes)) {
+ for (let i = 0; i < astNodeOrNodes.length; i++) {
+ computeDeserializationData(context, astNodeOrNodes[i], state);
+ }
+ } else {
+ const astNode = astNodeOrNodes;
+ switch (astNode.kind) {
+ case I18nNodeKind.TEXT: {
+ markHydrationRoot(context, astNode, state, MarkOptions.CLAIM);
+ break;
+ }
+
+ case I18nNodeKind.ELEMENT: {
+ const childState = state == null ? null : { currentNode: state.currentNode?.firstChild ?? null };
+ computeDeserializationData(context, astNode.children, childState);
+ markHydrationRoot(context, astNode, state, MarkOptions.CLAIM);
+ break;
+ }
+
+ case I18nNodeKind.PLACEHOLDER: {
+ const containerSize = getNgContainerSize(context.hydrationInfo, astNode.index - HEADER_OFFSET);
+
+ switch (astNode.type) {
+ case I18nPlaceholderType.ELEMENT: {
+ let childState = state;
+ let markOptions = MarkOptions.NONE;
+
+ if (containerSize == null) {
+ // Elements have an actual representation in the DOM,
+ // so we need to traverse their children, and continue
+ // hydrating from the next sibling.
+ childState = state == null ? null : { currentNode: state.currentNode?.firstChild ?? null };
+ markOptions = MarkOptions.CLAIM;
+ }
+
+ // Hydration expects to find the head of the element.
+ markHydrationRoot(context, astNode, state, markOptions);
+ computeDeserializationData(context, astNode.children, childState);
+
+ if (containerSize != null && state?.currentNode) {
+ // Skip over the anchor element for containers. The element
+ // will be claimed when the container is hydrated.
+ state.currentNode = state.currentNode.nextSibling;
+ }
+ break;
+ }
+
+ case I18nPlaceholderType.SUBTEMPLATE: {
+ ngDevMode && assertNotEqual(containerSize, null, 'expected a container size');
+
+ // Hydration expects to find the head of the template.
+ markHydrationRoot(context, astNode, state, MarkOptions.NONE);
+
+ if (state?.currentNode) {
+ // Skip over the template children since the template will take care
+ // of hydrating them.
+ state.currentNode = siblingAfter(containerSize!, state.currentNode);
+
+ // Also skip over the achor element. We don't include this in the
+ // siblingAfter above because it's OK for the node to be null, such
+ // as when we've reached the end of a leaf.
+ state.currentNode = state.currentNode?.nextSibling ?? null;
+ }
+ break;
+ }
+ }
+ break;
+ }
+
+ case I18nNodeKind.ICU: {
+ const selectedCase = state?.currentNode ? context.caseQueue.shift()! : null;
+ for (let i = 0; i < astNode.cases.length; i++) {
+ computeDeserializationData(context, astNode.cases[i], selectedCase === i ? state : null);
+ }
+ markHydrationRoot(context, astNode, state, MarkOptions.CLAIM);
+ break;
+ }
+ }
+ }
+}
diff --git a/packages/core/src/render3/i18n/i18n_parse.ts b/packages/core/src/render3/i18n/i18n_parse.ts
index 224a7a0523c26a..5b78183c058b39 100644
--- a/packages/core/src/render3/i18n/i18n_parse.ts
+++ b/packages/core/src/render3/i18n/i18n_parse.ts
@@ -17,7 +17,7 @@ import {CharCode} from '../../util/char_code';
import {loadIcuContainerVisitor} from '../instructions/i18n_icu_container_visitor';
import {allocExpando, createTNodeAtIndex} from '../instructions/shared';
import {getDocument} from '../interfaces/document';
-import {ELEMENT_MARKER, I18nCreateOpCode, I18nCreateOpCodes, I18nRemoveOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes, ICU_MARKER, IcuCreateOpCode, IcuCreateOpCodes, IcuExpression, IcuType, TI18n, TIcu} from '../interfaces/i18n';
+import {ELEMENT_MARKER, I18nCreateOpCode, I18nCreateOpCodes, I18nElementNode, I18nICUNode, I18nNode, I18nNodeKind, I18nPlaceholderNode, I18nPlaceholderType, I18nRemoveOpCodes, I18nTextNode, I18nUpdateOpCode, I18nUpdateOpCodes, ICU_MARKER, IcuCreateOpCode, IcuCreateOpCodes, IcuExpression, IcuType, TI18n, TIcu} from '../interfaces/i18n';
import {TNode, TNodeType} from '../interfaces/node';
import {SanitizerFn} from '../interfaces/sanitization';
import {HEADER_OFFSET, LView, TView} from '../interfaces/view';
@@ -88,6 +88,7 @@ export function i18nStartFirstCreatePass(
const createOpCodes: I18nCreateOpCodes = [] as any;
const updateOpCodes: I18nUpdateOpCodes = [] as any;
const existingTNodeStack: TNode[][] = [[]];
+ const astStack: Array> = [[]];
if (ngDevMode) {
attachDebugGetter(createOpCodes, i18nCreateOpCodesToString);
attachDebugGetter(updateOpCodes, i18nUpdateOpCodesToString);
@@ -107,8 +108,9 @@ export function i18nStartFirstCreatePass(
const text = part as string;
ngDevMode && assertString(text, 'Parsed ICU part should be string');
if (text !== '') {
- i18nStartFirstCreatePassProcessTextNode(
+ const textNodeIndex = i18nStartFirstCreatePassProcessTextNode(
tView, rootTNode, existingTNodeStack[0], createOpCodes, updateOpCodes, lView, text);
+ astStack[0].push({ kind: I18nNodeKind.TEXT, index: textNodeIndex, debug: text });
}
} else {
// `j` is Even therefor `part` is an `ICUExpression`
@@ -129,7 +131,7 @@ export function i18nStartFirstCreatePass(
ngDevMode &&
assertGreaterThanOrEqual(
icuNodeIndex, HEADER_OFFSET, 'Index must be in absolute LView offset');
- icuStart(tView, lView, updateOpCodes, parentTNodeIndex, icuExpression, icuNodeIndex);
+ icuStart(tView, lView, astStack[0], updateOpCodes, parentTNodeIndex, icuExpression, icuNodeIndex);
}
}
} else {
@@ -141,11 +143,21 @@ export function i18nStartFirstCreatePass(
const index = HEADER_OFFSET + Number.parseInt(value.substring((isClosing ? 2 : 1)));
if (isClosing) {
existingTNodeStack.shift();
+ astStack.shift();
setCurrentTNode(getCurrentParentTNode()!, false);
} else {
const tNode = createTNodePlaceholder(tView, existingTNodeStack[0], index);
existingTNodeStack.unshift([]);
setCurrentTNode(tNode, true);
+
+ const ast: I18nPlaceholderNode = {
+ kind: I18nNodeKind.PLACEHOLDER,
+ index,
+ children: [],
+ type: type === CharCode.HASH ? I18nPlaceholderType.ELEMENT : I18nPlaceholderType.SUBTEMPLATE,
+ };
+ astStack[0].push(ast);
+ astStack.unshift(ast.children);
}
}
}
@@ -153,6 +165,7 @@ export function i18nStartFirstCreatePass(
tView.data[index] = {
create: createOpCodes,
update: updateOpCodes,
+ ast: astStack[0],
};
}
@@ -230,13 +243,15 @@ function createTNodeAndAddOpCode(
*/
function i18nStartFirstCreatePassProcessTextNode(
tView: TView, rootTNode: TNode|null, existingTNodes: TNode[], createOpCodes: I18nCreateOpCodes,
- updateOpCodes: I18nUpdateOpCodes, lView: LView, text: string): void {
+ updateOpCodes: I18nUpdateOpCodes, lView: LView, text: string): number {
const hasBinding = text.match(BINDING_REGEXP);
const tNode = createTNodeAndAddOpCode(
tView, rootTNode, existingTNodes, lView, createOpCodes, hasBinding ? null : text, false);
+ const index = tNode.index;
if (hasBinding) {
- generateBindingUpdateOpCodes(updateOpCodes, text, tNode.index, null, 0, null);
+ generateBindingUpdateOpCodes(updateOpCodes, text, index, null, 0, null);
}
+ return index;
}
/**
@@ -442,7 +457,7 @@ export function getTranslationForTemplate(message: string, subTemplateIndex: num
* - `tView.data[anchorIdx]` points to the `TIcuContainerNode` if ICU is root (`null` otherwise)
*/
export function icuStart(
- tView: TView, lView: LView, updateOpCodes: I18nUpdateOpCodes, parentIdx: number,
+ tView: TView, lView: LView, ast: Array, updateOpCodes: I18nUpdateOpCodes, parentIdx: number,
icuExpression: IcuExpression, anchorIdx: number) {
ngDevMode && assertDefined(icuExpression, 'ICU expression must be defined');
let bindingMask = 0;
@@ -458,6 +473,7 @@ export function icuStart(
addUpdateIcuSwitch(updateOpCodes, icuExpression, anchorIdx);
setTIcu(tView, anchorIdx, tIcu);
const values = icuExpression.values;
+ const cases: I18nNode[][] = [];
for (let i = 0; i < values.length; i++) {
// Each value is an array of strings & other ICU expressions
const valueArr = values[i];
@@ -471,14 +487,17 @@ export function icuStart(
valueArr[j] = ``;
}
}
+ const ast: I18nNode[] = [];
+ cases.push(ast);
bindingMask = parseIcuCase(
- tView, tIcu, lView, updateOpCodes, parentIdx, icuExpression.cases[i],
+ tView, tIcu, lView, ast, updateOpCodes, parentIdx, icuExpression.cases[i],
valueArr.join(''), nestedIcus) |
bindingMask;
}
if (bindingMask) {
addUpdateIcuUpdate(updateOpCodes, bindingMask, anchorIdx);
}
+ ast.push({ kind: I18nNodeKind.ICU, index: anchorIdx, cases, currentCaseLViewIndex: tIcu.currentCaseLViewIndex } as I18nICUNode);
}
/**
@@ -586,7 +605,7 @@ export function i18nParseTextIntoPartsAndICU(pattern: string): (string|IcuExpres
*
*/
export function parseIcuCase(
- tView: TView, tIcu: TIcu, lView: LView, updateOpCodes: I18nUpdateOpCodes, parentIdx: number,
+ tView: TView, tIcu: TIcu, lView: LView, ast: Array, updateOpCodes: I18nUpdateOpCodes, parentIdx: number,
caseName: string, unsafeCaseHtml: string, nestedIcus: IcuExpression[]): number {
const create: IcuCreateOpCodes = [] as any;
const remove: I18nRemoveOpCodes = [] as any;
@@ -607,7 +626,7 @@ export function parseIcuCase(
const inertRootNode = getTemplateContent(inertBodyElement!) as Element || inertBodyElement;
if (inertRootNode) {
return walkIcuTree(
- tView, tIcu, lView, updateOpCodes, create, remove, update, inertRootNode, parentIdx,
+ tView, tIcu, lView, ast, updateOpCodes, create, remove, update, inertRootNode, parentIdx,
nestedIcus, 0);
} else {
return 0;
@@ -615,7 +634,7 @@ export function parseIcuCase(
}
function walkIcuTree(
- tView: TView, tIcu: TIcu, lView: LView, sharedUpdateOpCodes: I18nUpdateOpCodes,
+ tView: TView, tIcu: TIcu, lView: LView, ast: Array, sharedUpdateOpCodes: I18nUpdateOpCodes,
create: IcuCreateOpCodes, remove: I18nRemoveOpCodes, update: I18nUpdateOpCodes,
parentNode: Element, parentIdx: number, nestedIcus: IcuExpression[], depth: number): number {
let bindingMask = 0;
@@ -654,9 +673,12 @@ function walkIcuTree(
addCreateAttribute(create, newIndex, attr);
}
}
+ const elementNode: I18nElementNode = { kind: I18nNodeKind.ELEMENT, index: newIndex, children: [], debug: tagName };
+ ast.push(elementNode);
+
// Parse the children of this node (if any)
bindingMask = walkIcuTree(
- tView, tIcu, lView, sharedUpdateOpCodes, create, remove, update,
+ tView, tIcu, lView, elementNode.children, sharedUpdateOpCodes, create, remove, update,
currentNode as Element, newIndex, nestedIcus, depth + 1) |
bindingMask;
addRemoveNode(remove, newIndex, depth);
@@ -671,6 +693,7 @@ function walkIcuTree(
bindingMask =
generateBindingUpdateOpCodes(update, value, newIndex, null, 0, null) | bindingMask;
}
+ ast.push({ kind: I18nNodeKind.TEXT, index: newIndex, debug: value } as I18nTextNode);
break;
case Node.COMMENT_NODE:
// Check if the comment node is a placeholder for a nested ICU
@@ -682,7 +705,7 @@ function walkIcuTree(
addCreateNodeAndAppend(
create, ICU_MARKER, ngDevMode ? `nested ICU ${nestedIcuIndex}` : '', parentIdx,
newIndex);
- icuStart(tView, lView, sharedUpdateOpCodes, parentIdx, icuExpression, newIndex);
+ icuStart(tView, lView, ast, sharedUpdateOpCodes, parentIdx, icuExpression, newIndex);
addRemoveNestedIcu(remove, newIndex, depth);
}
break;
diff --git a/packages/core/src/render3/instructions/i18n.ts b/packages/core/src/render3/instructions/i18n.ts
index 37925ae2d24f33..b0a4477fd7522d 100644
--- a/packages/core/src/render3/instructions/i18n.ts
+++ b/packages/core/src/render3/instructions/i18n.ts
@@ -19,6 +19,7 @@ import {DECLARATION_COMPONENT_VIEW, FLAGS, HEADER_OFFSET, LViewFlags, T_HOST, TV
import {getClosestRElement} from '../node_manipulation';
import {getCurrentParentTNode, getLView, getTView, nextBindingIndex, setInI18nBlock} from '../state';
import {getConstant} from '../util/view_utils';
+import { computeI18nDeserialization } from '../i18n/i18n_hydration';
/**
* Marks a block of text as translatable.
@@ -77,8 +78,9 @@ export function ɵɵi18nStart(
// If `parentTNode` is an `ElementContainer` than it has ``.
// When we do inserts we have to make sure to insert in front of ``.
const insertInFrontOf = parentTNode && (parentTNode.type & TNodeType.ElementContainer) ?
- lView[parentTNode.index] :
- null;
+ lView[parentTNode.index] :
+ null;
+ computeI18nDeserialization(lView, adjustedIndex);
applyCreateOpCodes(lView, tI18n.create, parentRNode, insertInFrontOf);
setInI18nBlock(true);
}
diff --git a/packages/core/src/render3/interfaces/i18n.ts b/packages/core/src/render3/interfaces/i18n.ts
index 4b38b15a3af649..0947adab34b94f 100644
--- a/packages/core/src/render3/interfaces/i18n.ts
+++ b/packages/core/src/render3/interfaces/i18n.ts
@@ -341,6 +341,11 @@ export interface TI18n {
* DOM are required.
*/
update: I18nUpdateOpCodes;
+
+ /**
+ * TODO: The AST for the i18n message.
+ */
+ ast: Array;
}
/**
@@ -408,3 +413,46 @@ export interface IcuExpression {
cases: string[];
values: (string|IcuExpression)[][];
}
+
+export type I18nNode = I18nTextNode | I18nElementNode | I18nICUNode | I18nPlaceholderNode;
+
+export type I18nTextNode = {
+ kind: I18nNodeKind.TEXT;
+ index: number;
+ debug?: string;
+}
+
+export type I18nElementNode = {
+ kind: I18nNodeKind.ELEMENT;
+ index: number;
+ children: Array;
+ debug?: string;
+}
+
+export type I18nICUNode = {
+ kind: I18nNodeKind.ICU;
+ index: number;
+ cases: Array>;
+ currentCaseLViewIndex: number;
+}
+
+export type I18nPlaceholderNode = {
+ kind: I18nNodeKind.PLACEHOLDER;
+ index: number;
+ children: Array;
+ type: I18nPlaceholderType;
+}
+
+// TODO: These are strings for debugging.
+export const enum I18nPlaceholderType {
+ ELEMENT = 'ELEMENT',
+ SUBTEMPLATE = 'SUBTEMPLATE',
+}
+
+// TODO: These are strings for debugging.
+export const enum I18nNodeKind {
+ TEXT = 'TEXT',
+ ELEMENT = 'ELEMENT',
+ PLACEHOLDER = 'PLACEHOLDER',
+ ICU = 'ICU',
+}