fix(core): Store ICU state in `LView` rather than in `TView` (#39233)

Before this refactoring/fix the ICU would store the current selected
index in `TView`. This is incorrect, since if ICU is in `ngFor` it will
cause issues in some circumstances. This refactoring properly moves the
state to `LView`.

closes #37021
closes #38144
closes #38073

PR Close #39233
This commit is contained in:
Misko Hevery 2020-09-25 15:01:56 -07:00 committed by Alex Rickabaugh
parent 6790848f68
commit ca11ef2376
53 changed files with 3270 additions and 1643 deletions

View File

@ -223,6 +223,7 @@
"packages/core/src/render3/assert.ts",
"packages/core/src/render3/interfaces/container.ts",
"packages/core/src/render3/interfaces/node.ts",
"packages/core/src/render3/interfaces/i18n.ts",
"packages/core/src/render3/interfaces/view.ts",
"packages/core/src/di/injector.ts",
"packages/core/src/di/r3_injector.ts",
@ -239,6 +240,7 @@
"packages/core/src/render3/assert.ts",
"packages/core/src/render3/interfaces/container.ts",
"packages/core/src/render3/interfaces/node.ts",
"packages/core/src/render3/interfaces/i18n.ts",
"packages/core/src/render3/interfaces/view.ts",
"packages/core/src/metadata.ts",
"packages/core/src/di.ts",
@ -262,6 +264,7 @@
"packages/core/src/render3/assert.ts",
"packages/core/src/render3/interfaces/container.ts",
"packages/core/src/render3/interfaces/node.ts",
"packages/core/src/render3/interfaces/i18n.ts",
"packages/core/src/render3/interfaces/view.ts",
"packages/core/src/render3/interfaces/definition.ts",
"packages/core/src/core.ts",
@ -968,11 +971,13 @@
[
"packages/core/src/render3/interfaces/container.ts",
"packages/core/src/render3/interfaces/node.ts",
"packages/core/src/render3/interfaces/i18n.ts",
"packages/core/src/render3/interfaces/view.ts"
],
[
"packages/core/src/render3/interfaces/definition.ts",
"packages/core/src/render3/interfaces/node.ts",
"packages/core/src/render3/interfaces/i18n.ts",
"packages/core/src/render3/interfaces/view.ts"
],
[
@ -980,13 +985,23 @@
"packages/core/src/render3/interfaces/view.ts"
],
[
"packages/core/src/render3/interfaces/node.ts",
"packages/core/src/render3/interfaces/i18n.ts",
"packages/core/src/render3/interfaces/node.ts"
],
[
"packages/core/src/render3/interfaces/i18n.ts",
"packages/core/src/render3/interfaces/view.ts"
],
[
"packages/core/src/render3/interfaces/node.ts",
"packages/core/src/render3/interfaces/i18n.ts",
"packages/core/src/render3/interfaces/view.ts",
"packages/core/src/render3/interfaces/query.ts"
"packages/core/src/render3/interfaces/node.ts"
],
[
"packages/core/src/render3/interfaces/i18n.ts",
"packages/core/src/render3/interfaces/view.ts",
"packages/core/src/render3/interfaces/query.ts",
"packages/core/src/render3/interfaces/node.ts"
],
[
"packages/core/src/render3/interfaces/query.ts",

View File

@ -799,7 +799,7 @@ export declare abstract class Renderer2 {
abstract createElement(name: string, namespace?: string | null): any;
abstract createText(value: string): any;
abstract destroy(): void;
abstract insertBefore(parent: any, newChild: any, refChild: any): void;
abstract insertBefore(parent: any, newChild: any, refChild: any, isMove?: boolean): void;
abstract listen(target: 'window' | 'document' | 'body' | any, eventName: string, callback: (event: any) => boolean | void): () => void;
abstract nextSibling(node: any): any;
abstract parentNode(node: any): any;

View File

@ -12,7 +12,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 3037,
"main-es2015": 447742,
"main-es2015": 448676,
"polyfills-es2015": 52415
}
}

View File

@ -3,7 +3,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 1485,
"main-es2015": 140199,
"main-es2015": 140899,
"polyfills-es2015": 36571
}
}
@ -12,7 +12,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 1485,
"main-es2015": 16650,
"main-es2015": 17092,
"polyfills-es2015": 36657
}
}
@ -21,7 +21,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 1485,
"main-es2015": 146417,
"main-es2015": 147242,
"polyfills-es2015": 36571
}
}
@ -30,7 +30,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 1485,
"main-es2015": 135003,
"main-es2015": 136096,
"polyfills-es2015": 37248
}
}
@ -39,7 +39,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 2289,
"main-es2015": 241850,
"main-es2015": 242460,
"polyfills-es2015": 36938,
"5-es2015": 751
}
@ -49,7 +49,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 2289,
"main-es2015": 217827,
"main-es2015": 218527,
"polyfills-es2015": 36723,
"5-es2015": 781
}

View File

@ -158,8 +158,13 @@ export abstract class Renderer2 {
* @param parent The parent node.
* @param newChild The new child nodes.
* @param refChild The existing child node before which `newChild` is inserted.
* @param isMove Optional argument which signifies if the current `insertBefore` is a result of a
* move. Animation uses this information to trigger move animations. In the past the Animation
* would always assume that any `insertBefore` is a move. This is not strictly true because
* with runtime i18n it is possible to invoke `insertBefore` as a result of i18n and it should
* not trigger an animation move.
*/
abstract insertBefore(parent: any, newChild: any, refChild: any): void;
abstract insertBefore(parent: any, newChild: any, refChild: any, isMove?: boolean): void;
/**
* Implement this callback to remove a child node from the host element's DOM.
* @param parent The parent node.

View File

@ -10,6 +10,7 @@ import {assertDefined, assertEqual, assertNumber, throwError} from '../util/asse
import {getComponentDef, getNgModuleDef} from './definition';
import {LContainer} from './interfaces/container';
import {DirectiveDef} from './interfaces/definition';
import {TIcu} from './interfaces/i18n';
import {NodeInjectorOffset} from './interfaces/injector';
import {TNode} from './interfaces/node';
import {isLContainer, isLView} from './interfaces/type_checks';
@ -25,13 +26,28 @@ export function assertTNodeForLView(tNode: TNode, lView: LView) {
}
export function assertTNodeForTView(tNode: TNode, tView: TView) {
assertDefined(tNode, 'TNode must be defined');
assertTNode(tNode);
tNode.hasOwnProperty('tView_') &&
assertEqual(
(tNode as any as {tView_: TView}).tView_, tView,
'This TNode does not belong to this TView.');
}
export function assertTNode(tNode: TNode) {
assertDefined(tNode, 'TNode must be defined');
if (!(tNode && typeof tNode === 'object' && tNode.hasOwnProperty('directiveStylingLast'))) {
throwError('Not of type TNode, got: ' + tNode);
}
}
export function assertTIcu(tIcu: TIcu) {
assertDefined(tIcu, 'Expected TIcu to be defined');
if (!(typeof tIcu.currentCaseLViewIndex === 'number')) {
throwError('Object is not of TIcu type.');
}
}
export function assertComponentType(
actual: any,
msg: string = 'Type passed in is not ComponentType, it does not have \'ɵcmp\' property.') {
@ -106,18 +122,15 @@ export function assertIndexInDeclRange(lView: LView, index: number) {
export function assertIndexInVarsRange(lView: LView, index: number) {
const tView = lView[1];
assertBetween(
tView.bindingStartIndex, (tView as any as {i18nStartIndex: number}).i18nStartIndex, index);
}
export function assertIndexInI18nRange(lView: LView, index: number) {
const tView = lView[1];
assertBetween(
(tView as any as {i18nStartIndex: number}).i18nStartIndex, tView.expandoStartIndex, index);
tView.bindingStartIndex,
(tView as any as {originalExpandoStartIndex: number}).originalExpandoStartIndex, index);
}
export function assertIndexInExpandoRange(lView: LView, index: number) {
const tView = lView[1];
assertBetween(tView.expandoStartIndex, lView.length, index);
assertBetween(
(tView as any as {originalExpandoStartIndex: number}).originalExpandoStartIndex, lView.length,
index);
}
export function assertBetween(lower: number, upper: number, index: number) {

View File

@ -163,6 +163,7 @@ export function renderComponent<T>(
* @param rNode Render host element.
* @param def ComponentDef
* @param rootView The parent view where the host node is stored
* @param rendererFactory Factory to be used for creating child renderers.
* @param hostRenderer The current renderer
* @param sanitizer The sanitizer, if provided
*
@ -174,7 +175,10 @@ export function createRootComponentView(
const tView = rootView[TVIEW];
ngDevMode && assertIndexInRange(rootView, 0 + HEADER_OFFSET);
rootView[0 + HEADER_OFFSET] = rNode;
const tNode: TElementNode = getOrCreateTNode(tView, 0, TNodeType.Element, null, null);
// '#host' is added here as we don't know the real host DOM name (we don't want to read it) and at
// the same time we want to communicate the the debug `TNode` that this is a special `TNode`
// representing a host element.
const tNode = getOrCreateTNode(tView, 0, TNodeType.Element, '#host', null);
const mergedAttrs = tNode.mergedAttrs = def.hostAttrs;
if (mergedAttrs !== null) {
computeStaticStyling(tNode, mergedAttrs, true);

View File

@ -23,13 +23,13 @@ import {assertComponentType} from './assert';
import {createRootComponent, createRootComponentView, createRootContext, LifecycleHooksFeature} from './component';
import {getComponentDef} from './definition';
import {NodeInjector} from './di';
import {createLView, createTView, elementCreate, locateHostElement, renderView} from './instructions/shared';
import {createLView, createTView, locateHostElement, renderView} from './instructions/shared';
import {ComponentDef} from './interfaces/definition';
import {TContainerNode, TElementContainerNode, TElementNode, TNode} from './interfaces/node';
import {domRendererFactory3, RendererFactory3, RNode} from './interfaces/renderer';
import {LView, LViewFlags, TViewType} from './interfaces/view';
import {MATH_ML_NAMESPACE, SVG_NAMESPACE} from './namespaces';
import {writeDirectClass} from './node_manipulation';
import {createElementNode, writeDirectClass} from './node_manipulation';
import {extractAttrsAndClassesFromSelector, stringifyCSSSelectorList} from './node_selector_matcher';
import {enterView, leaveView} from './state';
import {setUpAttributes} from './util/attrs_utils';
@ -147,8 +147,8 @@ export class ComponentFactory<T> extends viewEngine_ComponentFactory<T> {
const elementName = this.componentDef.selectors[0][0] as string || 'div';
const hostRNode = rootSelectorOrNode ?
locateHostElement(hostRenderer, rootSelectorOrNode, this.componentDef.encapsulation) :
elementCreate(
elementName, rendererFactory.createRenderer(null, this.componentDef),
createElementNode(
rendererFactory.createRenderer(null, this.componentDef), elementName,
getNamespace(elementName));
const rootFlags = this.componentDef.onPush ? LViewFlags.Dirty | LViewFlags.IsRoot :

View File

@ -204,11 +204,7 @@ function findViaNativeElement(lView: LView, target: RElement): number {
* Locates the next tNode (child, sibling or parent).
*/
function traverseNextElement(tNode: TNode): TNode|null {
if (tNode.child && tNode.child.parent === tNode) {
// FIXME(misko): checking if `tNode.child.parent === tNode` should not be necessary
// We have added it here because i18n creates TNode's which are not valid, so this is a work
// around. The i18n code will be refactored in #39003 and once it lands this extra check can be
// deleted.
if (tNode.child) {
return tNode.child;
} else if (tNode.next) {
return tNode.next;

View File

@ -115,10 +115,6 @@ The i18n markers are:
- `index`: the index of the `template` instruction, as defined in the template instructions (e.g. `template(index, ...)`).
- `block`: the index of the parent sub-template block, in which this child sub-template block was declared.
- `<60>!{index}:{block}<7D>/<2F>/!{index}:{block}<7D>`: *Projection block*: Marks the beginning and end of <ng-content> that was embedded in the original translation block.
- `index`: the index of the projection, as defined in the template instructions (e.g. `projection(index, ...)`).
- `block` (*optional*): the index of the parent sub-template block, in which this child sub-template block was declared.
No other i18n marker format is supported.
The i18n markers in the example above can be interpreted as follows:

View File

@ -7,63 +7,108 @@
*/
import {getPluralCase} from '../../i18n/localization';
import {assertDefined, assertEqual, assertIndexInRange} from '../../util/assert';
import {assertDefined, assertDomNode, assertEqual, assertGreaterThan, assertIndexInRange, throwError} from '../../util/assert';
import {assertIndexInExpandoRange, assertTIcu} from '../assert';
import {attachPatchData} from '../context_discovery';
import {elementAttributeInternal, elementPropertyInternal, getOrCreateTNode, textBindingInternal} from '../instructions/shared';
import {LContainer, NATIVE} from '../interfaces/container';
import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nMutateOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes, IcuType, TI18n, TIcu} from '../interfaces/i18n';
import {TElementNode, TIcuContainerNode, TNode, TNodeFlags, TNodeType, TProjectionNode} from '../interfaces/node';
import {RComment, RElement, RText} from '../interfaces/renderer';
import {elementPropertyInternal, setElementAttribute, textBindingInternal} from '../instructions/shared';
import {COMMENT_MARKER, ELEMENT_MARKER, getCurrentICUCaseIndex, getParentFromI18nMutateOpCode, getRefFromI18nMutateOpCode, I18nCreateOpCode, I18nCreateOpCodes, I18nMutateOpCode, I18nMutateOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes, IcuType, TI18n, TIcu} from '../interfaces/i18n';
import {TNode} from '../interfaces/node';
import {RElement, RNode, RText} from '../interfaces/renderer';
import {SanitizerFn} from '../interfaces/sanitization';
import {isLContainer} from '../interfaces/type_checks';
import {HEADER_OFFSET, LView, RENDERER, T_HOST, TView} from '../interfaces/view';
import {appendChild, applyProjection, createTextNode, nativeRemoveNode} from '../node_manipulation';
import {getBindingIndex, getCurrentTNode, getLView, getTView, setCurrentTNode, setCurrentTNodeAsNotParent} from '../state';
import {HEADER_OFFSET, LView, RENDERER, TView} from '../interfaces/view';
import {createCommentNode, createElementNode, createTextNode, nativeInsertBefore, nativeParentNode, nativeRemoveNode, updateTextNode} from '../node_manipulation';
import {getBindingIndex} from '../state';
import {renderStringify} from '../util/misc_utils';
import {getNativeByIndex, getNativeByTNode, getTNode, load} from '../util/view_utils';
import {getNativeByIndex, unwrapRNode} from '../util/view_utils';
import {getLocaleId} from './i18n_locale_id';
import {getTIcu} from './i18n_util';
const i18nIndexStack: number[] = [];
let i18nIndexStackPointer = -1;
function popI18nIndex() {
return i18nIndexStack[i18nIndexStackPointer--];
}
export function pushI18nIndex(index: number) {
i18nIndexStack[++i18nIndexStackPointer] = index;
}
/**
* Keep track of which input bindings in `ɵɵi18nExp` have changed.
*
* This is used to efficiently update expressions in i18n only when the corresponding input has
* changed.
*
* 1) Each bit represents which of the `ɵɵi18nExp` has changed.
* 2) There are 32 bits allowed in JS.
* 3) Bit 32 is special as it is shared for all changes past 32. (In other words if you have more
* than 32 `ɵɵi18nExp` then all changes past 32nd `ɵɵi18nExp` will be mapped to same bit. This means
* that we may end up changing more than we need to. But i18n expressions with 32 bindings is rare
* so in practice it should not be an issue.)
*/
let changeMask = 0b0;
let shiftsCounter = 0;
export function setMaskBit(bit: boolean) {
if (bit) {
changeMask = changeMask | (1 << shiftsCounter);
/**
* Keeps track of which bit needs to be updated in `changeMask`
*
* This value gets incremented on every call to `ɵɵi18nExp`
*/
let changeMaskCounter = 0;
/**
* Keep track of which input bindings in `ɵɵi18nExp` have changed.
*
* `setMaskBit` gets invoked by each call to `ɵɵi18nExp`.
*
* @param hasChange did `ɵɵi18nExp` detect a change.
*/
export function setMaskBit(hasChange: boolean) {
if (hasChange) {
changeMask = changeMask | (1 << Math.min(changeMaskCounter, 31));
}
shiftsCounter++;
changeMaskCounter++;
}
export function applyI18n(tView: TView, lView: LView, index: number) {
if (shiftsCounter > 0) {
if (changeMaskCounter > 0) {
ngDevMode && assertDefined(tView, `tView should be defined`);
const tI18n = tView.data[index + HEADER_OFFSET] as TI18n | I18nUpdateOpCodes;
let updateOpCodes: I18nUpdateOpCodes;
let tIcus: TIcu[]|null = null;
if (Array.isArray(tI18n)) {
updateOpCodes = tI18n as I18nUpdateOpCodes;
} else {
updateOpCodes = (tI18n as TI18n).update;
tIcus = (tI18n as TI18n).icus;
}
const bindingsStartIndex = getBindingIndex() - shiftsCounter - 1;
applyUpdateOpCodes(tView, tIcus, lView, updateOpCodes, bindingsStartIndex, changeMask);
// When `index` points to an `ɵɵi18nAttributes` then we have an array otherwise `TI18n`
const updateOpCodes: I18nUpdateOpCodes =
Array.isArray(tI18n) ? tI18n as I18nUpdateOpCodes : (tI18n as TI18n).update;
const bindingsStartIndex = getBindingIndex() - changeMaskCounter - 1;
applyUpdateOpCodes(tView, lView, updateOpCodes, bindingsStartIndex, changeMask);
}
// Reset changeMask & maskBit to default for the next update cycle
changeMask = 0b0;
changeMaskCounter = 0;
}
// Reset changeMask & maskBit to default for the next update cycle
changeMask = 0b0;
shiftsCounter = 0;
/**
* Apply `I18nCreateOpCodes` op-codes as stored in `TI18n.create`.
*
* Creates text (and comment) nodes which are internationalized.
*
* @param lView Current lView
* @param createOpCodes Set of op-codes to apply
* @param parentRNode Parent node (so that direct children can be added eagerly) or `null` if it is
* a root node.
* @param insertInFrontOf DOM node that should be used as an anchor.
*/
export function applyCreateOpCodes(
lView: LView, createOpCodes: I18nCreateOpCodes, parentRNode: RElement|null,
insertInFrontOf: RElement|null): void {
const renderer = lView[RENDERER];
for (let i = 0; i < createOpCodes.length; i++) {
const opCode = createOpCodes[i++] as any;
const text = createOpCodes[i] as string;
const isComment = (opCode & I18nCreateOpCode.COMMENT) === I18nCreateOpCode.COMMENT;
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) {
nativeInsertBefore(renderer, parentRNode, rNode, insertInFrontOf, false);
}
}
}
@ -71,72 +116,86 @@ export function applyI18n(tView: TView, lView: LView, index: number) {
* Apply `I18nMutateOpCodes` OpCodes.
*
* @param tView Current `TView`
* @param rootIndex Pointer to the root (parent) tNode for the i18n.
* @param createOpCodes OpCodes to process
* @param mutableOpCodes Mutable OpCodes to process
* @param lView Current `LView`
* @param anchorRNode place where the i18n node should be inserted.
*/
export function applyCreateOpCodes(
tView: TView, rootindex: number, createOpCodes: I18nMutateOpCodes, lView: LView): number[] {
export function applyMutableOpCodes(
tView: TView, mutableOpCodes: I18nMutateOpCodes, lView: LView, anchorRNode: RNode): void {
ngDevMode && assertDomNode(anchorRNode);
const renderer = lView[RENDERER];
let currentTNode: TNode|null = null;
let previousTNode: TNode|null = null;
const visitedNodes: number[] = [];
for (let i = 0; i < createOpCodes.length; i++) {
const opCode = createOpCodes[i];
// `rootIdx` represents the node into which all inserts happen.
let rootIdx: number|null = null;
// `rootRNode` represents the real node into which we insert. This can be different from
// `lView[rootIdx]` if we have projection.
// - null we don't have a parent (as can be the case in when we are inserting into a root of
// LView which has no parent.)
// - `RElement` The element representing the root after taking projection into account.
let rootRNode!: RElement|null;
for (let i = 0; i < mutableOpCodes.length; i++) {
const opCode = mutableOpCodes[i];
if (typeof opCode == 'string') {
const textRNode = createTextNode(opCode, renderer);
const textNodeIndex = createOpCodes[++i] as number;
ngDevMode && ngDevMode.rendererCreateTextNode++;
previousTNode = currentTNode;
currentTNode =
createDynamicNodeAtIndex(tView, lView, textNodeIndex, TNodeType.Element, textRNode, null);
visitedNodes.push(textNodeIndex);
setCurrentTNodeAsNotParent();
const textNodeIndex = mutableOpCodes[++i] as number;
if (lView[textNodeIndex] === null) {
ngDevMode && ngDevMode.rendererCreateTextNode++;
ngDevMode && assertIndexInRange(lView, textNodeIndex);
lView[textNodeIndex] = createTextNode(renderer, opCode);
}
} else if (typeof opCode == 'number') {
switch (opCode & I18nMutateOpCode.MASK_INSTRUCTION) {
case I18nMutateOpCode.AppendChild:
const destinationNodeIndex = opCode >>> I18nMutateOpCode.SHIFT_PARENT;
let destinationTNode: TNode;
if (destinationNodeIndex === rootindex) {
// If the destination node is `i18nStart`, we don't have a
// top-level node and we should use the host node instead
destinationTNode = lView[T_HOST]!;
const parentIdx = getParentFromI18nMutateOpCode(opCode);
if (rootIdx === null) {
// The first operation should save the `rootIdx` because the first operation
// must insert into the root. (Only subsequent operations can insert into a dynamic
// parent)
rootIdx = parentIdx;
rootRNode = nativeParentNode(renderer, anchorRNode);
}
let insertInFrontOf: RNode|null;
let parentRNode: RElement|null;
if (parentIdx === rootIdx) {
insertInFrontOf = anchorRNode;
parentRNode = rootRNode;
} else {
destinationTNode = getTNode(tView, destinationNodeIndex);
insertInFrontOf = null;
parentRNode = unwrapRNode(lView[parentIdx]) as RElement;
}
ngDevMode &&
assertDefined(
currentTNode!,
`You need to create or select a node before you can insert it into the DOM`);
previousTNode =
appendI18nNode(tView, currentTNode!, destinationTNode, previousTNode, lView);
break;
case I18nMutateOpCode.Select:
// Negative indices indicate that a given TNode is a sibling node, not a parent node
// (see `i18nStartFirstPass` for additional information).
const isParent = opCode >= 0;
// FIXME(misko): This SHIFT_REF looks suspect as it does not have mask.
const nodeIndex = (isParent ? opCode : ~opCode) >>> I18nMutateOpCode.SHIFT_REF;
visitedNodes.push(nodeIndex);
previousTNode = currentTNode;
currentTNode = getTNode(tView, nodeIndex);
if (currentTNode) {
setCurrentTNode(currentTNode, isParent);
// FIXME(misko): Refactor with `processI18nText`
if (parentRNode !== null) {
// This can happen if the `LView` we are adding to is not attached to a parent `LView`.
// In such a case there is no "root" we can attach to. This is fine, as we still need to
// create the elements. When the `LView` gets later added to a parent these "root" nodes
// get picked up and added.
ngDevMode && assertDomNode(parentRNode);
const refIdx = getRefFromI18nMutateOpCode(opCode);
ngDevMode && assertGreaterThan(refIdx, HEADER_OFFSET, 'Missing ref');
// `unwrapRNode` is not needed here as all of these point to RNodes as part of the i18n
// which can't have components.
const child = lView[refIdx] as RElement;
ngDevMode && assertDomNode(child);
nativeInsertBefore(renderer, parentRNode, child, insertInFrontOf, false);
const tIcu = getTIcu(tView, refIdx);
if (tIcu !== null && typeof tIcu === 'object') {
// If we just added a comment node which has ICU then that ICU may have already been
// rendered and therefore we need to re-add it here.
ngDevMode && assertTIcu(tIcu);
const caseIndex = getCurrentICUCaseIndex(tIcu, lView);
if (caseIndex !== null) {
applyMutableOpCodes(tView, tIcu.create[caseIndex], lView, lView[tIcu.anchorIdx]);
}
}
}
break;
case I18nMutateOpCode.ElementEnd:
const elementIndex = opCode >>> I18nMutateOpCode.SHIFT_REF;
previousTNode = currentTNode = getTNode(tView, elementIndex);
setCurrentTNode(currentTNode, false);
break;
case I18nMutateOpCode.Attr:
const elementNodeIndex = opCode >>> I18nMutateOpCode.SHIFT_REF;
const attrName = createOpCodes[++i] as string;
const attrValue = createOpCodes[++i] as string;
const attrName = mutableOpCodes[++i] as string;
const attrValue = mutableOpCodes[++i] as string;
// This code is used for ICU expressions only, since we don't support
// directives/components in ICUs, we don't need to worry about inputs here
elementAttributeInternal(
getTNode(tView, elementNodeIndex), lView, attrName, attrValue, null, null);
setElementAttribute(
renderer, getNativeByIndex(elementNodeIndex - HEADER_OFFSET, lView) as RElement, null,
null, attrName, attrValue, null);
break;
default:
throw new Error(`Unable to determine the type of mutate operation for "${opCode}"`);
@ -144,45 +203,44 @@ export function applyCreateOpCodes(
} else {
switch (opCode) {
case COMMENT_MARKER:
const commentValue = createOpCodes[++i] as string;
const commentNodeIndex = createOpCodes[++i] as number;
ngDevMode &&
assertEqual(
typeof commentValue, 'string',
`Expected "${commentValue}" to be a comment node value`);
const commentRNode = renderer.createComment(commentValue);
ngDevMode && ngDevMode.rendererCreateComment++;
previousTNode = currentTNode;
currentTNode = createDynamicNodeAtIndex(
tView, lView, commentNodeIndex, TNodeType.IcuContainer, commentRNode, null);
visitedNodes.push(commentNodeIndex);
attachPatchData(commentRNode, lView);
// We will add the case nodes later, during the update phase
setCurrentTNodeAsNotParent();
const commentValue = mutableOpCodes[++i] as string;
const commentNodeIndex = mutableOpCodes[++i] as number;
if (lView[commentNodeIndex] === null) {
ngDevMode &&
assertEqual(
typeof commentValue, 'string',
`Expected "${commentValue}" to be a comment node value`);
ngDevMode && ngDevMode.rendererCreateComment++;
ngDevMode && assertIndexInExpandoRange(lView, commentNodeIndex);
const commentRNode = lView[commentNodeIndex] =
createCommentNode(renderer, commentValue);
// FIXME(misko): Attaching patch data is only needed for the root (Also add tests)
attachPatchData(commentRNode, lView);
}
break;
case ELEMENT_MARKER:
const tagNameValue = createOpCodes[++i] as string;
const elementNodeIndex = createOpCodes[++i] as number;
ngDevMode &&
assertEqual(
typeof tagNameValue, 'string',
`Expected "${tagNameValue}" to be an element node tag name`);
const elementRNode = renderer.createElement(tagNameValue);
ngDevMode && ngDevMode.rendererCreateElement++;
previousTNode = currentTNode;
currentTNode = createDynamicNodeAtIndex(
tView, lView, elementNodeIndex, TNodeType.Element, elementRNode, tagNameValue);
visitedNodes.push(elementNodeIndex);
const tagName = mutableOpCodes[++i] as string;
const elementNodeIndex = mutableOpCodes[++i] as number;
if (lView[elementNodeIndex] === null) {
ngDevMode &&
assertEqual(
typeof tagName, 'string',
`Expected "${tagName}" to be an element node tag name`);
ngDevMode && ngDevMode.rendererCreateElement++;
ngDevMode && assertIndexInExpandoRange(lView, elementNodeIndex);
const elementRNode = lView[elementNodeIndex] =
createElementNode(renderer, tagName, null);
// FIXME(misko): Attaching patch data is only needed for the root (Also add tests)
attachPatchData(elementRNode, lView);
}
break;
default:
throw new Error(`Unable to determine the type of mutate operation for "${opCode}"`);
ngDevMode &&
throwError(`Unable to determine the type of mutate operation for "${opCode}"`);
}
}
}
setCurrentTNodeAsNotParent();
return visitedNodes;
}
@ -190,7 +248,6 @@ export function applyCreateOpCodes(
* Apply `I18nUpdateOpCodes` OpCodes
*
* @param tView Current `TView`
* @param tIcus If ICUs present than this contains them.
* @param lView Current `LView`
* @param updateOpCodes OpCodes to process
* @param bindingsStartIndex Location of the first `ɵɵi18nApply`
@ -198,9 +255,8 @@ export function applyCreateOpCodes(
* `bindingsStartIndex`)
*/
export function applyUpdateOpCodes(
tView: TView, tIcus: TIcu[]|null, lView: LView, updateOpCodes: I18nUpdateOpCodes,
bindingsStartIndex: number, changeMask: number) {
let caseCreated = false;
tView: TView, lView: LView, updateOpCodes: I18nUpdateOpCodes, bindingsStartIndex: number,
changeMask: number) {
for (let i = 0; i < updateOpCodes.length; i++) {
// bit code to check if we should apply the next update
const checkBit = updateOpCodes[i] as number;
@ -218,31 +274,54 @@ export function applyUpdateOpCodes(
// Negative opCode represent `i18nExp` values offset.
value += renderStringify(lView[bindingsStartIndex - opCode]);
} else {
const nodeIndex = opCode >>> I18nUpdateOpCode.SHIFT_REF;
const nodeIndex = (opCode >>> I18nUpdateOpCode.SHIFT_REF);
switch (opCode & I18nUpdateOpCode.MASK_OPCODE) {
case I18nUpdateOpCode.Attr:
const propName = updateOpCodes[++j] as string;
const sanitizeFn = updateOpCodes[++j] as SanitizerFn | null;
elementPropertyInternal(
tView, getTNode(tView, nodeIndex), lView, propName, value, lView[RENDERER],
sanitizeFn, false);
const tNodeOrTagName = tView.data[nodeIndex] as TNode | string;
ngDevMode && assertDefined(tNodeOrTagName, 'Expecting TNode or string');
if (typeof tNodeOrTagName === 'string') {
// IF we don't have a `TNode`, then we are an element in ICU (as ICU content does
// not have TNode), in which case we know that there are no directives, and hence
// we use attribute setting.
setElementAttribute(
lView[RENDERER], lView[nodeIndex], null, tNodeOrTagName, propName, value,
sanitizeFn);
} else {
elementPropertyInternal(
tView, tNodeOrTagName, lView, propName, value, lView[RENDERER], sanitizeFn,
false);
}
break;
case I18nUpdateOpCode.Text:
textBindingInternal(lView, nodeIndex, value);
const rText = lView[nodeIndex] as RText | null;
rText !== null && updateTextNode(lView[RENDERER], rText, value);
break;
case I18nUpdateOpCode.IcuSwitch:
caseCreated =
applyIcuSwitchCase(tView, tIcus!, updateOpCodes[++j] as number, lView, value);
applyIcuSwitchCase(tView, getTIcu(tView, nodeIndex)!, lView, value);
break;
case I18nUpdateOpCode.IcuUpdate:
applyIcuUpdateCase(
tView, tIcus!, updateOpCodes[++j] as number, bindingsStartIndex, lView,
caseCreated);
applyIcuUpdateCase(tView, getTIcu(tView, nodeIndex)!, bindingsStartIndex, lView);
break;
}
}
}
}
} else {
const opCode = updateOpCodes[i + 1] as number;
if (opCode > 0 && (opCode & I18nUpdateOpCode.MASK_OPCODE) === I18nUpdateOpCode.IcuUpdate) {
// Special case for the `icuUpdateCase`. It could be that the mask did not match, but
// we still need to execute `icuUpdateCase` because the case has changed recently due to
// previous `icuSwitchCase` instruction. (`icuSwitchCase` and `icuUpdateCase` always come in
// pairs.)
const nodeIndex = (opCode >>> I18nUpdateOpCode.SHIFT_REF);
const tIcu = getTIcu(tView, nodeIndex)!;
const currentIndex = lView[tIcu.currentCaseLViewIndex];
if (currentIndex < 0) {
applyIcuUpdateCase(tView, tIcu, bindingsStartIndex, lView);
}
}
}
i += skipCodes;
}
@ -252,25 +331,23 @@ export function applyUpdateOpCodes(
* Apply OpCodes associated with updating an existing ICU.
*
* @param tView Current `TView`
* @param tIcus ICUs active at this location.
* @param tIcuIndex Index into `tIcus` to process.
* @param tIcu Current `TIcu`
* @param bindingsStartIndex Location of the first `ɵɵi18nApply`
* @param lView Current `LView`
* @param changeMask Each bit corresponds to a `ɵɵi18nExp` (Counting backwards from
* `bindingsStartIndex`)
*/
function applyIcuUpdateCase(
tView: TView, tIcus: TIcu[], tIcuIndex: number, bindingsStartIndex: number, lView: LView,
caseCreated: boolean) {
ngDevMode && assertIndexInRange(tIcus, tIcuIndex);
const tIcu = tIcus[tIcuIndex];
function applyIcuUpdateCase(tView: TView, tIcu: TIcu, bindingsStartIndex: number, lView: LView) {
ngDevMode && assertIndexInRange(lView, tIcu.currentCaseLViewIndex);
const activeCaseIndex = lView[tIcu.currentCaseLViewIndex];
let activeCaseIndex = lView[tIcu.currentCaseLViewIndex];
if (activeCaseIndex !== null) {
const mask = caseCreated ?
-1 : // -1 is same as all bits on, which simulates creation since it marks all bits dirty
changeMask;
applyUpdateOpCodes(tView, tIcus, lView, tIcu.update[activeCaseIndex], bindingsStartIndex, mask);
let mask = changeMask;
if (activeCaseIndex < 0) {
// Clear the flag.
// Negative number means that the ICU was freshly created and we need to force the update.
activeCaseIndex = lView[tIcu.currentCaseLViewIndex] = ~activeCaseIndex;
// -1 is same as all bits on, which simulates creation since it marks all bits dirty
mask = -1;
}
applyUpdateOpCodes(tView, lView, tIcu.update[activeCaseIndex], bindingsStartIndex, mask);
}
}
@ -280,48 +357,39 @@ function applyIcuUpdateCase(
* This involves tearing down existing case and than building up a new case.
*
* @param tView Current `TView`
* @param tIcus ICUs active at this location.
* @param tICuIndex Index into `tIcus` to process.
* @param tIcu Current `TIcu`
* @param lView Current `LView`
* @param value Value of the case to update to.
* @returns true if a new case was created (needed so that the update executes regardless of the
* bitmask)
*/
function applyIcuSwitchCase(
tView: TView, tIcus: TIcu[], tICuIndex: number, lView: LView, value: string): boolean {
applyIcuSwitchCaseRemove(tView, tIcus, tICuIndex, lView);
function applyIcuSwitchCase(tView: TView, tIcu: TIcu, lView: LView, value: string) {
// Rebuild a new case for this ICU
let caseCreated = false;
const tIcu = tIcus[tICuIndex];
const caseIndex = getCaseIndex(tIcu, value);
lView[tIcu.currentCaseLViewIndex] = caseIndex !== -1 ? caseIndex : null;
if (caseIndex > -1) {
// Add the nodes for the new case
applyCreateOpCodes(
tView, -1, // -1 means we don't have parent node
tIcu.create[caseIndex], lView);
caseCreated = true;
let activeCaseIndex = getCurrentICUCaseIndex(tIcu, lView);
if (activeCaseIndex !== caseIndex) {
applyIcuSwitchCaseRemove(tView, tIcu, lView);
lView[tIcu.currentCaseLViewIndex] = caseIndex === null ? null : ~caseIndex;
if (caseIndex !== null) {
// Add the nodes for the new case
const anchorRNode = lView[tIcu.anchorIdx];
if (anchorRNode) {
ngDevMode && assertDomNode(anchorRNode);
applyMutableOpCodes(tView, tIcu.create[caseIndex], lView, anchorRNode);
}
}
}
return caseCreated;
}
/**
* Apply OpCodes associated with tearing down of DOM.
* Apply OpCodes associated with tearing ICU case.
*
* This involves tearing down existing case and than building up a new case.
*
* @param tView Current `TView`
* @param tIcus ICUs active at this location.
* @param tIcuIndex Index into `tIcus` to process.
* @param tIcu Current `TIcu`
* @param lView Current `LView`
* @returns true if a new case was created (needed so that the update executes regardless of the
* bitmask)
*/
function applyIcuSwitchCaseRemove(tView: TView, tIcus: TIcu[], tIcuIndex: number, lView: LView) {
ngDevMode && assertIndexInRange(tIcus, tIcuIndex);
const tIcu = tIcus[tIcuIndex];
const activeCaseIndex = lView[tIcu.currentCaseLViewIndex];
function applyIcuSwitchCaseRemove(tView: TView, tIcu: TIcu, lView: LView) {
let activeCaseIndex = getCurrentICUCaseIndex(tIcu, lView);
if (activeCaseIndex !== null) {
const removeCodes = tIcu.remove[activeCaseIndex];
for (let k = 0; k < removeCodes.length; k++) {
@ -329,158 +397,17 @@ function applyIcuSwitchCaseRemove(tView: TView, tIcus: TIcu[], tIcuIndex: number
const nodeOrIcuIndex = removeOpCode >>> I18nMutateOpCode.SHIFT_REF;
switch (removeOpCode & I18nMutateOpCode.MASK_INSTRUCTION) {
case I18nMutateOpCode.Remove:
// FIXME(misko): this comment is wrong!
// Remove DOM element, but do *not* mark TNode as detached, since we are
// just switching ICU cases (while keeping the same TNode), so a DOM element
// representing a new ICU case will be re-created.
removeNode(tView, lView, nodeOrIcuIndex, /* markAsDetached */ false);
nativeRemoveNode(
lView[RENDERER], getNativeByIndex(nodeOrIcuIndex - HEADER_OFFSET, lView));
break;
case I18nMutateOpCode.RemoveNestedIcu:
applyIcuSwitchCaseRemove(tView, tIcus, nodeOrIcuIndex, lView);
applyIcuSwitchCaseRemove(tView, getTIcu(tView, nodeOrIcuIndex)!, lView);
break;
}
}
}
}
function appendI18nNode(
tView: TView, tNode: TNode, parentTNode: TNode, previousTNode: TNode|null,
lView: LView): TNode {
ngDevMode && ngDevMode.rendererMoveNode++;
const nextNode = tNode.next;
if (!previousTNode) {
previousTNode = parentTNode;
}
// Re-organize node tree to put this node in the correct position.
if (previousTNode === parentTNode && tNode !== parentTNode.child) {
tNode.next = parentTNode.child;
// FIXME(misko): Checking `tNode.parent` is a temporary workaround until we properly
// refactor the i18n code in #38707 and this code will be deleted.
if (tNode.parent === null) {
tView.firstChild = tNode;
} else {
parentTNode.child = tNode;
}
} else if (previousTNode !== parentTNode && tNode !== previousTNode.next) {
tNode.next = previousTNode.next;
previousTNode.next = tNode;
} else {
tNode.next = null;
}
if (parentTNode !== lView[T_HOST]) {
tNode.parent = parentTNode as TElementNode;
}
// If tNode was moved around, we might need to fix a broken link.
let cursor: TNode|null = tNode.next;
while (cursor) {
if (cursor.next === tNode) {
cursor.next = nextNode;
}
cursor = cursor.next;
}
// If the placeholder to append is a projection, we need to move the projected nodes instead
if (tNode.type === TNodeType.Projection) {
applyProjection(tView, lView, tNode as TProjectionNode);
return tNode;
}
appendChild(tView, lView, getNativeByTNode(tNode, lView), tNode);
const slotValue = lView[tNode.index];
if (tNode.type !== TNodeType.Container && isLContainer(slotValue)) {
// Nodes that inject ViewContainerRef also have a comment node that should be moved
appendChild(tView, lView, slotValue[NATIVE], tNode);
}
return tNode;
}
/**
* See `i18nEnd` above.
*/
export function i18nEndFirstPass(tView: TView, lView: LView) {
ngDevMode &&
assertEqual(
getBindingIndex(), tView.bindingStartIndex,
'i18nEnd should be called before any binding');
const rootIndex = popI18nIndex();
const tI18n = tView.data[rootIndex + HEADER_OFFSET] as TI18n;
ngDevMode && assertDefined(tI18n, `You should call i18nStart before i18nEnd`);
// Find the last node that was added before `i18nEnd`
const lastCreatedNode = getCurrentTNode();
// Read the instructions to insert/move/remove DOM elements
const visitedNodes = applyCreateOpCodes(tView, rootIndex, tI18n.create, lView);
// Remove deleted nodes
let index = rootIndex + 1;
while (lastCreatedNode !== null && index <= lastCreatedNode.index - HEADER_OFFSET) {
if (visitedNodes.indexOf(index) === -1) {
removeNode(tView, lView, index, /* markAsDetached */ true);
}
// Check if an element has any local refs and skip them
const tNode = getTNode(tView, index);
if (tNode &&
(tNode.type === TNodeType.Container || tNode.type === TNodeType.Element ||
tNode.type === TNodeType.ElementContainer) &&
tNode.localNames !== null) {
// Divide by 2 to get the number of local refs,
// since they are stored as an array that also includes directive indexes,
// i.e. ["localRef", directiveIndex, ...]
index += tNode.localNames.length >> 1;
}
index++;
}
}
function removeNode(tView: TView, lView: LView, index: number, markAsDetached: boolean) {
const removedPhTNode = getTNode(tView, index);
const removedPhRNode = getNativeByIndex(index, lView);
if (removedPhRNode) {
nativeRemoveNode(lView[RENDERER], removedPhRNode);
}
const slotValue = load(lView, index) as RElement | RComment | LContainer;
if (isLContainer(slotValue)) {
const lContainer = slotValue as LContainer;
if (removedPhTNode.type !== TNodeType.Container) {
nativeRemoveNode(lView[RENDERER], lContainer[NATIVE]);
}
}
if (markAsDetached && removedPhTNode) {
// Define this node as detached to avoid projecting it later
removedPhTNode.flags |= TNodeFlags.isDetached;
}
ngDevMode && ngDevMode.rendererRemoveNode++;
}
/**
* Creates and stores the dynamic TNode, and unhooks it from the tree for now.
*/
function createDynamicNodeAtIndex(
tView: TView, lView: LView, index: number, type: TNodeType, native: RElement|RText|null,
name: string|null): TElementNode|TIcuContainerNode {
const currentTNode = getCurrentTNode();
ngDevMode && assertIndexInRange(lView, index + HEADER_OFFSET);
lView[index + HEADER_OFFSET] = native;
// FIXME(misko): Why does this create A TNode??? I would not expect this to be here.
const tNode = getOrCreateTNode(tView, index, type as any, name, null);
// We are creating a dynamic node, the previous tNode might not be pointing at this node.
// We will link ourselves into the tree later with `appendI18nNode`.
if (currentTNode && currentTNode.next === tNode) {
currentTNode.next = null;
}
return tNode;
}
/**
* Returns the index of the current case of an ICU expression depending on the main binding value
@ -488,7 +415,7 @@ function createDynamicNodeAtIndex(
* @param icuExpression
* @param bindingValue The value of the main binding used by this ICU expression
*/
function getCaseIndex(icuExpression: TIcu, bindingValue: string): number {
function getCaseIndex(icuExpression: TIcu, bindingValue: string): number|null {
let index = icuExpression.cases.indexOf(bindingValue);
if (index === -1) {
switch (icuExpression.type) {
@ -506,5 +433,5 @@ function getCaseIndex(icuExpression: TIcu, bindingValue: string): number {
}
}
}
return index;
return index === -1 ? null : index;
}

View File

@ -7,8 +7,38 @@
*/
import {assertNumber, assertString} from '../../util/assert';
import {COMMENT_MARKER, ELEMENT_MARKER, getInstructionFromI18nMutateOpCode, getParentFromI18nMutateOpCode, getRefFromI18nMutateOpCode, I18nCreateOpCode, I18nMutateOpCode, I18nMutateOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes} from '../interfaces/i18n';
import {COMMENT_MARKER, ELEMENT_MARKER, getInstructionFromI18nMutateOpCode, getParentFromI18nMutateOpCode, getRefFromI18nMutateOpCode, I18nMutateOpCode, I18nMutateOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes} from '../interfaces/i18n';
/**
* Converts `I18nCreateOpCodes` array into a human readable format.
*
* This function is attached to the `I18nCreateOpCodes.debug` property if `ngDevMode` is enabled.
* This function provides a human readable view of the opcodes. This is useful when debugging the
* application as well as writing more readable tests.
*
* @param this `I18nCreateOpCodes` if attached as a method.
* @param opcodes `I18nCreateOpCodes` if invoked as a function.
*/
export function i18nCreateOpCodesToString(
this: I18nUpdateOpCodes|void, opcodes?: I18nUpdateOpCodes): string[] {
const createOpCodes: I18nUpdateOpCodes = opcodes || (Array.isArray(this) ? this : []);
let lines: string[] = [];
for (let i = 0; i < createOpCodes.length; i++) {
const opCode = createOpCodes[i++] as any;
const text = createOpCodes[i] as string;
const isComment = (opCode & I18nCreateOpCode.COMMENT) === I18nCreateOpCode.COMMENT;
const appendNow =
(opCode & I18nCreateOpCode.APPEND_EAGERLY) === I18nCreateOpCode.APPEND_EAGERLY;
const index = opCode >>> I18nCreateOpCode.SHIFT;
lines.push(`lView[${index}] = document.${isComment ? 'createComment' : 'createText'}(${
JSON.stringify(text)});`);
if (appendNow) {
lines.push(`parent.appendChild(lView[${index}]);`);
}
}
return lines;
}
/**
* Converts `I18nUpdateOpCodes` array into a human readable format.
@ -37,9 +67,9 @@ export function i18nUpdateOpCodesToString(
const value = sanitizationFn ? `(${sanitizationFn})($$$)` : '$$$';
return `(lView[${ref}] as Element).setAttribute('${attrName}', ${value})`;
case I18nUpdateOpCode.IcuSwitch:
return `icuSwitchCase(lView[${ref}] as Comment, ${parser.consumeNumber()}, $$$)`;
return `icuSwitchCase(${ref}, $$$)`;
case I18nUpdateOpCode.IcuUpdate:
return `icuUpdateCase(lView[${ref}] as Comment, ${parser.consumeNumber()})`;
return `icuUpdateCase(${ref})`;
}
throw new Error('unexpected OpCode');
}
@ -57,7 +87,9 @@ export function i18nUpdateOpCodesToString(
statement += value;
} else if (value < 0) {
// Negative numbers are ref indexes
statement += '${lView[' + (0 - value) + ']}';
// Here `i` refers to current binding index. It is to signify that the value is relative,
// rather than absolute.
statement += '${lView[i' + value + ']}';
} else {
// Positive numbers are operations.
const opCodeText = consumeOpCode(value);
@ -89,9 +121,6 @@ export function i18nMutateOpCodesToString(
const parent = getParentFromI18nMutateOpCode(opCode);
const ref = getRefFromI18nMutateOpCode(opCode);
switch (getInstructionFromI18nMutateOpCode(opCode)) {
case I18nMutateOpCode.Select:
lastRef = ref;
return '';
case I18nMutateOpCode.AppendChild:
return `(lView[${parent}] as Element).appendChild(lView[${lastRef}])`;
case I18nMutateOpCode.Remove:
@ -99,8 +128,6 @@ export function i18nMutateOpCodesToString(
case I18nMutateOpCode.Attr:
return `(lView[${ref}] as Element).setAttribute("${parser.consumeString()}", "${
parser.consumeString()}")`;
case I18nMutateOpCode.ElementEnd:
return `setCurrentTNode(tView.data[${ref}] as TNode)`;
case I18nMutateOpCode.RemoveNestedIcu:
return `removeNestedICU(${ref})`;
}

View File

@ -0,0 +1,86 @@
/**
* @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 {assertEqual} from '../../util/assert';
import {TNode, TNodeType} from '../interfaces/node';
/**
* Add `tNode` to `previousTNodes` list and update relevant `TNode`s in `previousTNodes` list
* `tNode.insertBeforeIndex`.
*
* Things to keep in mind:
* 1. All i18n text nodes are encoded as `TNodeType.Element` and are created eagerly by the
* `ɵɵi18nStart` instruction.
* 2. All `TNodeType.Placeholder` `TNodes` are elements which will be created later by
* `ɵɵelementStart` instruction.
* 3. `ɵɵelementStart` instruction will create `TNode`s in the ascending `TNode.index` order. (So a
* smaller index `TNode` is guaranteed to be created before a larger one)
*
* We use the above three invariants to determine `TNode.insertBeforeIndex`.
*
* In an ideal world `TNode.insertBeforeIndex` would always be `TNode.next.index`. However,
* this will not work because `TNode.next.index` may be larger than `TNode.index` which means that
* the next node is not yet created and therefore we can't insert in front of it.
*
* Rule1: `TNode.insertBeforeIndex = null` if `TNode.next === null` (Initial condition, as we don't
* know if there will be further `TNode`s inserted after.)
* Rule2: If `previousTNode` is created after the `tNode` being inserted, then
* `previousTNode.insertBeforeNode = tNode.index` (So when a new `tNode` is added we check
* previous to see if we can update its `insertBeforeTNode`)
*
* See `TNode.insertBeforeIndex` for more context.
*
* @param previousTNodes A list of previous TNodes so that we can easily traverse `TNode`s in
* reverse order. (If `TNode` would have `previous` this would not be necessary.)
* @param newTNode A TNode to add to the `previousTNodes` list.
*/
export function addTNodeAndUpdateInsertBeforeIndex(previousTNodes: TNode[], newTNode: TNode) {
// Start with Rule1
ngDevMode &&
assertEqual(newTNode.insertBeforeIndex, null, 'We expect that insertBeforeIndex is not set');
previousTNodes.push(newTNode);
if (previousTNodes.length > 1) {
for (let i = previousTNodes.length - 2; i >= 0; i--) {
const existingTNode = previousTNodes[i];
// Text nodes are created eagerly and so they don't need their `indexBeforeIndex` updated.
// It is safe to ignore them.
if (!isI18nText(existingTNode)) {
if (isNewTNodeCreatedBefore(existingTNode, newTNode) &&
getInsertBeforeIndex(existingTNode) === null) {
// If it was created before us in time, (and it does not yet have `insertBeforeIndex`)
// then add the `insertBeforeIndex`.
setInsertBeforeIndex(existingTNode, newTNode.index);
}
}
}
}
}
function isI18nText(tNode: TNode): boolean {
return tNode.type !== TNodeType.Placeholder;
}
function isNewTNodeCreatedBefore(existingTNode: TNode, newTNode: TNode): boolean {
return isI18nText(newTNode) || existingTNode.index > newTNode.index;
}
function getInsertBeforeIndex(tNode: TNode): number|null {
const index = tNode.insertBeforeIndex;
return Array.isArray(index) ? index[0] : index;
}
function setInsertBeforeIndex(tNode: TNode, value: number): void {
const index = tNode.insertBeforeIndex;
if (Array.isArray(index)) {
// Array is stored if we have to insert child nodes. See `TNode.insertBeforeIndex`
index[0] = value;
} else {
tNode.insertBeforeIndex = value;
}
}

View File

@ -11,20 +11,23 @@ import '../../util/ng_i18n_closure_mode';
import {getTemplateContent, SRCSET_ATTRS, URI_ATTRS, VALID_ATTRS, VALID_ELEMENTS} from '../../sanitization/html_sanitizer';
import {getInertBodyHelper} from '../../sanitization/inert_body';
import {_sanitizeUrl, sanitizeSrcset} from '../../sanitization/url_sanitizer';
import {addAllToArray} from '../../util/array_utils';
import {assertEqual} from '../../util/assert';
import {allocExpando, elementAttributeInternal, setInputsForProperty, setNgReflectProperties} from '../instructions/shared';
import {assertDefined, assertEqual, assertGreaterThanOrEqual, assertOneOf, assertString} from '../../util/assert';
import {CharCode} from '../../util/char_code';
import {loadIcuContainerVisitor} from '../instructions/i18n_icu_container_visitor';
import {allocExpando, createTNodeAtIndex, elementAttributeInternal, setInputsForProperty, setNgReflectProperties} from '../instructions/shared';
import {getDocument} from '../interfaces/document';
import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nMutateOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes, IcuCase, IcuExpression, IcuType, TI18n, TIcu} from '../interfaces/i18n';
import {TNodeType} from '../interfaces/node';
import {COMMENT_MARKER, ELEMENT_MARKER, ensureIcuContainerVisitorLoaded, I18nCreateOpCode, I18nCreateOpCodes, I18nMutateOpCode, i18nMutateOpCode, I18nMutateOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes, IcuExpression, IcuType, TI18n, TIcu} from '../interfaces/i18n';
import {TNode, TNodeType} from '../interfaces/node';
import {RComment, RElement} from '../interfaces/renderer';
import {SanitizerFn} from '../interfaces/sanitization';
import {HEADER_OFFSET, LView, T_HOST, TView} from '../interfaces/view';
import {getCurrentTNode, isCurrentTNodeParent} from '../state';
import {HEADER_OFFSET, LView, TView} from '../interfaces/view';
import {getCurrentParentTNode, getCurrentTNode, setCurrentTNode} from '../state';
import {attachDebugGetter} from '../util/debug_utils';
import {getNativeByIndex, getTNode} from '../util/view_utils';
import {i18nMutateOpCodesToString, i18nUpdateOpCodesToString} from './i18n_debug';
import {i18nCreateOpCodesToString, i18nMutateOpCodesToString, i18nUpdateOpCodesToString} from './i18n_debug';
import {addTNodeAndUpdateInsertBeforeIndex} from './i18n_insert_before_index';
import {createTNodePlaceholder, setTIcu, setTNodeInsertBeforeIndex} from './i18n_util';
@ -33,22 +36,9 @@ const ICU_REGEXP = /({\s*<2A>\d+:?\d*<2A>\s*,\s*\S{6}\s*,[\s\S]*})/gi;
const NESTED_ICU = /<2F>(\d+)<29>/;
const ICU_BLOCK_REGEXP = /^\s*(<28>\d+:?\d*<2A>)\s*,\s*(select|plural)\s*,/;
// Count for the number of vars that will be allocated for each i18n block.
// It is global because this is used in multiple functions that include loops and recursive calls.
// This is reset to 0 when `i18nStartFirstPass` is called.
let i18nVarsCount: number;
const parentIndexStack: number[] = [];
const MARKER = `<EFBFBD>`;
const SUBTEMPLATE_REGEXP = /<2F>\/?\*(\d+:\d+)<29>/gi;
const PH_REGEXP = /<2F>(\/?[#*!]\d+):?\d*<2A>/gi;
const enum TagType {
ELEMENT = '#',
TEMPLATE = '*',
PROJECTION = '!',
}
/**
* Angular Dart introduced &ngsp; as a placeholder for non-removable space, see:
@ -62,148 +52,170 @@ function replaceNgsp(value: string): string {
return value.replace(NGSP_UNICODE_REGEXP, ' ');
}
/**
* See `i18nStart` above.
* Create dynamic nodes from i18n translation block.
*
* - Text nodes are created synchronously
* - TNodes are linked into tree lazily
*
* @param tView Current `TView`
* @parentTNodeIndex index to the parent TNode of this i18n block
* @param lView Current `LView`
* @param index Index of `ɵɵi18nStart` instruction.
* @param message Message to translate.
* @param subTemplateIndex Index into the sub template of message translation. (ie in case of
* `ngIf`) (-1 otherwise)
*/
export function i18nStartFirstPass(
lView: LView, tView: TView, index: number, message: string, subTemplateIndex?: number) {
const startIndex = tView.blueprint.length - HEADER_OFFSET;
i18nVarsCount = 0;
const currentTNode = getCurrentTNode()!;
const parentTNode = isCurrentTNodeParent() ? currentTNode : currentTNode && currentTNode.parent;
let parentIndex =
parentTNode && parentTNode !== lView[T_HOST] ? parentTNode.index - HEADER_OFFSET : index;
let parentIndexPointer = 0;
parentIndexStack[parentIndexPointer] = parentIndex;
const createOpCodes: I18nMutateOpCodes = [];
if (ngDevMode) {
attachDebugGetter(createOpCodes, i18nMutateOpCodesToString);
}
// If the previous node wasn't the direct parent then we have a translation without top level
// element and we need to keep a reference of the previous element if there is one. We should also
// keep track whether an element was a parent node or not, so that the logic that consumes
// the generated `I18nMutateOpCode`s can leverage this information to properly set TNode state
// (whether it's a parent or sibling).
if (index > 0 && currentTNode !== parentTNode) {
let previousTNodeIndex = currentTNode.index - HEADER_OFFSET;
// If current TNode is a sibling node, encode it using a negative index. This information is
// required when the `Select` action is processed (see the `readCreateOpCodes` function).
if (!isCurrentTNodeParent()) {
previousTNodeIndex = ~previousTNodeIndex;
}
// Create an OpCode to select the previous TNode
createOpCodes.push(previousTNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select);
}
export function i18nStartFirstCreatePass(
tView: TView, parentTNodeIndex: number, lView: LView, index: number, message: string,
subTemplateIndex: number) {
const rootTNode = getCurrentParentTNode();
const createOpCodes: I18nCreateOpCodes = [];
const updateOpCodes: I18nUpdateOpCodes = [];
const existingTNodeStack: TNode[][] = [[]];
if (ngDevMode) {
attachDebugGetter(createOpCodes, i18nCreateOpCodesToString);
attachDebugGetter(updateOpCodes, i18nUpdateOpCodesToString);
}
const icuExpressions: TIcu[] = [];
if (message === '' && isRootTemplateMessage(subTemplateIndex)) {
// If top level translation is an empty string, do not invoke additional processing
// and just create op codes for empty text node instead.
createOpCodes.push(
message, allocNodeIndex(startIndex),
parentIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild);
} else {
const templateTranslation = getTranslationForTemplate(message, subTemplateIndex);
const msgParts = replaceNgsp(templateTranslation).split(PH_REGEXP);
for (let i = 0; i < msgParts.length; i++) {
let value = msgParts[i];
if (i & 1) {
// Odd indexes are placeholders (elements and sub-templates)
if (value.charAt(0) === '/') {
// It is a closing tag
if (value.charAt(1) === TagType.ELEMENT) {
const phIndex = parseInt(value.substr(2), 10);
parentIndex = parentIndexStack[--parentIndexPointer];
createOpCodes.push(phIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.ElementEnd);
message = getTranslationForTemplate(message, subTemplateIndex);
const msgParts = replaceNgsp(message).split(PH_REGEXP);
for (let i = 0; i < msgParts.length; i++) {
let value = msgParts[i];
if ((i & 1) === 0) {
// Even indexes are text (including bindings & ICU expressions)
const parts = i18nParseTextIntoPartsAndICU(value);
for (let j = 0; j < parts.length; j++) {
let part = parts[j];
if ((j & 1) === 0) {
// `j` is odd therefore `part` is string
const text = part as string;
ngDevMode && assertString(text, 'Parsed ICU part should be string');
if (text !== '') {
i18nStartFirstCreatePassProcessTextNode(
tView, rootTNode, existingTNodeStack[0], createOpCodes, updateOpCodes, lView, text);
}
} else {
const phIndex = parseInt(value.substr(1), 10);
const isElement = value.charAt(0) === TagType.ELEMENT;
// The value represents a placeholder that we move to the designated index.
// Note: positive indicies indicate that a TNode with a given index should also be marked
// as parent while executing `Select` instruction.
createOpCodes.push(
(isElement ? phIndex : ~phIndex) << I18nMutateOpCode.SHIFT_REF |
I18nMutateOpCode.Select,
parentIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild);
if (isElement) {
parentIndexStack[++parentIndexPointer] = parentIndex = phIndex;
// `j` is Even therefor `part` is an `ICUExpression`
const icuExpression: IcuExpression = part as IcuExpression;
// Verify that ICU expression has the right shape. Translations might contain invalid
// constructions (while original messages were correct), so ICU parsing at runtime may
// not succeed (thus `icuExpression` remains a string).
if (ngDevMode && typeof icuExpression !== 'object') {
throw new Error(`Unable to parse ICU expression in "${message}" message.`);
}
const icuContainerTNode = createTNodeAndAddOpCode(
tView, rootTNode, existingTNodeStack[0], lView, createOpCodes,
ngDevMode ? `ICU ${index}:${icuExpression.mainBinding}` : '', true);
const icuNodeIndex = icuContainerTNode.index;
ngDevMode &&
assertGreaterThanOrEqual(
icuNodeIndex, HEADER_OFFSET, 'Index must be in absolute LView offset');
icuStart(tView, lView, updateOpCodes, parentTNodeIndex, icuExpression, icuNodeIndex);
}
}
} else {
// Odd indexes are placeholders (elements and sub-templates)
// At this point value is something like: '/#1:2' (orginally coming from '<27>/#1:2<>')
const isClosing = value.charCodeAt(0) === CharCode.SLASH;
const type = value.charCodeAt(isClosing ? 1 : 0);
ngDevMode && assertOneOf(type, CharCode.STAR, CharCode.HASH, CharCode.EXCLAMATION);
const index = HEADER_OFFSET + Number.parseInt(value.substring((isClosing ? 2 : 1)));
if (isClosing) {
existingTNodeStack.shift();
setCurrentTNode(getCurrentParentTNode()!, false);
} else {
// Even indexes are text (including bindings & ICU expressions)
const parts = extractParts(value);
for (let j = 0; j < parts.length; j++) {
if (j & 1) {
// Odd indexes are ICU expressions
const icuExpression = parts[j] as IcuExpression;
// Verify that ICU expression has the right shape. Translations might contain invalid
// constructions (while original messages were correct), so ICU parsing at runtime may
// not succeed (thus `icuExpression` remains a string).
if (typeof icuExpression !== 'object') {
throw new Error(
`Unable to parse ICU expression in "${templateTranslation}" message.`);
}
// Create the comment node that will anchor the ICU expression
const icuNodeIndex = allocNodeIndex(startIndex);
createOpCodes.push(
COMMENT_MARKER, ngDevMode ? `ICU ${icuNodeIndex}` : '', icuNodeIndex,
parentIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild);
// Update codes for the ICU expression
const mask = getBindingMask(icuExpression);
icuStart(icuExpressions, icuExpression, icuNodeIndex, icuNodeIndex);
// Since this is recursive, the last TIcu that was pushed is the one we want
const tIcuIndex = icuExpressions.length - 1;
updateOpCodes.push(
toMaskBit(icuExpression.mainBinding), // mask of the main binding
3, // skip 3 opCodes if not changed
-1 - icuExpression.mainBinding,
icuNodeIndex << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.IcuSwitch, tIcuIndex,
mask, // mask of all the bindings of this ICU expression
2, // skip 2 opCodes if not changed
icuNodeIndex << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.IcuUpdate, tIcuIndex);
} else if (parts[j] !== '') {
const text = parts[j] as string;
// Even indexes are text (including bindings)
const hasBinding = text.match(BINDING_REGEXP);
// Create text nodes
const textNodeIndex = allocNodeIndex(startIndex);
createOpCodes.push(
// If there is a binding, the value will be set during update
hasBinding ? '' : text, textNodeIndex,
parentIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild);
if (hasBinding) {
addAllToArray(generateBindingUpdateOpCodes(text, textNodeIndex), updateOpCodes);
}
}
}
const tNode = createTNodePlaceholder(tView, existingTNodeStack[0], index);
existingTNodeStack.unshift([]);
setCurrentTNode(tNode, true);
}
}
}
if (i18nVarsCount > 0) {
allocExpando(tView, lView, i18nVarsCount);
}
// NOTE: local var needed to properly assert the type of `TI18n`.
const tI18n: TI18n = {
vars: i18nVarsCount,
tView.data[index + HEADER_OFFSET] = <TI18n>{
create: createOpCodes,
update: updateOpCodes,
icus: icuExpressions.length ? icuExpressions : null,
};
}
tView.data[index + HEADER_OFFSET] = tI18n;
/**
* Allocate space in i18n Range add create OpCode instruction to crete a text or comment node.
*
* @param tView Current `TView` needed to allocate space in i18n range.
* @param rootTNode Root `TNode` of the i18n block. This node determines if the new TNode will be
* added as part of the `i18nStart` instruction or as part of the `TNode.insertBeforeIndex`.
* @param existingTNodes internal state for `addTNodeAndUpdateInsertBeforeIndex`.
* @param lView Current `LView` needed to allocate space in i18n range.
* @param createOpCodes Array storing `I18nCreateOpCodes` where new opCodes will be added.
* @param text Text to be added when the `Text` or `Comment` node will be created.
* @param isICU true if a `Comment` node for ICU (instead of `Text`) node should be created.
*/
function createTNodeAndAddOpCode(
tView: TView, rootTNode: TNode|null, existingTNodes: TNode[], lView: LView,
createOpCodes: I18nCreateOpCodes, text: string, isICU: boolean): TNode {
const i18nNodeIdx = allocExpando(tView, lView, 1);
let opCode = i18nNodeIdx << I18nCreateOpCode.SHIFT;
let parentTNode = getCurrentParentTNode();
if (rootTNode === parentTNode) {
// FIXME(misko): A null `parentTNode` should represent when we fall of the `LView` boundry.
// (there is no parent), but in some circumstances (because we are inconsistent about how we set
// `previousOrParentTNode`) it could point to `rootTNode` So this is a work around.
parentTNode = null;
}
if (parentTNode === null) {
// If we don't have a parent that means that we can eagerly add nodes.
// If we have a parent than these nodes can't be added now (as the parent has not been created
// yet) and instead the `parentTNode` is responsible for adding it. See
// `TNode.insertBeforeIndex`
opCode |= I18nCreateOpCode.APPEND_EAGERLY;
}
if (isICU) {
opCode |= I18nCreateOpCode.COMMENT;
ensureIcuContainerVisitorLoaded(loadIcuContainerVisitor);
}
createOpCodes.push(opCode, text);
const tNode = createTNodeAtIndex(
tView, i18nNodeIdx, isICU ? TNodeType.IcuContainer : TNodeType.Element, null, null);
addTNodeAndUpdateInsertBeforeIndex(existingTNodes, tNode);
const tNodeIdx = tNode.index;
setCurrentTNode(tNode, false /* Text nodes are self closing */);
if (parentTNode !== null && rootTNode !== parentTNode) {
// We are a child of deeper node (rather than a direct child of `i18nStart` instruction.)
// We have to make sure to add ourselves to the parent.
setTNodeInsertBeforeIndex(parentTNode, tNodeIdx);
}
return tNode;
}
/**
* Processes text node in i18n block.
*
* Text nodes can have:
* - Create instruction in `createOpCodes` for creating the text node.
* - Allocate spec for text node in i18n range of `LView`
* - If contains binding:
* - bindings => allocate space in i18n range of `LView` to store the binding value.
* - populate `updateOpCodes` with update instructions.
*
* @param tView Current `TView`
* @param rootTNode Root `TNode` of the i18n block. This node determines if the new TNode will
* be added as part of the `i18nStart` instruction or as part of the
* `TNode.insertBeforeIndex`.
* @param existingTNodes internal state for `addTNodeAndUpdateInsertBeforeIndex`.
* @param createOpCodes Location where the creation OpCodes will be stored.
* @param lView Current `LView`
* @param text The translated text (which may contain binding)
*/
function i18nStartFirstCreatePassProcessTextNode(
tView: TView, rootTNode: TNode|null, existingTNodes: TNode[], createOpCodes: I18nCreateOpCodes,
updateOpCodes: I18nUpdateOpCodes, lView: LView, text: string): void {
const hasBinding = text.match(BINDING_REGEXP);
const tNode = createTNodeAndAddOpCode(
tView, rootTNode, existingTNodes, lView, createOpCodes, hasBinding ? '' : text, false);
if (hasBinding) {
generateBindingUpdateOpCodes(updateOpCodes, text, tNode.index);
}
}
/**
@ -212,7 +224,7 @@ export function i18nStartFirstPass(
export function i18nAttributesFirstPass(
lView: LView, tView: TView, index: number, values: string[]) {
const previousElement = getCurrentTNode()!;
const previousElementIndex = previousElement.index - HEADER_OFFSET;
const previousElementIndex = previousElement.index;
const updateOpCodes: I18nUpdateOpCodes = [];
if (ngDevMode) {
attachDebugGetter(updateOpCodes, i18nUpdateOpCodesToString);
@ -233,11 +245,10 @@ export function i18nAttributesFirstPass(
const hasBinding = !!value.match(BINDING_REGEXP);
if (hasBinding) {
if (tView.firstCreatePass && tView.data[index + HEADER_OFFSET] === null) {
addAllToArray(
generateBindingUpdateOpCodes(value, previousElementIndex, attrName), updateOpCodes);
generateBindingUpdateOpCodes(updateOpCodes, value, previousElementIndex, attrName);
}
} else {
const tNode = getTNode(tView, previousElementIndex);
const tNode = getTNode(tView, previousElementIndex - HEADER_OFFSET);
// Set attributes for Elements only, for other types (like ElementContainer),
// only set inputs below
if (tNode.type === TNodeType.Element) {
@ -248,7 +259,9 @@ export function i18nAttributesFirstPass(
if (dataValue) {
setInputsForProperty(tView, lView, dataValue, attrName, value);
if (ngDevMode) {
const element = getNativeByIndex(previousElementIndex, lView) as RElement | RComment;
const element =
getNativeByIndex(previousElementIndex - HEADER_OFFSET, lView) as RElement |
RComment;
setNgReflectProperties(lView, element, tNode.type, dataValue, value);
}
}
@ -266,15 +279,22 @@ export function i18nAttributesFirstPass(
/**
* Generate the OpCodes to update the bindings of a string.
*
* @param updateOpCodes Place where the update opcodes will be stored.
* @param str The string containing the bindings.
* @param destinationNode Index of the destination node which will receive the binding.
* @param attrName Name of the attribute, if the string belongs to an attribute.
* @param sanitizeFn Sanitization function used to sanitize the string after update, if necessary.
*/
export function generateBindingUpdateOpCodes(
str: string, destinationNode: number, attrName?: string,
sanitizeFn: SanitizerFn|null = null): I18nUpdateOpCodes {
const updateOpCodes: I18nUpdateOpCodes = [null, null]; // Alloc space for mask and size
updateOpCodes: I18nUpdateOpCodes, str: string, destinationNode: number, attrName?: string,
sanitizeFn: SanitizerFn|null = null): number {
ngDevMode &&
assertGreaterThanOrEqual(
destinationNode, HEADER_OFFSET, 'Index must be in absolute LView offset');
const maskIndex = updateOpCodes.length; // Location of mask
const sizeIndex = maskIndex + 1; // location of size for skipping
updateOpCodes.push(null, null); // Alloc space for mask and size
const startIndex = maskIndex + 2; // location of first allocation.
if (ngDevMode) {
attachDebugGetter(updateOpCodes, i18nUpdateOpCodesToString);
}
@ -301,9 +321,9 @@ export function generateBindingUpdateOpCodes(
if (attrName) {
updateOpCodes.push(attrName, sanitizeFn);
}
updateOpCodes[0] = mask;
updateOpCodes[1] = updateOpCodes.length - 2;
return updateOpCodes;
updateOpCodes[maskIndex] = mask;
updateOpCodes[sizeIndex] = updateOpCodes.length - startIndex;
return mask;
}
function getBindingMask(icuExpression: IcuExpression, mask = 0): number {
@ -325,26 +345,21 @@ function getBindingMask(icuExpression: IcuExpression, mask = 0): number {
return mask;
}
function allocNodeIndex(startIndex: number): number {
return startIndex + i18nVarsCount++;
}
/**
* Convert binding index to mask bit.
*
* Each index represents a single bit on the bit-mask. Because bit-mask only has 32 bits, we make
* the 32nd bit share all masks for all bindings higher than 32. Since it is extremely rare to have
* more than 32 bindings this will be hit very rarely. The downside of hitting this corner case is
* that we will execute binding code more often than necessary. (penalty of performance)
* the 32nd bit share all masks for all bindings higher than 32. Since it is extremely rare to
* have more than 32 bindings this will be hit very rarely. The downside of hitting this corner
* case is that we will execute binding code more often than necessary. (penalty of performance)
*/
function toMaskBit(bindingIndex: number): number {
return 1 << Math.min(bindingIndex, 31);
}
export function isRootTemplateMessage(subTemplateIndex: number|
undefined): subTemplateIndex is undefined {
return subTemplateIndex === undefined;
export function isRootTemplateMessage(subTemplateIndex: number): subTemplateIndex is - 1 {
return subTemplateIndex === -1;
}
@ -385,8 +400,8 @@ function removeInnerTemplateTranslation(message: string): string {
/**
* Extracts a part of a message and removes the rest.
*
* This method is used for extracting a part of the message associated with a template. A translated
* message can span multiple templates.
* This method is used for extracting a part of the message associated with a template. A
* translated message can span multiple templates.
*
* Example:
* ```
@ -397,7 +412,7 @@ function removeInnerTemplateTranslation(message: string): string {
* @param subTemplateIndex Index of the sub-template to extract. If undefined it returns the
* external template and removes all sub-templates.
*/
export function getTranslationForTemplate(message: string, subTemplateIndex?: number) {
export function getTranslationForTemplate(message: string, subTemplateIndex: number) {
if (isRootTemplateMessage(subTemplateIndex)) {
// We want the root template message, ignore all sub-templates
return removeInnerTemplateTranslation(message);
@ -413,19 +428,27 @@ export function getTranslationForTemplate(message: string, subTemplateIndex?: nu
/**
* Generate the OpCodes for ICU expressions.
*
* @param tIcus
* @param icuExpression
* @param startIndex
* @param expandoStartIndex
* @param index Index where the anchor is stored and an optional `TIcuContainerNode`
* - `lView[anchorIdx]` points to a `Comment` node representing the anchor for the ICU.
* - `tView.data[anchorIdx]` points to the `TIcuContainerNode` if ICU is root (`null` otherwise)
*/
export function icuStart(
tIcus: TIcu[], icuExpression: IcuExpression, startIndex: number,
expandoStartIndex: number): void {
const createCodes: I18nMutateOpCodes[] = [];
const removeCodes: I18nMutateOpCodes[] = [];
const updateCodes: I18nUpdateOpCodes[] = [];
const vars = [];
const childIcus: number[][] = [];
tView: TView, lView: LView, updateOpCodes: I18nUpdateOpCodes, parentIdx: number,
icuExpression: IcuExpression, anchorIdx: number) {
ngDevMode && assertDefined(icuExpression, 'ICU expression must be defined');
let bindingMask = 0;
const tIcu: TIcu = {
type: icuExpression.type,
currentCaseLViewIndex: allocExpando(tView, lView, 1),
anchorIdx,
cases: [],
create: [],
remove: [],
update: []
};
addUpdateIcuSwitch(updateOpCodes, icuExpression, anchorIdx);
setTIcu(tView, anchorIdx, tIcu);
const values = icuExpression.values;
for (let i = 0; i < values.length; i++) {
// Each value is an array of strings & other ICU expressions
@ -440,29 +463,14 @@ export function icuStart(
valueArr[j] = `<!--<2D>${icuIndex}<EFBFBD>-->`;
}
}
const icuCase: IcuCase =
parseIcuCase(valueArr.join(''), startIndex, nestedIcus, tIcus, expandoStartIndex);
createCodes.push(icuCase.create);
removeCodes.push(icuCase.remove);
updateCodes.push(icuCase.update);
vars.push(icuCase.vars);
childIcus.push(icuCase.childIcus);
bindingMask = parseIcuCase(
tView, tIcu, lView, updateOpCodes, parentIdx, icuExpression.cases[i],
valueArr.join(''), nestedIcus) |
bindingMask;
}
if (bindingMask) {
addUpdateIcuUpdate(updateOpCodes, bindingMask, anchorIdx);
}
const tIcu: TIcu = {
type: icuExpression.type,
vars,
currentCaseLViewIndex: HEADER_OFFSET +
expandoStartIndex // expandoStartIndex does not include the header so add it.
+ 1, // The first item stored is the `<!--ICU #-->` anchor so skip it.
childIcus,
cases: icuExpression.cases,
create: createCodes,
remove: removeCodes,
update: updateCodes
};
tIcus.push(tIcu);
// Adding the maximum possible of vars needed (based on the cases with the most vars)
i18nVarsCount += Math.max(...vars);
}
/**
@ -487,7 +495,7 @@ export function parseICUBlock(pattern: string): IcuExpression {
return '';
});
const parts = extractParts(pattern) as string[];
const parts = i18nParseTextIntoPartsAndICU(pattern) as string[];
// Looking for (key block)+ sequence. One of the keys has to be "other".
for (let pos = 0; pos < parts.length;) {
let key = parts[pos++].trim();
@ -499,7 +507,7 @@ export function parseICUBlock(pattern: string): IcuExpression {
cases.push(key);
}
const blocks = extractParts(parts[pos++]) as string[];
const blocks = i18nParseTextIntoPartsAndICU(parts[pos++]) as string[];
if (cases.length > values.length) {
values.push(blocks);
}
@ -510,51 +518,17 @@ export function parseICUBlock(pattern: string): IcuExpression {
}
/**
* Transforms a string template into an HTML template and a list of instructions used to update
* attributes or nodes that contain bindings.
*
* @param unsafeHtml The string to parse
* @param parentIndex
* @param nestedIcus
* @param tIcus
* @param expandoStartIndex
*/
function parseIcuCase(
unsafeHtml: string, parentIndex: number, nestedIcus: IcuExpression[], tIcus: TIcu[],
expandoStartIndex: number): IcuCase {
const inertBodyHelper = getInertBodyHelper(getDocument());
const inertBodyElement = inertBodyHelper.getInertBodyElement(unsafeHtml);
if (!inertBodyElement) {
throw new Error('Unable to generate inert body element');
}
const wrapper = getTemplateContent(inertBodyElement!) as Element || inertBodyElement;
const opCodes: IcuCase = {
vars: 1, // allocate space for `TIcu.currentCaseLViewIndex`
childIcus: [],
create: [],
remove: [],
update: []
};
if (ngDevMode) {
attachDebugGetter(opCodes.create, i18nMutateOpCodesToString);
attachDebugGetter(opCodes.remove, i18nMutateOpCodesToString);
attachDebugGetter(opCodes.update, i18nUpdateOpCodesToString);
}
parseNodes(wrapper.firstChild, opCodes, parentIndex, nestedIcus, tIcus, expandoStartIndex);
return opCodes;
}
/**
* Breaks pattern into strings and top level {...} blocks.
* Can be used to break a message into text and ICU expressions, or to break an ICU expression into
* keys and cases.
* Original code from closure library, modified for Angular.
* Can be used to break a message into text and ICU expressions, or to break an ICU expression
* into keys and cases. Original code from closure library, modified for Angular.
*
* @param pattern (sub)Pattern to be broken.
*
* @returns An `Array<string|IcuExpression>` where:
* - odd positions: `string` => text between ICU expressions
* - even positions: `ICUExpression` => ICU expression parsed into `ICUExpression` record.
*/
function extractParts(pattern: string): (string|IcuExpression)[] {
export function i18nParseTextIntoPartsAndICU(pattern: string): (string|IcuExpression)[] {
if (!pattern) {
return [];
}
@ -602,131 +576,147 @@ function extractParts(pattern: string): (string|IcuExpression)[] {
/**
* Parses a node, its children and its siblings, and generates the mutate & update OpCodes.
*
* @param currentNode The first node to parse
* @param icuCase The data for the ICU expression case that contains those nodes
* @param parentIndex Index of the current node's parent
* @param nestedIcus Data for the nested ICU expressions that this case contains
* @param tIcus Data for all ICU expressions of the current message
* @param expandoStartIndex Expando start index for the current ICU expression
*/
export function parseNodes(
currentNode: Node|null, icuCase: IcuCase, parentIndex: number, nestedIcus: IcuExpression[],
tIcus: TIcu[], expandoStartIndex: number) {
if (currentNode) {
const nestedIcusToCreate: [IcuExpression, number][] = [];
while (currentNode) {
const nextNode: Node|null = currentNode.nextSibling;
const newIndex = expandoStartIndex + ++icuCase.vars;
switch (currentNode.nodeType) {
case Node.ELEMENT_NODE:
const element = currentNode as Element;
const tagName = element.tagName.toLowerCase();
if (!VALID_ELEMENTS.hasOwnProperty(tagName)) {
// This isn't a valid element, we won't create an element for it
icuCase.vars--;
} else {
icuCase.create.push(
ELEMENT_MARKER, tagName, newIndex,
parentIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild);
const elAttrs = element.attributes;
for (let i = 0; i < elAttrs.length; i++) {
const attr = elAttrs.item(i)!;
const lowerAttrName = attr.name.toLowerCase();
const hasBinding = !!attr.value.match(BINDING_REGEXP);
// we assume the input string is safe, unless it's using a binding
if (hasBinding) {
if (VALID_ATTRS.hasOwnProperty(lowerAttrName)) {
if (URI_ATTRS[lowerAttrName]) {
addAllToArray(
generateBindingUpdateOpCodes(attr.value, newIndex, attr.name, _sanitizeUrl),
icuCase.update);
} else if (SRCSET_ATTRS[lowerAttrName]) {
addAllToArray(
generateBindingUpdateOpCodes(
attr.value, newIndex, attr.name, sanitizeSrcset),
icuCase.update);
} else {
addAllToArray(
generateBindingUpdateOpCodes(attr.value, newIndex, attr.name),
icuCase.update);
}
} else {
ngDevMode &&
console.warn(`WARNING: ignoring unsafe attribute value ${
lowerAttrName} on element ${tagName} (see http://g.co/ng/security#xss)`);
}
} else {
icuCase.create.push(
newIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Attr, attr.name,
attr.value);
}
}
// Parse the children of this node (if any)
parseNodes(
currentNode.firstChild, icuCase, newIndex, nestedIcus, tIcus, expandoStartIndex);
// Remove the parent node after the children
icuCase.remove.push(newIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove);
}
break;
case Node.TEXT_NODE:
const value = currentNode.textContent || '';
const hasBinding = value.match(BINDING_REGEXP);
icuCase.create.push(
hasBinding ? '' : value, newIndex,
parentIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild);
icuCase.remove.push(newIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove);
if (hasBinding) {
addAllToArray(generateBindingUpdateOpCodes(value, newIndex), icuCase.update);
}
break;
case Node.COMMENT_NODE:
// Check if the comment node is a placeholder for a nested ICU
const match = NESTED_ICU.exec(currentNode.textContent || '');
if (match) {
const nestedIcuIndex = parseInt(match[1], 10);
const newLocal = ngDevMode ? `nested ICU ${nestedIcuIndex}` : '';
// Create the comment node that will anchor the ICU expression
icuCase.create.push(
COMMENT_MARKER, newLocal, newIndex,
parentIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild);
const nestedIcu = nestedIcus[nestedIcuIndex];
nestedIcusToCreate.push([nestedIcu, newIndex]);
} else {
// We do not handle any other type of comment
icuCase.vars--;
}
break;
default:
// We do not handle any other type of element
icuCase.vars--;
}
currentNode = nextNode!;
}
export function parseIcuCase(
tView: TView, tIcu: TIcu, lView: LView, updateOpCodes: I18nUpdateOpCodes, parentIdx: number,
caseName: string, unsafeCaseHtml: string, nestedIcus: IcuExpression[]): number {
const create: I18nMutateOpCodes = [];
const remove: I18nMutateOpCodes = [];
const update: I18nUpdateOpCodes = [];
if (ngDevMode) {
attachDebugGetter(create, i18nMutateOpCodesToString);
attachDebugGetter(remove, i18nMutateOpCodesToString);
attachDebugGetter(update, i18nUpdateOpCodesToString);
}
tIcu.cases.push(caseName);
tIcu.create.push(create);
tIcu.remove.push(remove);
tIcu.update.push(update);
for (let i = 0; i < nestedIcusToCreate.length; i++) {
const nestedIcu = nestedIcusToCreate[i][0];
const nestedIcuNodeIndex = nestedIcusToCreate[i][1];
icuStart(tIcus, nestedIcu, nestedIcuNodeIndex, expandoStartIndex + icuCase.vars);
// Since this is recursive, the last TIcu that was pushed is the one we want
const nestTIcuIndex = tIcus.length - 1;
icuCase.vars += Math.max(...tIcus[nestTIcuIndex].vars);
icuCase.childIcus.push(nestTIcuIndex);
const mask = getBindingMask(nestedIcu);
icuCase.update.push(
toMaskBit(nestedIcu.mainBinding), // mask of the main binding
3, // skip 3 opCodes if not changed
-1 - nestedIcu.mainBinding,
nestedIcuNodeIndex << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.IcuSwitch,
// FIXME(misko): Index should be part of the opcode
nestTIcuIndex,
mask, // mask of all the bindings of this ICU expression
2, // skip 2 opCodes if not changed
nestedIcuNodeIndex << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.IcuUpdate,
nestTIcuIndex);
icuCase.remove.push(
nestTIcuIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.RemoveNestedIcu,
// FIXME(misko): Index should be part of the opcode
nestedIcuNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove);
}
const inertBodyHelper = getInertBodyHelper(getDocument());
const inertBodyElement = inertBodyHelper.getInertBodyElement(unsafeCaseHtml);
ngDevMode && assertDefined(inertBodyElement, 'Unable to generate inert body element');
const inertRootNode = getTemplateContent(inertBodyElement!) as Element || inertBodyElement;
if (inertRootNode) {
return walkIcuTree(
tView, tIcu, lView, updateOpCodes, create, remove, update, inertRootNode, parentIdx,
nestedIcus, 0);
} else {
return 0;
}
}
function walkIcuTree(
tView: TView, tIcu: TIcu, lView: LView, sharedUpdateOpCodes: I18nUpdateOpCodes,
create: I18nMutateOpCodes, remove: I18nMutateOpCodes, update: I18nUpdateOpCodes,
parentNode: Element, parentIdx: number, nestedIcus: IcuExpression[], depth: number): number {
let bindingMask = 0;
let currentNode = parentNode.firstChild;
while (currentNode) {
const newIndex = allocExpando(tView, lView, 1);
switch (currentNode.nodeType) {
case Node.ELEMENT_NODE:
const element = currentNode as Element;
const tagName = element.tagName.toLowerCase();
if (VALID_ELEMENTS.hasOwnProperty(tagName)) {
addCreateNodeAndAppend(create, ELEMENT_MARKER, tagName, parentIdx, newIndex);
tView.data[newIndex] = tagName;
const elAttrs = element.attributes;
for (let i = 0; i < elAttrs.length; i++) {
const attr = elAttrs.item(i)!;
const lowerAttrName = attr.name.toLowerCase();
const hasBinding = !!attr.value.match(BINDING_REGEXP);
// we assume the input string is safe, unless it's using a binding
if (hasBinding) {
if (VALID_ATTRS.hasOwnProperty(lowerAttrName)) {
if (URI_ATTRS[lowerAttrName]) {
generateBindingUpdateOpCodes(
update, attr.value, newIndex, attr.name, _sanitizeUrl);
} else if (SRCSET_ATTRS[lowerAttrName]) {
generateBindingUpdateOpCodes(
update, attr.value, newIndex, attr.name, sanitizeSrcset);
} else {
generateBindingUpdateOpCodes(update, attr.value, newIndex, attr.name);
}
} else {
ngDevMode && console.warn(` WARNING:
ignoring unsafe attribute value ${lowerAttrName} on element $ {
tagName
} (see http://g.co/ng/security#xss)`);
}
} else {
create.push(
newIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Attr, attr.name,
attr.value);
}
}
// Parse the children of this node (if any)
bindingMask = walkIcuTree(
tView, tIcu, lView, sharedUpdateOpCodes, create, remove, update,
currentNode as Element, newIndex, nestedIcus, depth + 1) |
bindingMask;
addRemoveNode(remove, newIndex, depth);
}
break;
case Node.TEXT_NODE:
const value = currentNode.textContent || '';
const hasBinding = value.match(BINDING_REGEXP);
addCreateNodeAndAppend(create, null, hasBinding ? '' : value, parentIdx, newIndex);
addRemoveNode(remove, newIndex, depth);
if (hasBinding) {
bindingMask = generateBindingUpdateOpCodes(update, value, newIndex) | bindingMask;
}
break;
case Node.COMMENT_NODE:
// Check if the comment node is a placeholder for a nested ICU
const isNestedIcu = NESTED_ICU.exec(currentNode.textContent || '');
if (isNestedIcu) {
const nestedIcuIndex = parseInt(isNestedIcu[1], 10);
const icuExpression: IcuExpression = nestedIcus[nestedIcuIndex];
// Create the comment node that will anchor the ICU expression
addCreateNodeAndAppend(
create, COMMENT_MARKER, ngDevMode ? `nested ICU ${nestedIcuIndex}` : '', parentIdx,
newIndex);
icuStart(tView, lView, sharedUpdateOpCodes, parentIdx, icuExpression, newIndex);
addRemoveNestedIcu(remove, newIndex, depth);
}
break;
}
currentNode = currentNode.nextSibling;
}
return bindingMask;
}
function addRemoveNode(remove: I18nMutateOpCodes, index: number, depth: number) {
if (depth === 0) {
remove.push(index << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove);
}
}
function addRemoveNestedIcu(remove: I18nMutateOpCodes, index: number, depth: number) {
if (depth === 0) {
remove.push(index << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.RemoveNestedIcu);
remove.push(index << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove);
}
}
function addUpdateIcuSwitch(
update: I18nUpdateOpCodes, icuExpression: IcuExpression, index: number) {
update.push(
toMaskBit(icuExpression.mainBinding), 2, -1 - icuExpression.mainBinding,
index << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.IcuSwitch);
}
function addUpdateIcuUpdate(update: I18nUpdateOpCodes, bindingMask: number, index: number) {
update.push(bindingMask, 1, index << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.IcuUpdate);
}
function addCreateNodeAndAppend(
create: I18nMutateOpCodes, marker: null|COMMENT_MARKER|ELEMENT_MARKER, text: string,
appendToParentIdx: number, createAtIdx: number) {
if (marker !== null) {
create.push(marker);
}
create.push(
text, createAtIdx,
i18nMutateOpCode(I18nMutateOpCode.AppendChild, appendToParentIdx, createAtIdx));
}

View File

@ -0,0 +1,107 @@
/**
* @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 {assertEqual, throwError} from '../../util/assert';
import {assertTIcu, assertTNode} from '../assert';
import {createTNodeAtIndex} from '../instructions/shared';
import {TIcu} from '../interfaces/i18n';
import {TIcuContainerNode, TNode, TNodeType} from '../interfaces/node';
import {TView} from '../interfaces/view';
import {assertNodeType} from '../node_assert';
import {addTNodeAndUpdateInsertBeforeIndex} from './i18n_insert_before_index';
/**
* Retrieve `TIcu` at a given `index`.
*
* The `TIcu` can be stored either directly (if it is nested ICU) OR
* it is stored inside tho `TIcuContainer` if it is top level ICU.
*
* The reason for this is that the top level ICU need a `TNode` so that they are part of the render
* tree, but nested ICU's have no TNode, because we don't know ahead of time if the nested ICU is
* expressed (parent ICU may have selected a case which does not contain it.)
*
* @param tView Current `TView`.
* @param index Index where the value should be read from.
*/
export function getTIcu(tView: TView, index: number): TIcu|null {
const value = tView.data[index] as null | TIcu | TIcuContainerNode | string;
if (value === null || typeof value === 'string') return null;
if (ngDevMode &&
!(value.hasOwnProperty('tViews') || value.hasOwnProperty('currentCaseLViewIndex'))) {
throwError('We expect to get \'null\'|\'TIcu\'|\'TIcuContainer\', but got: ' + value);
}
// Here the `value.hasOwnProperty('currentCaseLViewIndex')` is a polymorphic read as it can be
// either TIcu or TIcuContainerNode. This is not ideal, but we still think it is OK because it
// will be just two cases which fits into the browser inline cache (inline cache can take up to
// 4)
const tIcu = value.hasOwnProperty('currentCaseLViewIndex') ?
value :
(value as TIcuContainerNode).tagName as any;
ngDevMode && assertTIcu(tIcu);
return tIcu;
}
/**
* Store `TIcu` at a give `index`.
*
* The `TIcu` can be stored either directly (if it is nested ICU) OR
* it is stored inside tho `TIcuContainer` if it is top level ICU.
*
* The reason for this is that the top level ICU need a `TNode` so that they are part of the render
* tree, but nested ICU's have no TNode, because we don't know ahead of time if the nested ICU is
* expressed (parent ICU may have selected a case which does not contain it.)
*
* @param tView Current `TView`.
* @param index Index where the value should be stored at in `Tview.data`
* @param tIcu The TIcu to store.
*/
export function setTIcu(tView: TView, index: number, tIcu: TIcu): void {
const tNode = tView.data[index] as null | TIcuContainerNode;
ngDevMode &&
assertEqual(
tNode === null || tNode.hasOwnProperty('tViews'), true,
'We expect to get \'null\'|\'TIcuContainer\'');
if (tNode === null) {
tView.data[index] = tIcu;
} else {
ngDevMode && assertNodeType(tNode, TNodeType.IcuContainer);
// FIXME(misko): This is a hack which allows us to associate `TI18n` with `TNode`.
// This should be refactored so that one can attach arbitrary data with `TNode`
tNode.tagName = tIcu as any;
}
}
/**
* Set `TNode.insertBeforeIndex` taking the `Array` into account.
*
* See `TNode.insertBeforeIndex`
*/
export function setTNodeInsertBeforeIndex(tNode: TNode, index: number) {
ngDevMode && assertTNode(tNode);
let insertBeforeIndex = tNode.insertBeforeIndex;
if (insertBeforeIndex === null) {
insertBeforeIndex = tNode.insertBeforeIndex =
[null!/* may be updated to number later */, index];
} else {
assertEqual(Array.isArray(insertBeforeIndex), true, 'Expecting array here');
(insertBeforeIndex as number[]).push(index);
}
}
/**
* Create `TNode.type=TNodeType.Placeholder` node.
*
* See `TNodeType.Placeholder` for more information.
*/
export function createTNodePlaceholder(
tView: TView, previousTNodes: TNode[], index: number): TNode {
const tNode = createTNodeAtIndex(tView, index, TNodeType.Placeholder, null, null);
addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tNode);
return tNode;
}

View File

@ -10,19 +10,18 @@ import {assertDefined, assertEqual, assertIndexInRange} from '../../util/assert'
import {assertFirstCreatePass, assertHasParent} from '../assert';
import {attachPatchData} from '../context_discovery';
import {registerPostOrderHooks} from '../hooks';
import {hasClassInput, hasStyleInput, TAttributes, TElementNode, TNode, TNodeType} from '../interfaces/node';
import {hasClassInput, hasStyleInput, TAttributes, TElementNode, TNode, TNodeFlags, TNodeType} from '../interfaces/node';
import {RElement} from '../interfaces/renderer';
import {isContentQueryHost, isDirectiveHost} from '../interfaces/type_checks';
import {HEADER_OFFSET, LView, RENDERER, T_HOST, TVIEW, TView} from '../interfaces/view';
import {HEADER_OFFSET, LView, RENDERER, TView} from '../interfaces/view';
import {assertNodeType} from '../node_assert';
import {appendChild, writeDirectClass, writeDirectStyle} from '../node_manipulation';
import {appendChild, createElementNode, writeDirectClass, writeDirectStyle} from '../node_manipulation';
import {decreaseElementDepthCount, getBindingIndex, getCurrentTNode, getElementDepthCount, getLView, getNamespace, getTView, increaseElementDepthCount, isCurrentTNodeParent, setCurrentTNode, setCurrentTNodeAsNotParent} from '../state';
import {computeStaticStyling} from '../styling/static_styling';
import {setUpAttributes} from '../util/attrs_utils';
import {getConstant} from '../util/view_utils';
import {setDirectiveInputsWhichShadowsStyling} from './property';
import {createDirectivesInstances, elementCreate, executeContentQueries, getOrCreateTNode, matchingSchemas, resolveDirectives, saveResolvedLocalsInData} from './shared';
import {createDirectivesInstances, executeContentQueries, getOrCreateTNode, matchingSchemas, resolveDirectives, saveResolvedLocalsInData} from './shared';
function elementStartFirstCreatePass(
@ -78,12 +77,10 @@ export function ɵɵelementStart(
assertEqual(
getBindingIndex(), tView.bindingStartIndex,
'elements should be created before any bindings');
ngDevMode && ngDevMode.rendererCreateElement++;
ngDevMode && assertIndexInRange(lView, adjustedIndex);
const renderer = lView[RENDERER];
const native = lView[adjustedIndex] = elementCreate(name, renderer, getNamespace());
const native = lView[adjustedIndex] = createElementNode(renderer, name, getNamespace());
const tNode = tView.firstCreatePass ?
elementStartFirstCreatePass(index, tView, lView, native, name, attrsIndex, localRefsIndex) :
tView.data[adjustedIndex] as TElementNode;
@ -102,7 +99,11 @@ export function ɵɵelementStart(
writeDirectStyle(renderer, native, styles);
}
appendChild(tView, lView, native, tNode);
if ((tNode.flags & TNodeFlags.isDetached) !== TNodeFlags.isDetached) {
// In the i18n case, the translation may have removed this element, so only add it if it is not
// detached. See `TNodeType.Placeholder` and `LFrame.inI18n` for more context.
appendChild(tView, lView, native, tNode);
}
// any immediate children of a component or template container must be pre-emptively
// monkey-patched with the component view data so that the element can be inspected

View File

@ -10,15 +10,16 @@ import '../../util/ng_i18n_closure_mode';
import {assertDefined} from '../../util/assert';
import {bindingUpdated} from '../bindings';
import {applyI18n, i18nEndFirstPass, pushI18nIndex, setMaskBit} from '../i18n/i18n_apply';
import {i18nAttributesFirstPass, i18nStartFirstPass} from '../i18n/i18n_parse';
import {applyCreateOpCodes, applyI18n, setMaskBit} from '../i18n/i18n_apply';
import {i18nAttributesFirstPass, i18nStartFirstCreatePass} from '../i18n/i18n_parse';
import {i18nPostprocess} from '../i18n/i18n_postprocess';
import {HEADER_OFFSET} from '../interfaces/view';
import {getLView, getTView, nextBindingIndex} from '../state';
import {TI18n} from '../interfaces/i18n';
import {TElementNode, TNodeType} from '../interfaces/node';
import {HEADER_OFFSET, T_HOST} from '../interfaces/view';
import {getClosestRElement} from '../node_manipulation';
import {getCurrentParentTNode, getLView, getTView, nextBindingIndex, setInI18nBlock} from '../state';
import {getConstant} from '../util/view_utils';
import {setDelayProjection} from './projection';
/**
* Marks a block of text as translatable.
*
@ -34,10 +35,6 @@ import {setDelayProjection} from './projection';
* and end of DOM element that were embedded in the original translation block. The placeholder
* `index` points to the element index in the template instructions set. An optional `block` that
* matches the sub-template in which it was declared.
* - `<EFBFBD>!{index}(:{block})<29>`/`<EFBFBD>/!{index}(:{block})<29>`: *Projection Placeholder*: Marks the
* beginning and end of <ng-content> that was embedded in the original translation block.
* The placeholder `index` points to the element index in the template instructions set.
* An optional `block` that matches the sub-template in which it was declared.
* - `<EFBFBD>*{index}:{block}<7D>`/`<EFBFBD>/*{index}:{block}<7D>`: *Sub-template Placeholder*: Sub-templates must be
* split up and translated separately in each angular template function. The `index` points to the
* `template` instruction index. A `block` that matches the sub-template in which it was declared.
@ -48,16 +45,28 @@ import {setDelayProjection} from './projection';
*
* @codeGenApi
*/
export function ɵɵi18nStart(index: number, messageIndex: number, subTemplateIndex?: number): void {
export function ɵɵi18nStart(
index: number, messageIndex: number, subTemplateIndex: number = -1): void {
const tView = getTView();
const lView = getLView();
ngDevMode && assertDefined(tView, `tView should be defined`);
const message = getConstant<string>(tView.consts, messageIndex)!;
pushI18nIndex(index);
// We need to delay projections until `i18nEnd`
setDelayProjection(true);
if (tView.firstCreatePass && tView.data[index + HEADER_OFFSET] === null) {
i18nStartFirstPass(getLView(), tView, index, message, subTemplateIndex);
const parentTNode = getCurrentParentTNode() as TElementNode | null;
if (tView.firstCreatePass) {
i18nStartFirstCreatePass(
tView, parentTNode === null ? 0 : parentTNode.index, lView, index, message,
subTemplateIndex);
}
const tI18n = tView.data[HEADER_OFFSET + index] as TI18n;
const sameViewParentTNode = parentTNode === lView[T_HOST] ? null : parentTNode;
const parentRNode = getClosestRElement(tView, sameViewParentTNode, lView);
// If `parentTNode` is an `ElementContainer` than it has `<!--ng-container--->`.
// When we do inserts we have to make sure to insert in front of `<!--ng-container--->`.
const insertInFrontOf = parentTNode && parentTNode.type === TNodeType.ElementContainer ?
lView[parentTNode.index] :
null;
applyCreateOpCodes(lView, tI18n.create, parentRNode, insertInFrontOf);
setInI18nBlock(true);
}
@ -69,12 +78,7 @@ export function ɵɵi18nStart(index: number, messageIndex: number, subTemplateIn
* @codeGenApi
*/
export function ɵɵi18nEnd(): void {
const lView = getLView();
const tView = getTView();
ngDevMode && assertDefined(tView, `tView should be defined`);
i18nEndFirstPass(tView, lView);
// Stop delaying projections
setDelayProjection(false);
setInI18nBlock(false);
}
/**

View File

@ -0,0 +1,93 @@
/**
* @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 {assertDomNode, assertEqual, assertNumber, assertNumberInRange} from '../../util/assert';
import {assertTIcu, assertTNodeForLView} from '../assert';
import {EMPTY_ARRAY} from '../empty';
import {getCurrentICUCaseIndex, I18nMutateOpCode, I18nMutateOpCodes, TIcu} from '../interfaces/i18n';
import {TIcuContainerNode} from '../interfaces/node';
import {RNode} from '../interfaces/renderer';
import {LView, TVIEW} from '../interfaces/view';
export function loadIcuContainerVisitor() {
const _stack: any[] = [];
let _index: number = -1;
let _lView: LView;
let _removes: I18nMutateOpCodes;
/**
* Retrieves a set of root nodes from `TIcu.remove`. Used by `TNodeType.ICUContainer`
* to determine which root belong to the ICU.
*
* Example of usage.
* ```
* const nextRNode = icuContainerIteratorStart(tIcuContainerNode, lView);
* let rNode: RNode|null;
* while(rNode = nextRNode()) {
* console.log(rNode);
* }
* ```
*
* @param tIcuContainerNode Current `TIcuContainerNode`
* @param lView `LView` where the `RNode`s should be looked up.
*/
function icuContainerIteratorStart(tIcuContainerNode: TIcuContainerNode, lView: LView): () =>
RNode | null {
_lView = lView;
while (_stack.length) _stack.pop();
// FIXME(misko): This is a hack which allows us to associate `TI18n` with `TNode`.
// This should be refactored so that one can attach arbitrary data with `TNode`
ngDevMode && assertTNodeForLView(tIcuContainerNode, lView);
const tIcu: TIcu = tIcuContainerNode.tagName as any;
enterIcu(tIcu, lView);
return icuContainerIteratorNext;
}
function enterIcu(tIcu: TIcu, lView: LView) {
_index = 0;
const currentCase = getCurrentICUCaseIndex(tIcu, lView);
if (currentCase !== null) {
ngDevMode && assertNumberInRange(currentCase, 0, tIcu.cases.length - 1);
_removes = tIcu.remove[currentCase];
} else {
_removes = EMPTY_ARRAY;
}
}
function icuContainerIteratorNext(): RNode|null {
if (_index < _removes.length) {
const removeOpCode = _removes[_index++] as number;
ngDevMode && assertNumber(removeOpCode, 'Expecting OpCode number');
const opCode = removeOpCode & I18nMutateOpCode.MASK_INSTRUCTION;
if (opCode === I18nMutateOpCode.Remove) {
const rNode = _lView[removeOpCode >>> I18nMutateOpCode.SHIFT_REF];
ngDevMode && assertDomNode(rNode);
return rNode;
} else {
ngDevMode &&
assertEqual(opCode, I18nMutateOpCode.RemoveNestedIcu, 'Expecting RemoveNestedIcu');
_stack.push(_index, _removes);
const tIcu = _lView[TVIEW].data[removeOpCode >>> I18nMutateOpCode.SHIFT_REF] as TIcu;
ngDevMode && assertTIcu(tIcu);
enterIcu(tIcu, _lView);
return icuContainerIteratorNext();
}
} else {
if (_stack.length === 0) {
return null;
} else {
_removes = _stack.pop();
_index = _stack.pop();
return icuContainerIteratorNext();
}
}
}
return icuContainerIteratorStart;
}

View File

@ -17,7 +17,7 @@ import {getInjectorIndex} from '../di';
import {CONTAINER_HEADER_OFFSET, HAS_TRANSPLANTED_VIEWS, LContainer, MOVED_VIEWS, NATIVE} from '../interfaces/container';
import {ComponentTemplate, DirectiveDef, DirectiveDefList, PipeDefList, ViewQueriesFunction} from '../interfaces/definition';
import {NO_PARENT_INJECTOR, NodeInjectorOffset} from '../interfaces/injector';
import {AttributeMarker, PropertyAliases, TConstants, TContainerNode, TElementNode, TNode as ITNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TNodeTypeAsString} from '../interfaces/node';
import {AttributeMarker, InsertBeforeIndex, PropertyAliases, TConstants, TContainerNode, TElementNode, TNode as ITNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TNodeTypeAsString} from '../interfaces/node';
import {SelectorFlags} from '../interfaces/projection';
import {LQueries, TQueries} from '../interfaces/query';
import {RComment, RElement, Renderer3, RendererFactory3, RNode} from '../interfaces/renderer';
@ -160,7 +160,13 @@ export const TViewConstructor = class TView implements ITView {
return TViewTypeAsString[this.type] || `TViewType.?${this.type}?`;
}
get i18nStartIndex(): number {
/**
* Returns initial value of `expandoStartIndex`.
*/
// FIXME(misko): `originalExpandoStartIndex` should not be needed because it should be the same as
// `expandoStartIndex`. However `expandoStartIndex` is misnamed as it changes as more items get
// allocated in expando.
get originalExpandoStartIndex(): number {
return HEADER_OFFSET + this._decls + this._vars;
}
};
@ -170,6 +176,7 @@ class TNode implements ITNode {
public tView_: TView, //
public type: TNodeType, //
public index: number, //
public insertBeforeIndex: InsertBeforeIndex, //
public injectorIndex: number, //
public directiveStart: number, //
public directiveEnd: number, //
@ -249,8 +256,13 @@ class TNode implements ITNode {
}
get template_(): string {
if (this.tagName === null && this.type === TNodeType.Element) return '#text';
const buf: string[] = [];
buf.push('<', this.tagName || this.type_);
const tagName = typeof this.tagName === 'string' && this.tagName || this.type_;
buf.push('<', tagName);
if (this.flags) {
buf.push(' ', this.flags_);
}
if (this.attrs) {
for (let i = 0; i < this.attrs.length;) {
const attrName = this.attrs[i++];
@ -263,7 +275,7 @@ class TNode implements ITNode {
}
buf.push('>');
processTNodeChildren(this.child, buf);
buf.push('</', this.tagName || this.type_, '>');
buf.push('</', tagName, '>');
return buf.join('');
}
@ -444,7 +456,7 @@ export class LViewDebug implements ILViewDebug {
return toHtml(this._raw_lView[HOST], true);
}
get html(): string {
return (this.nodes || []).map(node => toHtml(node.native, true)).join('');
return (this.nodes || []).map(mapToHTML).join('');
}
get context(): {}|null {
return this._raw_lView[CONTEXT];
@ -458,7 +470,9 @@ export class LViewDebug implements ILViewDebug {
const tNode = lView[TVIEW].firstChild;
return toDebugNodes(tNode, lView);
}
get template(): string {
return (this.tView as any as {template_: string}).template_;
}
get tView(): ITView {
return this._raw_lView[TVIEW];
}
@ -504,20 +518,15 @@ export class LViewDebug implements ILViewDebug {
const tView = this.tView;
return toLViewRange(
tView, this._raw_lView, tView.bindingStartIndex,
(tView as any as {i18nStartIndex: number}).i18nStartIndex);
}
get i18n(): LViewDebugRange {
const tView = this.tView;
return toLViewRange(
tView, this._raw_lView, (tView as any as {i18nStartIndex: number}).i18nStartIndex,
tView.expandoStartIndex);
(tView as any as {originalExpandoStartIndex: number}).originalExpandoStartIndex);
}
get expando(): LViewDebugRange {
const tView = this.tView as any as {_decls: number, _vars: number};
return toLViewRange(
this.tView, this._raw_lView, this.tView.expandoStartIndex, this._raw_lView.length);
this.tView, this._raw_lView,
(tView as any as {originalExpandoStartIndex: number}).originalExpandoStartIndex,
this._raw_lView.length);
}
/**
@ -534,6 +543,16 @@ export class LViewDebug implements ILViewDebug {
}
}
function mapToHTML(node: DebugNode): string {
if (node.type === 'ElementContainer') {
return (node.children || []).map(mapToHTML).join('');
} else if (node.type === 'IcuContainer') {
throw new Error('Not implemented');
} else {
return toHtml(node.native, true) || '';
}
}
function toLViewRange(tView: TView, lView: LView, start: number, end: number): LViewDebugRange {
let content: LViewDebugRangeContent[] = [];
for (let index = start; index < end; index++) {

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {newArray} from '../../util/array_utils';
import {TAttributes, TElementNode, TNode, TNodeType} from '../interfaces/node';
import {TAttributes, TElementNode, TNode, TNodeFlags, TNodeType} from '../interfaces/node';
import {ProjectionSlots} from '../interfaces/projection';
import {DECLARATION_COMPONENT_VIEW, T_HOST} from '../interfaces/view';
import {applyProjection} from '../node_manipulation';
@ -103,11 +103,6 @@ export function ɵɵprojectionDef(projectionSlots?: ProjectionSlots): void {
}
}
let delayProjection = false;
export function setDelayProjection(value: boolean) {
delayProjection = value;
}
/**
* Inserts previously re-distributed projected nodes. This instruction must be preceded by a call
@ -133,8 +128,7 @@ export function ɵɵprojection(
// `<ng-content>` has no content
setCurrentTNodeAsNotParent();
// We might need to delay the projection of nodes if they are in the middle of an i18n block
if (!delayProjection) {
if ((tProjectionNode.flags & TNodeFlags.isDetached) !== TNodeFlags.isDetached) {
// re-distribution of projectable nodes is stored on a component's view level
applyProjection(tView, lView, tProjectionNode);
}

View File

@ -5,43 +5,43 @@
* 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 {Injector} from '../../di';
import {ErrorHandler} from '../../error_handler';
import {DoCheck, OnChanges, OnInit} from '../../interface/lifecycle_hooks';
import {CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA, SchemaMetadata} from '../../metadata/schema';
import {ViewEncapsulation} from '../../metadata/view';
import {validateAgainstEventAttributes, validateAgainstEventProperties} from '../../sanitization/sanitization';
import {Sanitizer} from '../../sanitization/sanitizer';
import {assertDefined, assertDomNode, assertEqual, assertGreaterThan, assertIndexInRange, assertLessThan, assertNotEqual, assertNotSame, assertSame} from '../../util/assert';
import {createNamedArrayType} from '../../util/named_array_type';
import {initNgDevMode} from '../../util/ng_dev_mode';
import {normalizeDebugBindingName, normalizeDebugBindingValue} from '../../util/ng_reflect';
import {stringify} from '../../util/stringify';
import {assertFirstCreatePass, assertLContainer, assertLView, assertTNodeForLView} from '../assert';
import {attachPatchData} from '../context_discovery';
import {getFactoryDef} from '../definition';
import {diPublicInInjector, getNodeInjectable, getOrCreateNodeInjectorForNode} from '../di';
import {throwMultipleComponentError} from '../errors';
import {executeCheckHooks, executeInitAndCheckHooks, incrementInitPhaseFlags} from '../hooks';
import {CONTAINER_HEADER_OFFSET, HAS_TRANSPLANTED_VIEWS, LContainer, MOVED_VIEWS} from '../interfaces/container';
import {ComponentDef, ComponentTemplate, DirectiveDef, DirectiveDefListOrFactory, PipeDefListOrFactory, RenderFlags, ViewQueriesFunction} from '../interfaces/definition';
import {NodeInjectorFactory, NodeInjectorOffset} from '../interfaces/injector';
import {AttributeMarker, InitialInputData, InitialInputs, LocalRefExtractor, PropertyAliases, PropertyAliasValue, TAttributes, TConstantsOrFactory, TContainerNode, TDirectiveHostNode, TElementContainerNode, TElementNode, TIcuContainerNode, TNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TProjectionNode} from '../interfaces/node';
import {isProceduralRenderer, RComment, RElement, Renderer3, RendererFactory3, RNode, RText} from '../interfaces/renderer';
import {SanitizerFn} from '../interfaces/sanitization';
import {isComponentDef, isComponentHost, isContentQueryHost, isRootView} from '../interfaces/type_checks';
import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_COMPONENT_VIEW, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, InitPhaseState, INJECTOR, LView, LViewFlags, NEXT, PARENT, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, T_HOST, TData, TRANSPLANTED_VIEWS_TO_REFRESH, TVIEW, TView, TViewType} from '../interfaces/view';
import {assertNodeNotOfTypes, assertNodeOfPossibleTypes} from '../node_assert';
import {isInlineTemplate, isNodeMatchingSelectorList} from '../node_selector_matcher';
import {enterView, getBindingsEnabled, getCurrentDirectiveIndex, getCurrentTNode, getSelectedIndex, isCurrentTNodeParent, isInCheckNoChangesMode, leaveView, setBindingIndex, setBindingRootForHostBindings, setCurrentDirectiveIndex, setCurrentQueryIndex, setCurrentTNode, setIsInCheckNoChangesMode, setSelectedIndex} from '../state';
import {NO_CHANGE} from '../tokens';
import {isAnimationProp, mergeHostAttrs} from '../util/attrs_utils';
import {INTERPOLATION_DELIMITER, renderStringify, stringifyForError} from '../util/misc_utils';
import {getFirstLContainer, getLViewParent, getNextLContainer} from '../util/view_traversal_utils';
import {getComponentLViewByIndex, getNativeByIndex, getNativeByTNode, isCreationMode, readPatchedLView, resetPreOrderHookFlags, unwrapLView, updateTransplantedViewCount, viewAttachedToChangeDetector} from '../util/view_utils';
import {selectIndexInternal} from './advance';
import {attachLContainerDebug, attachLViewDebug, cloneToLViewFromTViewBlueprint, cloneToTViewData, LCleanup, LViewBlueprint, MatchesArray, TCleanup, TNodeDebug, TNodeInitialInputs, TNodeLocalNames, TViewComponents, TViewConstructor} from './lview_debug';
import { Injector } from '../../di';
import { ErrorHandler } from '../../error_handler';
import { DoCheck, OnChanges, OnInit } from '../../interface/lifecycle_hooks';
import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA, SchemaMetadata } from '../../metadata/schema';
import { ViewEncapsulation } from '../../metadata/view';
import { validateAgainstEventAttributes, validateAgainstEventProperties } from '../../sanitization/sanitization';
import { Sanitizer } from '../../sanitization/sanitizer';
import { assertDefined, assertDomNode, assertEqual, assertGreaterThan, assertIndexInRange, assertLessThan, assertNotEqual, assertNotSame, assertSame, assertString } from '../../util/assert';
import { createNamedArrayType } from '../../util/named_array_type';
import { initNgDevMode } from '../../util/ng_dev_mode';
import { normalizeDebugBindingName, normalizeDebugBindingValue } from '../../util/ng_reflect';
import { stringify } from '../../util/stringify';
import { assertFirstCreatePass, assertFirstUpdatePass, assertLContainer, assertLView, assertTNodeForLView, assertTNodeForTView } from '../assert';
import { attachPatchData } from '../context_discovery';
import { getFactoryDef } from '../definition';
import { diPublicInInjector, getNodeInjectable, getOrCreateNodeInjectorForNode } from '../di';
import { throwMultipleComponentError } from '../errors';
import { executeCheckHooks, executeInitAndCheckHooks, incrementInitPhaseFlags } from '../hooks';
import { CONTAINER_HEADER_OFFSET, HAS_TRANSPLANTED_VIEWS, LContainer, MOVED_VIEWS } from '../interfaces/container';
import { ComponentDef, ComponentTemplate, DirectiveDef, DirectiveDefListOrFactory, PipeDefListOrFactory, RenderFlags, ViewQueriesFunction } from '../interfaces/definition';
import { NodeInjectorFactory, NodeInjectorOffset } from '../interfaces/injector';
import { AttributeMarker, InitialInputData, InitialInputs, LocalRefExtractor, PropertyAliases, PropertyAliasValue, TAttributes, TConstantsOrFactory, TContainerNode, TDirectiveHostNode, TElementContainerNode, TElementNode, TIcuContainerNode, TNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TProjectionNode } from '../interfaces/node';
import { isProceduralRenderer, RComment, RElement, Renderer3, RendererFactory3, RNode, RText } from '../interfaces/renderer';
import { SanitizerFn } from '../interfaces/sanitization';
import { isComponentDef, isComponentHost, isContentQueryHost, isRootView } from '../interfaces/type_checks';
import { CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_COMPONENT_VIEW, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, InitPhaseState, INJECTOR, LView, LViewFlags, NEXT, PARENT, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, TData, TRANSPLANTED_VIEWS_TO_REFRESH, TVIEW, TView, TViewType, T_HOST } from '../interfaces/view';
import { assertNodeNotOfTypes, assertNodeOfPossibleTypes } from '../node_assert';
import { updateTextNode } from '../node_manipulation';
import { isInlineTemplate, isNodeMatchingSelectorList } from '../node_selector_matcher';
import { enterView, getBindingsEnabled, getCurrentDirectiveIndex, getCurrentParentTNode, getCurrentTNode, getCurrentTNodePlaceholderOk, getSelectedIndex, isCurrentTNodeParent, isInCheckNoChangesMode, isInI18nBlock, leaveView, setBindingIndex, setBindingRootForHostBindings, setCurrentDirectiveIndex, setCurrentQueryIndex, setCurrentTNode, setIsInCheckNoChangesMode, setSelectedIndex } from '../state';
import { NO_CHANGE } from '../tokens';
import { isAnimationProp, mergeHostAttrs } from '../util/attrs_utils';
import { INTERPOLATION_DELIMITER, renderStringify, stringifyForError } from '../util/misc_utils';
import { getFirstLContainer, getLViewParent, getNextLContainer } from '../util/view_traversal_utils';
import { getComponentLViewByIndex, getNativeByIndex, getNativeByTNode, isCreationMode, readPatchedLView, resetPreOrderHookFlags, unwrapLView, updateTransplantedViewCount, viewAttachedToChangeDetector } from '../util/view_utils';
import { selectIndexInternal } from './advance';
import { attachLContainerDebug, attachLViewDebug, cloneToLViewFromTViewBlueprint, cloneToTViewData, LCleanup, LViewBlueprint, MatchesArray, TCleanup, TNodeDebug, TNodeInitialInputs, TNodeLocalNames, TViewComponents, TViewConstructor } from './lview_debug';
@ -155,21 +155,6 @@ function renderChildComponents(hostLView: LView, components: number[]): void {
}
}
/**
* Creates a native element from a tag name, using a renderer.
* @param name the tag name
* @param renderer A renderer to use
* @returns the element created
*/
export function elementCreate(name: string, renderer: Renderer3, namespace: string|null): RElement {
if (isProceduralRenderer(renderer)) {
return renderer.createElement(name, namespace);
} else {
return namespace === null ? renderer.createElement(name) :
renderer.createElementNS(namespace, name);
}
}
export function createLView<T>(
parentLView: LView|null, tView: TView, context: T|null, flags: LViewFlags, host: RElement|null,
tHostNode: TNode|null, rendererFactory: RendererFactory3|null, renderer: Renderer3|null,
@ -230,17 +215,34 @@ export function getOrCreateTNode(
TElementNode&TContainerNode&TElementContainerNode&TProjectionNode&TIcuContainerNode {
// Keep this function short, so that the VM will inline it.
const adjustedIndex = index + HEADER_OFFSET;
const tNode = tView.data[adjustedIndex] as TNode ||
createTNodeAtIndex(tView, adjustedIndex, type, name, attrs);
let tNode = tView.data[adjustedIndex] as TNode;
if (tNode === null) {
tNode = createTNodeAtIndex(tView, adjustedIndex, type, name, attrs);
if (isInI18nBlock()) {
// If we are in i18n block then all elements should be pre declared through `Placeholder`
// See `TNodeType.Placeholder` and `LFrame.inI18n` for more context.
// If the `TNode` was not pre-declared than it means it was not mentioned which means it was
// removed, so we mark it as detached.
tNode.flags |= TNodeFlags.isDetached;
}
} else if (tNode.type == TNodeType.Placeholder) {
tNode.type = type;
tNode.tagName = name;
tNode.attrs = attrs;
const parent = getCurrentParentTNode();
tNode.injectorIndex = parent === null ? -1 : parent.injectorIndex;
ngDevMode && assertTNodeForTView(tNode, tView);
ngDevMode && assertEqual(index + HEADER_OFFSET, tNode.index, 'Expecting same index');
}
setCurrentTNode(tNode, true);
return tNode as TElementNode & TContainerNode & TElementContainerNode & TProjectionNode &
TIcuContainerNode;
}
function createTNodeAtIndex(
export function createTNodeAtIndex(
tView: TView, adjustedIndex: number, type: TNodeType, name: string|null,
attrs: TAttributes|null) {
const currentTNode = getCurrentTNode();
const currentTNode = getCurrentTNodePlaceholderOk();
const isParent = isCurrentTNodeParent();
const parent = isParent ? currentTNode : currentTNode && currentTNode.parent;
// Parents cannot cross component boundaries because components will be used in multiple places.
@ -253,11 +255,18 @@ function createTNodeAtIndex(
tView.firstChild = tNode;
}
if (currentTNode !== null) {
if (isParent && currentTNode.child == null && tNode.parent !== null) {
// We are in the same view, which means we are adding content node to the parent view.
currentTNode.child = tNode;
} else if (!isParent) {
currentTNode.next = tNode;
if (isParent) {
// FIXME(misko): This logic looks unnecessarily complicated. Could we simplify?
if (currentTNode.child == null && tNode.parent !== null) {
// We are in the same view, which means we are adding content node to the parent view.
currentTNode.child = tNode;
}
} else {
if (currentTNode.next === null) {
// In the case of i18n the `currentTNode` may already be linked, in which case we don't want
// to break the links which i18n created.
currentTNode.next = tNode;
}
}
}
return tNode;
@ -266,36 +275,40 @@ function createTNodeAtIndex(
/**
* When elements are created dynamically after a view blueprint is created (e.g. through
* i18nApply() or ComponentFactory.create), we need to adjust the blueprint for future
* i18nApply()), we need to adjust the blueprint for future
* template passes.
*
* @param tView `TView` associated with `LView`
* @param view The `LView` containing the blueprint to adjust
* @param lView The `LView` containing the blueprint to adjust
* @param numSlotsToAlloc The number of slots to alloc in the LView, should be >0
*/
export function allocExpando(tView: TView, lView: LView, numSlotsToAlloc: number) {
ngDevMode &&
assertGreaterThan(
numSlotsToAlloc, 0, 'The number of slots to alloc should be greater than 0');
if (numSlotsToAlloc > 0) {
if (tView.firstCreatePass) {
for (let i = 0; i < numSlotsToAlloc; i++) {
tView.blueprint.push(null);
tView.data.push(null);
lView.push(null);
}
// We should only increment the expando start index if there aren't already directives
// and injectors saved in the "expando" section
if (!tView.expandoInstructions) {
tView.expandoStartIndex += numSlotsToAlloc;
} else {
// Since we're adding the dynamic nodes into the expando section, we need to let the host
// bindings know that they should skip x slots
tView.expandoInstructions.push(numSlotsToAlloc);
}
}
export function allocExpando(tView: TView, lView: LView, numSlotsToAlloc: number): number {
if (ngDevMode) {
assertGreaterThan(numSlotsToAlloc, 0, 'The number of slots to alloc should be greater than 0');
assertEqual(tView.data.length, lView.length, 'Expecting LView to be same size as TView');
assertEqual(
tView.data.length, tView.blueprint.length, 'Expecting Blueprint to be same size as TView');
assertFirstUpdatePass(tView);
}
const allocIdx = lView.length;
for (let i = 0; i < numSlotsToAlloc; i++) {
tView.blueprint.push(null);
tView.data.push(null);
lView.push(null);
}
// We should only increment the expando start index if there aren't already directives
// and injectors saved in the "expando" section
if (!tView.expandoInstructions) {
tView.expandoStartIndex += numSlotsToAlloc;
} else {
// Since we're adding the dynamic nodes into the expando section, we need to let the host
// bindings know that they should skip x slots
// FIXME(misko): Refactor `expandoInstructions` so that it does not rely on relative binding
// offsets, but absolute values which Means we would not have to store it here.
tView.expandoInstructions.push(numSlotsToAlloc);
}
return allocIdx;
}
@ -824,12 +837,14 @@ export function createTNode(
tagName: string|null, attrs: TAttributes|null): TNode {
ngDevMode && assertNotSame(attrs, undefined, '\'undefined\' is not valid value for \'attrs\'');
ngDevMode && ngDevMode.tNode++;
ngDevMode && tParent && assertTNodeForTView(tParent, tView);
let injectorIndex = tParent ? tParent.injectorIndex : -1;
const tNode = ngDevMode ?
new TNodeDebug(
tView, // tView_: TView
type, // type: TNodeType
adjustedIndex, // index: number
null, // insertBeforeIndex: null|-1|number|number[]
injectorIndex, // injectorIndex: number
-1, // directiveStart: number
-1, // directiveEnd: number
@ -862,6 +877,7 @@ export function createTNode(
{
type: type,
index: adjustedIndex,
insertBeforeIndex: null,
injectorIndex: injectorIndex,
directiveStart: -1,
directiveEnd: -1,
@ -1509,7 +1525,12 @@ export function elementAttributeInternal(
`Host bindings are not valid on ng-container or ng-template.`);
}
const element = getNativeByTNode(tNode, lView) as RElement;
const renderer = lView[RENDERER];
setElementAttribute(lView[RENDERER], element, namespace, tNode.tagName, name, value, sanitizer);
}
export function setElementAttribute(
renderer: Renderer3, element: RElement, namespace: string|null|undefined, tagName: string|null,
name: string, value: any, sanitizer: SanitizerFn|null|undefined) {
if (value == null) {
ngDevMode && ngDevMode.rendererRemoveAttribute++;
isProceduralRenderer(renderer) ? renderer.removeAttribute(element, name, namespace) :
@ -1517,7 +1538,7 @@ export function elementAttributeInternal(
} else {
ngDevMode && ngDevMode.rendererSetAttribute++;
const strValue =
sanitizer == null ? renderStringify(value) : sanitizer(value, tNode.tagName || '', name);
sanitizer == null ? renderStringify(value) : sanitizer(value, tagName || '', name);
if (isProceduralRenderer(renderer)) {
@ -2065,11 +2086,10 @@ export function setInputsForProperty(
* Updates a text binding at a given index in a given LView.
*/
export function textBindingInternal(lView: LView, index: number, value: string): void {
ngDevMode && assertString(value, 'Value should be a string');
ngDevMode && assertNotSame(value, NO_CHANGE as any, 'value should not be NO_CHANGE');
ngDevMode && assertIndexInRange(lView, index + HEADER_OFFSET);
const element = getNativeByIndex(index, lView) as any as RText;
ngDevMode && assertDefined(element, 'native element should exist');
ngDevMode && ngDevMode.rendererSetText++;
const renderer = lView[RENDERER];
isProceduralRenderer(renderer) ? renderer.setValue(element, value) : element.textContent = value;
updateTextNode(lView[RENDERER], element, value);
}

View File

@ -38,7 +38,7 @@ export function ɵɵtext(index: number, value: string = ''): void {
getOrCreateTNode(tView, index, TNodeType.Element, null, null) :
tView.data[adjustedIndex] as TElementNode;
const textNative = lView[adjustedIndex] = createTextNode(value, lView[RENDERER]);
const textNative = lView[adjustedIndex] = createTextNode(lView[RENDERER], value);
appendChild(tView, lView, textNative, tNode);
// Text nodes are self closing.

View File

@ -6,7 +6,11 @@
* found in the LICENSE file at https://angular.io/license
*/
import {assertGreaterThan, assertGreaterThanOrEqual} from '../../util/assert';
import {TIcuContainerNode} from './node';
import {RNode} from './renderer';
import {SanitizerFn} from './sanitization';
import {LView} from './view';
/**
* `I18nMutateOpCode` defines OpCodes for `I18nMutateOpCodes` array.
@ -43,6 +47,7 @@ export const enum I18nMutateOpCode {
/**
* Mask for OpCode
*/
// FIXME(misko): Shrink mask to 2 bits as 4 choices can fit into two bits.
MASK_INSTRUCTION = 0b111,
/**
@ -53,11 +58,6 @@ export const enum I18nMutateOpCode {
// 11111110000000000
// 65432109876543210
/**
* Instruction to select a node. (next OpCode will contain the operation.)
*/
Select = 0b000,
/**
* Instruction to append the current node to `PARENT`.
*/
@ -73,29 +73,37 @@ export const enum I18nMutateOpCode {
*/
Attr = 0b100,
/**
* Instruction to simulate elementEnd()
*/
ElementEnd = 0b101,
/**
* Instruction to removed the nested ICU.
*/
RemoveNestedIcu = 0b110,
}
// FIXME(misko): These function are technically not interfaces, and so we may consider moving them
// elsewhere.
// FIXME(misko): rename to `getParentFromI18nCreateOpCode`
export function getParentFromI18nMutateOpCode(mergedCode: number): number {
return mergedCode >>> I18nMutateOpCode.SHIFT_PARENT;
}
// FIXME(misko): rename to `getRefFromI18nCreateOpCode`
export function getRefFromI18nMutateOpCode(mergedCode: number): number {
return (mergedCode & I18nMutateOpCode.MASK_REF) >>> I18nMutateOpCode.SHIFT_REF;
}
// FIXME(misko): rename to `getInstructionFromI18nCreateOpCode`
export function getInstructionFromI18nMutateOpCode(mergedCode: number): number {
return mergedCode & I18nMutateOpCode.MASK_INSTRUCTION;
}
// FIXME(misko): rename to `i18nCreateOpCode`
export function i18nMutateOpCode(opCode: I18nMutateOpCode, parentIdx: number, refIdx: number) {
ngDevMode && assertGreaterThanOrEqual(parentIdx, 0, 'Missing parent index');
ngDevMode && assertGreaterThan(refIdx, 0, 'Missing ref index');
return opCode | parentIdx << I18nMutateOpCode.SHIFT_PARENT | refIdx << I18nMutateOpCode.SHIFT_REF;
}
/**
* Marks that the next string is an element name.
*
@ -113,6 +121,7 @@ export interface ELEMENT_MARKER {
*
* See `I18nMutateOpCodes` documentation.
*/
// FIXME(misko): Rename to ICU marker
export const COMMENT_MARKER: COMMENT_MARKER = {
marker: 'comment'
};
@ -132,6 +141,62 @@ export interface I18nDebug {
debug?: string[];
}
/**
* Array storing OpCode for dynamically creating `i18n` translation DOM elements.
*
* This array creates a sequence of `Text` and `Comment` (as ICU anchor) DOM elements. It consists
* of a pair of `number` and `string` pairs which encode the operations for the creation of the
* translated block.
*
* The number is shifted and encoded according to `I18nCreateOpCode`
*
* Pseudocode:
* ```
* const i18nCreateOpCodes = [
* 10 << I18nCreateOpCode.SHIFT, "Text Node add to DOM",
* 11 << I18nCreateOpCode.SHIFT | I18nCreateOpCode.COMMENT, "Comment Node add to DOM",
* 12 << I18nCreateOpCode.SHIFT | I18nCreateOpCode.APPEND_LATER, "Text Node added later"
* ];
*
* for(var i=0; i<i18nCreateOpCodes.length; i++) {
* const opcode = i18NCreateOpCodes[i++];
* const index = opcode >> I18nCreateOpCode.SHIFT;
* const text = i18NCreateOpCodes[i];
* let node: Text|Comment;
* if (opcode & I18nCreateOpCode.COMMENT === I18nCreateOpCode.COMMENT) {
* node = lView[~index] = document.createComment(text);
* } else {
* node = lView[index] = document.createText(text);
* }
* if (opcode & I18nCreateOpCode.APPEND_EAGERLY !== I18nCreateOpCode.APPEND_EAGERLY) {
* parentNode.appendChild(node);
* }
* }
* ```
*/
export interface I18nCreateOpCodes extends Array<number|string>, I18nDebug {}
/**
* See `I18nCreateOpCodes`
*/
export enum I18nCreateOpCode {
/**
* Number of bits to shift index so that it can be combined with the `APPEND_EAGERLY` and
* `COMMENT`.
*/
SHIFT = 2,
/**
* Should the node be appended to parent imedditatly after creation.
*/
APPEND_EAGERLY = 0b01,
/**
* If set the node should be comment (rather than a text) node.
*/
COMMENT = 0b10,
}
/**
* Array storing OpCode for dynamically creating `i18n` blocks.
@ -289,38 +354,18 @@ export interface I18nUpdateOpCodes extends Array<string|number|SanitizerFn|null>
* Store information for the i18n translation block.
*/
export interface TI18n {
/**
* Number of slots to allocate in expando.
*
* This is the max number of DOM elements which will be created by this i18n + ICU blocks. When
* the DOM elements are being created they are stored in the EXPANDO, so that update OpCodes can
* write into them.
*/
vars: number;
/**
* A set of OpCodes which will create the Text Nodes and ICU anchors for the translation blocks.
*
* NOTE: The ICU anchors are filled in with ICU Update OpCode.
*/
create: I18nMutateOpCodes;
create: I18nCreateOpCodes;
/**
* A set of OpCodes which will be executed on each change detection to determine if any changes to
* DOM are required.
*/
update: I18nUpdateOpCodes;
/**
* A list of ICUs in a translation block (or `null` if block has no ICUs).
*
* Example:
* Given: `<div i18n>You have {count, plural, ...} and {state, switch, ...}</div>`
* There would be 2 ICUs in this array.
* 1. `{count, plural, ...}`
* 2. `{state, switch, ...}`
*/
icus: TIcu[]|null;
}
/**
@ -338,53 +383,24 @@ export interface TIcu {
type: IcuType;
/**
* Number of slots to allocate in expando for each case.
*
* This is the max number of DOM elements which will be created by this i18n + ICU blocks. When
* the DOM elements are being created they are stored in the EXPANDO, so that update OpCodes can
* write into them.
* Index in `LView` where the anchor node is stored. `<!-- ICU 0:0 -->`
*/
vars: number[];
anchorIdx: number;
/**
* Currently selected ICU case pointer.
*
* `lView[currentCaseLViewIndex]` stores the currently selected case. This is needed to know how
* to clean up the current case when transitioning no the new case.
*
* If the value stored is:
* `null`: No current case selected.
* `<0`: A flag which means that the ICU just switched and that `icuUpdate` must be executed
* regardless of the `mask`. (After the execution the flag is cleared)
* `>=0` A currently selected case index.
*/
currentCaseLViewIndex: number;
/**
* An optional array of child/sub ICUs.
*
* In case of nested ICUs such as:
* ```
* {<EFBFBD>0<EFBFBD>, plural,
* =0 {zero}
* other {<EFBFBD>0<EFBFBD> {<EFBFBD>1<EFBFBD>, select,
* cat {cats}
* dog {dogs}
* other {animals}
* }!
* }
* }
* ```
* When the parent ICU is changing it must clean up child ICUs as well. For this reason it needs
* to know which child ICUs to run clean up for as well.
*
* In the above example this would be:
* ```ts
* [
* [], // `=0` has no sub ICUs
* [1], // `other` has one subICU at `1`st index.
* ]
* ```
*
* The reason why it is Array of Arrays is because first array represents the case, and second
* represents the child ICUs to clean up. There may be more than one child ICUs per case.
*/
childIcus: number[][];
/**
* A list of case values which the current ICU will try to match.
*
@ -395,11 +411,13 @@ export interface TIcu {
/**
* A set of OpCodes to apply in order to build up the DOM render tree for the ICU
*/
// FIXME(misko): Rename `I18nMutateOpCodes` to `I18nCreateOpCodes`.
create: I18nMutateOpCodes[];
/**
* A set of OpCodes to apply in order to destroy the DOM render tree for the ICU.
*/
// FIXME(misko): Rename `I18nMutateOpCodes` to `I18nRemoveOpCodes`.
remove: I18nMutateOpCodes[];
/**
@ -412,6 +430,9 @@ export interface TIcu {
// failure based on types.
export const unusedValueExportToPlacateAjd = 1;
/**
* Parsed ICU expression
*/
export interface IcuExpression {
type: IcuType;
mainBinding: number;
@ -419,33 +440,39 @@ export interface IcuExpression {
values: (string|IcuExpression)[][];
}
export interface IcuCase {
/**
* Number of slots to allocate in expando for this case.
*
* This is the max number of DOM elements which will be created by this i18n + ICU blocks. When
* the DOM elements are being created they are stored in the EXPANDO, so that update OpCodes can
* write into them.
*/
vars: number;
let _icuContainerIterate: (tIcuContainerNode: TIcuContainerNode, lView: LView) =>
(() => RNode | null);
/**
* An optional array of child/sub ICUs.
*/
childIcus: number[];
/**
* A set of OpCodes to apply in order to build up the DOM render tree for the ICU
*/
create: I18nMutateOpCodes;
/**
* A set of OpCodes to apply in order to destroy the DOM render tree for the ICU.
*/
remove: I18nMutateOpCodes;
/**
* A set of OpCodes to apply in order to update the DOM render tree for the ICU bindings.
*/
update: I18nUpdateOpCodes;
/**
* Iterator which provides ability to visit all of the `TIcuContainerNode` root `RNode`s.
*/
export function icuContainerIterate(tIcuContainerNode: TIcuContainerNode, lView: LView): () =>
RNode | null {
return _icuContainerIterate(tIcuContainerNode, lView);
}
/**
* Ensures that `IcuContainerVisitor`'s implementation is present.
*
* This function is invoked when i18n instruction comes across an ICU. The purpose is to allow the
* bundler to tree shake ICU logic and only load it if ICU instruction is executed.
*/
export function ensureIcuContainerVisitorLoaded(
loader: () => ((tIcuContainerNode: TIcuContainerNode, lView: LView) => (() => RNode | null))) {
if (_icuContainerIterate === undefined) {
// Do not inline this function. We want to keep `ensureIcuContainerVisitorLoaded` light, so it
// can be inlined into call-site.
_icuContainerIterate = loader();
}
}
/**
* Returns current ICU case.
*
* We store negative numbers for cases which have just been switched. This function removes that.
*/
export function getCurrentICUCaseIndex(tIcu: TIcu, lView: LView) {
const currentCase: number|null = lView[tIcu.currentCaseLViewIndex];
return currentCase === null ? currentCase : (currentCase < 0 ? ~currentCase : currentCase);
}

View File

@ -7,6 +7,7 @@
*/
import {KeyValueArray} from '../../util/array_utils';
import {TStylingRange} from '../interfaces/styling';
import {TIcu} from './i18n';
import {CssSelector} from './projection';
import {RNode} from './renderer';
import {LView, TView} from './view';
@ -16,9 +17,11 @@ import {LView, TView} from './view';
* TNodeType corresponds to the {@link TNode} `type` property.
*/
export const enum TNodeType {
// FIXME(misko): Add `Text` type so that it would be much easier to reason/debug about `TNode`s.
/**
* The TNode contains information about an {@link LContainer} for embedded views.
*/
// FIXME(misko): Verify that we still need a `Container`, at the very least update the text.
Container = 0,
/**
* The TNode contains information about an `<ng-content>` projection
@ -36,6 +39,20 @@ export const enum TNodeType {
* The TNode contains information about an ICU comment used in `i18n`.
*/
IcuContainer = 4,
/**
* Special node type representing a placeholder for future `TNode` at this location.
*
* I18n translation blocks are created before the element nodes which they contain. (I18n blocks
* can span over many elements.) Because i18n `TNode`s (representing text) are created first they
* often may need to point to element `TNode`s which are not yet created. In such a case we create
* a `Placeholder` `TNode`. This allows the i18n to structurally link the `TNode`s together
* without knowing any information about the future nodes which will be at that location.
*
* On `firstCreatePass` When element instruction executes it will try to create a `TNode` at that
* location. Seeing a `Placeholder` `TNode` already there tells the system that it should reuse
* existing `TNode` (rather than create a new one) and just update the missing information.
*/
Placeholder = 5,
}
/**
@ -47,7 +64,8 @@ export const TNodeTypeAsString = [
'Projection', // 1
'Element', // 2
'ElementContainer', // 3
'IcuContainer' // 4
'IcuContainer', // 4
'Placeholder', // 5
] as const;
@ -293,6 +311,59 @@ export interface TNode {
*/
index: number;
/**
* Insert before existing DOM node index.
*
* When DOM nodes are being inserted, normally they are being appended as they are created.
* Under i18n case, the translated text nodes are created ahead of time as part of the
* `ɵɵi18nStart` instruction which means that this `TNode` can't just be appended and instead
* needs to be inserted using `insertBeforeIndex` semantics.
*
* Additionally sometimes it is necessary to insert new text nodes as a child of this `TNode`. In
* such a case the value stores an array of text nodes to insert.
*
* Example:
* ```
* <div i18n>
* Hello <span>World</span>!
* </div>
* ```
* In the above example the `ɵɵi18nStart` instruction can create `Hello `, `World` and `!` text
* nodes. It can also insert `Hello ` and `!` text node as a child of `<div>`, but it can't
* insert `World` because the `<span>` node has not yet been created. In such a case the
* `<span>` `TNode` will have an array which will direct the `<span>` to not only insert
* itself in front of `!` but also to insert the `World` (created by `ɵɵi18nStart`) into `<span>`
* itself.
*
* Pseudo code:
* ```
* if (insertBeforeIndex === null) {
* // append as normal
* } else if (Array.isArray(insertBeforeIndex)) {
* // First insert current `TNode` at correct location
* const currentNode = lView[this.index];
* parentNode.insertBefore(currentNode, lView[this.insertBeforeIndex[0]]);
* // Now append all of the children
* for(let i=1; i<this.insertBeforeIndex; i++) {
* currentNode.appendChild(lView[this.insertBeforeIndex[i]]);
* }
* } else {
* parentNode.insertBefore(lView[this.index], lView[this.insertBeforeIndex])
* }
* ```
* - null: Append as normal using `parentNode.appendChild`
* - `number`: Append using
* `parentNode.insertBefore(lView[this.index], lView[this.insertBeforeIndex])`
*
* *Initialization*
*
* Because `ɵɵi18nStart` executes before nodes are created, on `TView.firstCreatePass` it is not
* possible for `ɵɵi18nStart` to set the `insertBeforeIndex` value as the corresponding `TNode`
* has not yet been created. For this reason the `ɵɵi18nStart` creates a `TNodeType.Placeholder`
* `TNode` at that location. See `TNodeType.Placeholder` for more information.
*/
insertBeforeIndex: InsertBeforeIndex;
/**
* The index of the closest injector in this node's LView.
*
@ -357,6 +428,8 @@ export interface TNode {
providerIndexes: TNodeProviderIndexes;
/** The tag name associated with this node. */
// FIXME(misko): rename to `value` and change the type to `any` so that
// subclasses of `TNode` can use it to link additional payload
tagName: string|null;
/**
@ -643,6 +716,11 @@ export interface TNode {
styleBindings: TStylingRange;
}
/**
* See `TNode.insertBeforeIndex`
*/
export type InsertBeforeIndex = null|number|number[];
/** Static data for an element */
export interface TElementNode extends TNode {
/** Index in the data[] array */
@ -715,10 +793,12 @@ export interface TElementContainerNode extends TNode {
export interface TIcuContainerNode extends TNode {
/** Index in the LView[] array. */
index: number;
child: TElementNode|TTextNode|null;
child: null;
parent: TElementNode|TElementContainerNode|null;
tViews: null;
projection: null;
// FIXME(misko): Refactor to enable the next line
// tagName: TIcu;
}
/** Static data for an LProjectionNode */

View File

@ -74,7 +74,7 @@ export interface ProceduralRenderer3 {
*/
destroyNode?: ((node: RNode) => void)|null;
appendChild(parent: RElement, newChild: RNode): void;
insertBefore(parent: RNode, newChild: RNode, refChild: RNode|null): void;
insertBefore(parent: RNode, newChild: RNode, refChild: RNode|null, isMove?: boolean): void;
removeChild(parent: RElement, oldChild: RNode, isHostElement?: boolean): void;
selectRootElement(selectorOrNode: string|any, preserveContent?: boolean): RElement;

View File

@ -14,7 +14,7 @@ import {Sanitizer} from '../../sanitization/sanitizer';
import {LContainer} from './container';
import {ComponentDef, ComponentTemplate, DirectiveDef, DirectiveDefList, HostBindingsFunction, PipeDef, PipeDefList, ViewQueriesFunction} from './definition';
import {I18nUpdateOpCodes, TI18n} from './i18n';
import {I18nUpdateOpCodes, TI18n, TIcu} from './i18n';
import {TConstants, TNode, TNodeTypeAsString} from './node';
import {PlayerHandler} from './player';
import {LQueries, TQueries} from './query';
@ -839,7 +839,7 @@ export type DestroyHookData = (HookEntry|HookData)[];
*/
export type TData =
(TNode|PipeDef<any>|DirectiveDef<any>|ComponentDef<any>|number|TStylingRange|TStylingKey|
Type<any>|InjectionToken<any>|TI18n|I18nUpdateOpCodes|null|string)[];
Type<any>|InjectionToken<any>|TI18n|I18nUpdateOpCodes|TIcu|null|string)[];
// Note: This hack is necessary so we don't erroneously get a circular dependency
// failure based on types.
@ -872,6 +872,11 @@ export interface LViewDebug {
indexWithinInitPhase: number,
};
/**
* Associated TView
*/
readonly tView: TView;
/**
* Parent view (or container)
*/
@ -894,6 +899,12 @@ export interface LViewDebug {
*/
readonly nodes: DebugNode[];
/**
* Template structure (no instance data).
* (Shows how TNodes are connected)
*/
readonly template: string;
/**
* HTML representation of the `LView`.
*
@ -921,11 +932,6 @@ export interface LViewDebug {
*/
readonly vars: LViewDebugRange;
/**
* Sub range of `LView` containing i18n (translated DOM elements).
*/
readonly i18n: LViewDebugRange;
/**
* Sub range of `LView` containing expando (used by DI).
*/

View File

@ -9,16 +9,17 @@
import {ViewEncapsulation} from '../metadata/view';
import {Renderer2} from '../render/api';
import {addToArray, removeFromArray} from '../util/array_utils';
import {assertDefined, assertDomNode, assertEqual, assertString} from '../util/assert';
import {assertDefined, assertDomNode, assertEqual, assertIndexInRange, assertString} from '../util/assert';
import {assertLContainer, assertLView, assertTNodeForLView} from './assert';
import {attachPatchData} from './context_discovery';
import {CONTAINER_HEADER_OFFSET, HAS_TRANSPLANTED_VIEWS, LContainer, MOVED_VIEWS, NATIVE, unusedValueExportToPlacateAjd as unused1} from './interfaces/container';
import {ComponentDef} from './interfaces/definition';
import {icuContainerIterate} from './interfaces/i18n';
import {NodeInjectorFactory} from './interfaces/injector';
import {TElementNode, TNode, TNodeFlags, TNodeType, TProjectionNode, unusedValueExportToPlacateAjd as unused2} from './interfaces/node';
import {TElementNode, TIcuContainerNode, TNode, TNodeFlags, TNodeType, TProjectionNode, unusedValueExportToPlacateAjd as unused2} from './interfaces/node';
import {unusedValueExportToPlacateAjd as unused3} from './interfaces/projection';
import {isProceduralRenderer, ProceduralRenderer3, RElement, Renderer3, RNode, RText, unusedValueExportToPlacateAjd as unused4} from './interfaces/renderer';
import {isProceduralRenderer, ProceduralRenderer3, RComment, RElement, Renderer3, RNode, RText, unusedValueExportToPlacateAjd as unused4} from './interfaces/renderer';
import {isLContainer, isLView} from './interfaces/type_checks';
import {CHILD_HEAD, CLEANUP, DECLARATION_COMPONENT_VIEW, DECLARATION_LCONTAINER, DestroyHookData, FLAGS, HookData, HookFn, HOST, LView, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, T_HOST, TVIEW, TView, TViewType, unusedValueExportToPlacateAjd as unused5} from './interfaces/view';
import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert';
@ -77,10 +78,10 @@ function applyToElementOrContainer(
if (beforeNode == null) {
nativeAppendChild(renderer, parent, rNode);
} else {
nativeInsertBefore(renderer, parent, rNode, beforeNode || null);
nativeInsertBefore(renderer, parent, rNode, beforeNode || null, true);
}
} else if (action === WalkTNodeTreeAction.Insert && parent !== null) {
nativeInsertBefore(renderer, parent, rNode, beforeNode || null);
nativeInsertBefore(renderer, parent, rNode, beforeNode || null, true);
} else if (action === WalkTNodeTreeAction.Detach) {
nativeRemoveNode(renderer, rNode, isComponent);
} else if (action === WalkTNodeTreeAction.Destroy) {
@ -93,13 +94,44 @@ function applyToElementOrContainer(
}
}
export function createTextNode(value: string, renderer: Renderer3): RText {
export function createTextNode(renderer: Renderer3, value: string): RText {
ngDevMode && ngDevMode.rendererCreateTextNode++;
ngDevMode && ngDevMode.rendererSetText++;
return isProceduralRenderer(renderer) ? renderer.createText(value) :
renderer.createTextNode(value);
}
export function updateTextNode(renderer: Renderer3, rNode: RText, value: string): void {
ngDevMode && ngDevMode.rendererSetText++;
isProceduralRenderer(renderer) ? renderer.setValue(rNode, value) : rNode.textContent = value;
}
export function createCommentNode(renderer: Renderer3, value: string): RComment {
ngDevMode && ngDevMode.rendererCreateComment++;
// isProceduralRenderer check is not needed because both `Renderer2` and `Renderer3` have the same
// method name.
return renderer.createComment(value);
}
/**
* Creates a native element from a tag name, using a renderer.
* @param renderer A renderer to use
* @param name the tag name
* @param namespace Optional namespace for element.
* @returns the element created
*/
export function createElementNode(
renderer: Renderer3, name: string, namespace: string|null): RElement {
ngDevMode && ngDevMode.rendererCreateElement++;
if (isProceduralRenderer(renderer)) {
return renderer.createElement(name, namespace);
} else {
return namespace === null ? renderer.createElement(name) :
renderer.createElementNS(namespace, name);
}
}
/**
* Removes all DOM elements associated with a view.
*
@ -479,11 +511,34 @@ function executeOnDestroys(tView: TView, lView: LView): void {
* parent container, which itself is disconnected. For example the parent container is part
* of a View which has not be inserted or is made for projection but has not been inserted
* into destination.
*
* @param tView: Current `TView`.
* @param tNode: `TNode` for which we wish to retrieve render parent.
* @param lView: Current `LView`.
*/
function getRenderParent(tView: TView, tNode: TNode, currentView: LView): RElement|null {
export function getParentRElement(tView: TView, tNode: TNode, lView: LView): RElement|null {
return getClosestRElement(tView, tNode.parent, lView);
}
/**
* Get closest `RElement` or `null` if it can't be found.
*
* If `TNode` is `TNodeType.Element` => return `RElement` at `LView[tNode.index]` location.
* If `TNode` is `TNodeType.ElementContainer|IcuContain` => return the parent (recursively).
* If `TNode` is `null` then return host `RElement`:
* - return `null` if projection
* - return `null` if parent container is disconnected (we have no parent.)
*
* @param tView: Current `TView`.
* @param tNode: `TNode` for which we wish to retrieve `RElement` (or `null` if host element is
* needed).
* @param lView: Current `LView`.
* @returns `null` if the `RElement` can't be determined at this time (no parent / projection)
*/
export function getClosestRElement(tView: TView, tNode: TNode|null, lView: LView): RElement|null {
let parentTNode: TNode|null = tNode;
// Skip over element and ICU containers as those are represented by a comment node and
// can't be used as a render parent.
let parentTNode = tNode.parent;
while (parentTNode != null &&
(parentTNode.type === TNodeType.ElementContainer ||
parentTNode.type === TNodeType.IcuContainer)) {
@ -496,21 +551,14 @@ function getRenderParent(tView: TView, tNode: TNode, currentView: LView): REleme
if (parentTNode === null) {
// We are inserting a root element of the component view into the component host element and
// it should always be eager.
return currentView[HOST];
return lView[HOST];
} else {
const isIcuCase = tNode && tNode.type === TNodeType.IcuContainer;
// If the parent of this node is an ICU container, then it is represented by comment node and we
// need to use it as an anchor. If it is projected then it's direct parent node is the renderer.
if (isIcuCase && tNode.flags & TNodeFlags.isProjected) {
return getNativeByTNode(tNode, currentView).parentNode as RElement;
}
ngDevMode && assertNodeType(parentTNode, TNodeType.Element);
// ngDevMode && assertTNodeType(parentTNode, TNodeType.AnyRNode | TNodeType.Container);
if (parentTNode.flags & TNodeFlags.isComponentHost) {
ngDevMode && assertTNodeForLView(parentTNode, lView);
const tData = tView.data;
const tNode = tData[parentTNode.index] as TNode;
const encapsulation = (tData[tNode.directiveStart] as ComponentDef<any>).encapsulation;
// We've got a parent which is an element in the current view. We just need to verify if the
// parent element is not a component. Component's content nodes are not inserted immediately
// because they will be projected, and so doing insert at this point would be wasteful.
@ -523,7 +571,7 @@ function getRenderParent(tView: TView, tNode: TNode, currentView: LView): REleme
}
}
return getNativeByTNode(parentTNode, currentView) as RElement;
return getNativeByTNode(parentTNode, lView) as RElement;
}
}
@ -533,12 +581,13 @@ function getRenderParent(tView: TView, tNode: TNode, currentView: LView): REleme
* actual renderer being used.
*/
export function nativeInsertBefore(
renderer: Renderer3, parent: RElement, child: RNode, beforeNode: RNode|null): void {
renderer: Renderer3, parent: RElement, child: RNode, beforeNode: RNode|null,
isMove: boolean): void {
ngDevMode && ngDevMode.rendererInsertBefore++;
if (isProceduralRenderer(renderer)) {
renderer.insertBefore(parent, child, beforeNode);
renderer.insertBefore(parent, child, beforeNode, isMove);
} else {
parent.insertBefore(child, beforeNode, true);
parent.insertBefore(child, beforeNode, isMove);
}
}
@ -553,9 +602,9 @@ function nativeAppendChild(renderer: Renderer3, parent: RElement, child: RNode):
}
function nativeAppendOrInsertBefore(
renderer: Renderer3, parent: RElement, child: RNode, beforeNode: RNode|null) {
renderer: Renderer3, parent: RElement, child: RNode, beforeNode: RNode|null, isMove: boolean) {
if (beforeNode !== null) {
nativeInsertBefore(renderer, parent, child, beforeNode);
nativeInsertBefore(renderer, parent, child, beforeNode, isMove);
} else {
nativeAppendChild(renderer, parent, child);
}
@ -586,15 +635,28 @@ export function nativeNextSibling(renderer: Renderer3, node: RNode): RNode|null
}
/**
* Finds a native "anchor" node for cases where we can't append a native child directly
* (`appendChild`) and need to use a reference (anchor) node for the `insertBefore` operation.
* @param parentTNode
* @param lView
* Find a node in front of which `currentTNode` should be inserted.
*
* This method determines the `RNode` in front of which we should insert the `currentRNode`. This
* takes `TNode.insertBeforeIndex` into account.
*
* @param parentTNode parent `TNode`
* @param currentTNode current `TNode` (The node which we would like to insert into the DOM)
* @param lView current `LView`
*/
function getNativeAnchorNode(parentTNode: TNode, lView: LView): RNode|null {
if (parentTNode.type === TNodeType.ElementContainer ||
parentTNode.type === TNodeType.IcuContainer) {
return getNativeByTNode(parentTNode, lView);
function getInsertInFrontOfRNode(parentTNode: TNode, currentTNode: TNode, lView: LView): RNode|
null {
const tNodeInsertBeforeIndex = currentTNode.insertBeforeIndex;
const insertBeforeIndex =
Array.isArray(tNodeInsertBeforeIndex) ? tNodeInsertBeforeIndex[0] : tNodeInsertBeforeIndex;
if (insertBeforeIndex === null) {
if (parentTNode.type === TNodeType.ElementContainer ||
parentTNode.type === TNodeType.IcuContainer) {
return getNativeByTNode(parentTNode, lView);
}
} else {
ngDevMode && assertIndexInRange(lView, insertBeforeIndex);
return unwrapRNode(lView[insertBeforeIndex]);
}
return null;
}
@ -602,27 +664,69 @@ function getNativeAnchorNode(parentTNode: TNode, lView: LView): RNode|null {
/**
* Appends the `child` native node (or a collection of nodes) to the `parent`.
*
* The element insertion might be delayed {@link canInsertNativeNode}.
*
* @param tView The `TView' to be appended
* @param lView The current LView
* @param childEl The native child (or children) that should be appended
* @param childRNode The native child (or children) that should be appended
* @param childTNode The TNode of the child element
* @returns Whether or not the child was appended
*/
export function appendChild(
tView: TView, lView: LView, childEl: RNode|RNode[], childTNode: TNode): void {
const renderParent = getRenderParent(tView, childTNode, lView);
if (renderParent != null) {
const renderer = lView[RENDERER];
const parentTNode: TNode = childTNode.parent || lView[T_HOST]!;
const anchorNode = getNativeAnchorNode(parentTNode, lView);
if (Array.isArray(childEl)) {
for (let i = 0; i < childEl.length; i++) {
nativeAppendOrInsertBefore(renderer, renderParent, childEl[i], anchorNode);
tView: TView, lView: LView, childRNode: RNode|RNode[], childTNode: TNode): void {
const parentRNode = getParentRElement(tView, childTNode, lView);
const renderer = lView[RENDERER];
const parentTNode: TNode = childTNode.parent || lView[T_HOST]!;
const anchorNode = getInsertInFrontOfRNode(parentTNode, childTNode, lView);
if (parentRNode != null) {
if (Array.isArray(childRNode)) {
for (let i = 0; i < childRNode.length; i++) {
nativeAppendOrInsertBefore(renderer, parentRNode, childRNode[i], anchorNode, false);
}
} else {
nativeAppendOrInsertBefore(renderer, renderParent, childEl, anchorNode);
nativeAppendOrInsertBefore(renderer, parentRNode, childRNode, anchorNode, false);
}
}
const tNodeInsertBeforeIndex = childTNode.insertBeforeIndex;
if (Array.isArray(tNodeInsertBeforeIndex) &&
(childTNode.flags & TNodeFlags.isComponentHost) === 0) {
// An array indicates that there are i18n nodes that need to be added as children of this
// `rChildNode`. These i18n nodes were created before this `rChildNode` was available and so
// only now can be added. The first element of the array is the normal index where we should
// insert the `rChildNode`. Additional elements are the extra nodes to be added as children of
// `rChildNode`.
processI18nText(renderer, childTNode, lView, childRNode, parentRNode, tNodeInsertBeforeIndex);
}
}
/**
* Process `TNode.insertBeforeIndex` by adding i18n text nodes.
*
* See `TNode.insertBeforeIndex`
*
* @param renderer
* @param childTNode
* @param lView
* @param childRNode
* @param parentRElement
* @param i18nChildren
*/
function processI18nText(
renderer: Renderer3, childTNode: TNode, lView: LView, childRNode: RNode|RNode[],
parentRElement: RElement|null, i18nChildren: number[]): void {
ngDevMode && assertDomNode(childRNode);
const isProcedural = isProceduralRenderer(renderer);
let i18nParent: RElement|null = childRNode as RElement;
let anchorRNode: RNode|null = null;
if (childTNode.type !== TNodeType.Element) {
anchorRNode = i18nParent;
i18nParent = parentRElement;
}
const isViewRoot = childTNode.parent === null;
if (i18nParent !== null) {
for (let i = 1; i < i18nChildren.length; i++) {
// No need to `unwrapRNode` because all of the indexes point to i18n text nodes.
// see `assertDomNode` below.
const i18nChild = lView[i18nChildren[i]];
nativeInsertBefore(renderer, i18nParent, i18nChild, anchorRNode, false);
}
}
}
@ -644,7 +748,7 @@ function getFirstNativeNode(lView: LView, tNode: TNode|null): RNode|null {
return getNativeByTNode(tNode, lView);
} else if (tNodeType === TNodeType.Container) {
return getBeforeNodeForView(-1, lView[tNode.index]);
} else if (tNodeType === TNodeType.ElementContainer || tNodeType === TNodeType.IcuContainer) {
} else if (tNodeType === TNodeType.ElementContainer) {
const elIcuContainerChild = tNode.child;
if (elIcuContainerChild !== null) {
return getFirstNativeNode(lView, elIcuContainerChild);
@ -656,6 +760,11 @@ function getFirstNativeNode(lView: LView, tNode: TNode|null): RNode|null {
return unwrapRNode(rNodeOrLContainer);
}
}
} else if (tNodeType === TNodeType.IcuContainer) {
let nextRNode = icuContainerIterate(tNode as TIcuContainerNode, lView);
let rNode: RNode|null = nextRNode();
// If the ICU container has no nodes, than we use the ICU anchor as the node.
return rNode || unwrapRNode(lView[tNode.index]);
} else {
const componentView = lView[DECLARATION_COMPONENT_VIEW];
const componentHost = componentView[T_HOST] as TElementNode;
@ -698,6 +807,7 @@ export function getBeforeNodeForView(viewIndexInContainer: number, lContainer: L
* @param isHostElement A flag indicating if a node to be removed is a host of a component.
*/
export function nativeRemoveNode(renderer: Renderer3, rNode: RNode, isHostElement?: boolean): void {
ngDevMode && ngDevMode.rendererRemoveNode++;
const nativeParent = nativeParentNode(renderer, rNode);
if (nativeParent) {
nativeRemoveChild(renderer, nativeParent, rNode, isHostElement);
@ -711,7 +821,7 @@ export function nativeRemoveNode(renderer: Renderer3, rNode: RNode, isHostElemen
*/
function applyNodes(
renderer: Renderer3, action: WalkTNodeTreeAction, tNode: TNode|null, lView: LView,
renderParent: RElement|null, beforeNode: RNode|null, isProjection: boolean) {
parentRElement: RElement|null, beforeNode: RNode|null, isProjection: boolean) {
while (tNode != null) {
ngDevMode && assertTNodeForLView(tNode, lView);
ngDevMode && assertNodeOfPossibleTypes(tNode, [
@ -727,15 +837,22 @@ function applyNodes(
}
}
if ((tNode.flags & TNodeFlags.isDetached) !== TNodeFlags.isDetached) {
if (tNodeType === TNodeType.ElementContainer || tNodeType === TNodeType.IcuContainer) {
applyNodes(renderer, action, tNode.child, lView, renderParent, beforeNode, false);
applyToElementOrContainer(action, renderer, renderParent, rawSlotValue, beforeNode);
if (tNodeType === TNodeType.ElementContainer) {
applyNodes(renderer, action, tNode.child, lView, parentRElement, beforeNode, false);
applyToElementOrContainer(action, renderer, parentRElement, rawSlotValue, beforeNode);
} else if (tNodeType === TNodeType.IcuContainer) {
const nextRNode = icuContainerIterate(tNode as TIcuContainerNode, lView);
let rNode: RNode|null;
while (rNode = nextRNode()) {
applyToElementOrContainer(action, renderer, parentRElement, rNode, beforeNode);
}
applyToElementOrContainer(action, renderer, parentRElement, rawSlotValue, beforeNode);
} else if (tNodeType === TNodeType.Projection) {
applyProjectionRecursive(
renderer, action, lView, tNode as TProjectionNode, renderParent, beforeNode);
renderer, action, lView, tNode as TProjectionNode, parentRElement, beforeNode);
} else {
ngDevMode && assertNodeOfPossibleTypes(tNode, [TNodeType.Element, TNodeType.Container]);
applyToElementOrContainer(action, renderer, renderParent, rawSlotValue, beforeNode);
applyToElementOrContainer(action, renderer, parentRElement, rawSlotValue, beforeNode);
}
}
tNode = isProjection ? tNode.projectionNext : tNode.next;
@ -763,19 +880,19 @@ function applyNodes(
* @param lView The LView which needs to be inserted, detached, destroyed.
* @param renderer Renderer to use
* @param action action to perform (insert, detach, destroy)
* @param renderParent parent DOM element for insertion (Removal does not need it).
* @param parentRElement parent DOM element for insertion (Removal does not need it).
* @param beforeNode Before which node the insertions should happen.
*/
function applyView(
tView: TView, lView: LView, renderer: Renderer3, action: WalkTNodeTreeAction.Destroy,
renderParent: null, beforeNode: null): void;
parentRElement: null, beforeNode: null): void;
function applyView(
tView: TView, lView: LView, renderer: Renderer3, action: WalkTNodeTreeAction,
renderParent: RElement|null, beforeNode: RNode|null): void;
parentRElement: RElement|null, beforeNode: RNode|null): void;
function applyView(
tView: TView, lView: LView, renderer: Renderer3, action: WalkTNodeTreeAction,
renderParent: RElement|null, beforeNode: RNode|null): void {
applyNodes(renderer, action, tView.firstChild, lView, renderParent, beforeNode, false);
parentRElement: RElement|null, beforeNode: RNode|null): void {
applyNodes(renderer, action, tView.firstChild, lView, parentRElement, beforeNode, false);
}
/**
@ -790,11 +907,11 @@ function applyView(
*/
export function applyProjection(tView: TView, lView: LView, tProjectionNode: TProjectionNode) {
const renderer = lView[RENDERER];
const renderParent = getRenderParent(tView, tProjectionNode, lView);
const parentRNode = getParentRElement(tView, tProjectionNode, lView);
const parentTNode = tProjectionNode.parent || lView[T_HOST]!;
let beforeNode = getNativeAnchorNode(parentTNode, lView);
let beforeNode = getInsertInFrontOfRNode(parentTNode, tProjectionNode, lView);
applyProjectionRecursive(
renderer, WalkTNodeTreeAction.Create, lView, tProjectionNode, renderParent, beforeNode);
renderer, WalkTNodeTreeAction.Create, lView, tProjectionNode, parentRNode, beforeNode);
}
/**
@ -808,12 +925,12 @@ export function applyProjection(tView: TView, lView: LView, tProjectionNode: TPr
* @param action action to perform (insert, detach, destroy)
* @param lView The LView which needs to be inserted, detached, destroyed.
* @param tProjectionNode node to project
* @param renderParent parent DOM element for insertion/removal.
* @param parentRElement parent DOM element for insertion/removal.
* @param beforeNode Before which node the insertions should happen.
*/
function applyProjectionRecursive(
renderer: Renderer3, action: WalkTNodeTreeAction, lView: LView,
tProjectionNode: TProjectionNode, renderParent: RElement|null, beforeNode: RNode|null) {
tProjectionNode: TProjectionNode, parentRElement: RElement|null, beforeNode: RNode|null) {
const componentLView = lView[DECLARATION_COMPONENT_VIEW];
const componentNode = componentLView[T_HOST] as TElementNode;
ngDevMode &&
@ -827,13 +944,13 @@ function applyProjectionRecursive(
// This should be refactored and cleaned up.
for (let i = 0; i < nodeToProjectOrRNodes.length; i++) {
const rNode = nodeToProjectOrRNodes[i];
applyToElementOrContainer(action, renderer, renderParent, rNode, beforeNode);
applyToElementOrContainer(action, renderer, parentRElement, rNode, beforeNode);
}
} else {
let nodeToProject: TNode|null = nodeToProjectOrRNodes;
const projectedComponentLView = componentLView[PARENT] as LView;
applyNodes(
renderer, action, nodeToProject, projectedComponentLView, renderParent, beforeNode, true);
renderer, action, nodeToProject, projectedComponentLView, parentRElement, beforeNode, true);
}
}
@ -848,31 +965,31 @@ function applyProjectionRecursive(
* @param renderer Renderer to use
* @param action action to perform (insert, detach, destroy)
* @param lContainer The LContainer which needs to be inserted, detached, destroyed.
* @param renderParent parent DOM element for insertion/removal.
* @param parentRElement parent DOM element for insertion/removal.
* @param beforeNode Before which node the insertions should happen.
*/
function applyContainer(
renderer: Renderer3, action: WalkTNodeTreeAction, lContainer: LContainer,
renderParent: RElement|null, beforeNode: RNode|null|undefined) {
parentRElement: RElement|null, beforeNode: RNode|null|undefined) {
ngDevMode && assertLContainer(lContainer);
const anchor = lContainer[NATIVE]; // LContainer has its own before node.
const native = unwrapRNode(lContainer);
// An LContainer can be created dynamically on any node by injecting ViewContainerRef.
// Asking for a ViewContainerRef on an element will result in a creation of a separate anchor node
// (comment in the DOM) that will be different from the LContainer's host node. In this particular
// case we need to execute action on 2 nodes:
// Asking for a ViewContainerRef on an element will result in a creation of a separate anchor
// node (comment in the DOM) that will be different from the LContainer's host node. In this
// particular case we need to execute action on 2 nodes:
// - container's host node (this is done in the executeActionOnElementOrContainer)
// - container's host node (this is done here)
if (anchor !== native) {
// This is very strange to me (Misko). I would expect that the native is same as anchor. I don't
// see a reason why they should be different, but they are.
// This is very strange to me (Misko). I would expect that the native is same as anchor. I
// don't see a reason why they should be different, but they are.
//
// If they are we need to process the second anchor as well.
applyToElementOrContainer(action, renderer, renderParent, anchor, beforeNode);
applyToElementOrContainer(action, renderer, parentRElement, anchor, beforeNode);
}
for (let i = CONTAINER_HEADER_OFFSET; i < lContainer.length; i++) {
const lView = lContainer[i] as LView;
applyView(lView[TVIEW], lView, renderer, action, renderParent, anchor);
applyView(lView[TVIEW], lView, renderer, action, parentRElement, anchor);
}
}
@ -908,8 +1025,8 @@ export function applyStyling(
}
}
} else {
// TODO(misko): Can't import RendererStyleFlags2.DashCase as it causes imports to be resolved in
// different order which causes failures. Using direct constant as workaround for now.
// TODO(misko): Can't import RendererStyleFlags2.DashCase as it causes imports to be resolved
// in different order which causes failures. Using direct constant as workaround for now.
const flags = prop.indexOf('-') == -1 ? undefined : 2 /* RendererStyleFlags2.DashCase */;
if (value == null /** || value === undefined */) {
ngDevMode && ngDevMode.rendererRemoveStyle++;

View File

@ -6,10 +6,10 @@
* found in the LICENSE file at https://angular.io/license
*/
import {assertDefined, assertEqual} from '../util/assert';
import {assertDefined, assertEqual, assertNotEqual} from '../util/assert';
import {assertLViewOrUndefined, assertTNodeForTView} from './assert';
import {DirectiveDef} from './interfaces/definition';
import {TNode} from './interfaces/node';
import {TNode, TNodeType} from './interfaces/node';
import {CONTEXT, DECLARATION_VIEW, LView, OpaqueViewState, TData, TVIEW, TView} from './interfaces/view';
import {MATH_ML_NAMESPACE, SVG_NAMESPACE} from './namespaces';
import {getTNode} from './util/view_utils';
@ -116,6 +116,22 @@ interface LFrame {
* `LView[currentDirectiveIndex]` is directive instance.
*/
currentDirectiveIndex: number;
/**
* Are we currently in i18n block as denoted by `ɵɵelementStart` and `ɵɵelementEnd`.
*
* This information is needed because while we are in i18n block all elements must be pre-declared
* in the translation. (i.e. `Hello <20>#2<>World<6C>/#2<>!` pre-declares element at `<EFBFBD>#2<>` location.)
* This allocates `TNodeType.Placeholder` element at location `2`. If translator removes `<EFBFBD>#2<>`
* from translation than the runtime must also ensure tha element at `2` does not get inserted
* into the DOM. The translation does not carry information about deleted elements. Therefor the
* only way to know that an element is deleted is that it was not pre-declared in the translation.
*
* This flag works by ensuring that elements which are created without pre-declaration
* (`TNodeType.Placeholder`) are not inserted into the DOM render tree. (It does mean that the
* element still gets instantiated along with all of its behavior [directives])
*/
inI18n: boolean;
}
/**
@ -166,12 +182,21 @@ interface InstructionState {
isInCheckNoChangesMode: boolean;
}
export const instructionState: InstructionState = {
const instructionState: InstructionState = {
lFrame: createLFrame(null),
bindingsEnabled: true,
isInCheckNoChangesMode: false,
};
/**
* Returns true if the instruction state stack is empty.
*
* Intended to be called from tests only (tree shaken otherwise).
*/
export function specOnlyIsInstructionStateEmpty(): boolean {
return instructionState.lFrame.parent === null;
}
export function getElementDepthCount() {
return instructionState.lFrame.elementDepthCount;
@ -265,14 +290,30 @@ export function ɵɵrestoreView(viewToRestore: OpaqueViewState) {
instructionState.lFrame.contextLView = viewToRestore as any as LView;
}
export function getCurrentTNode(): TNode|null {
let currentTNode = getCurrentTNodePlaceholderOk();
while (currentTNode !== null && currentTNode.type === TNodeType.Placeholder) {
currentTNode = currentTNode.parent;
}
return currentTNode;
}
export function getCurrentTNodePlaceholderOk(): TNode|null {
return instructionState.lFrame.currentTNode;
}
export function setCurrentTNode(tNode: TNode, isParent: boolean) {
ngDevMode && assertTNodeForTView(tNode, instructionState.lFrame.tView);
instructionState.lFrame.currentTNode = tNode;
instructionState.lFrame.isParent = isParent;
export function getCurrentParentTNode(): TNode|null {
const lFrame = instructionState.lFrame;
const currentTNode = lFrame.currentTNode;
return lFrame.isParent ? currentTNode : currentTNode!.parent;
}
export function setCurrentTNode(tNode: TNode|null, isParent: boolean) {
ngDevMode && tNode && assertTNodeForTView(tNode, instructionState.lFrame.tView);
const lFrame = instructionState.lFrame;
lFrame.currentTNode = tNode;
lFrame.isParent = isParent;
}
export function isCurrentTNodeParent(): boolean {
@ -328,6 +369,14 @@ export function incrementBindingIndex(count: number): number {
return index;
}
export function isInI18nBlock() {
return instructionState.lFrame.inI18n;
}
export function setInI18nBlock(isInI18nBlock: boolean): void {
instructionState.lFrame.inI18n = isInI18nBlock;
}
/**
* Set a new binding root index so that host template functions can execute.
*
@ -429,6 +478,7 @@ export function enterView(newView: LView): void {
newLFrame.tView = tView;
newLFrame.contextLView = newView!;
newLFrame.bindingIndex = tView.bindingStartIndex;
newLFrame.inI18n = false;
}
/**
@ -443,20 +493,21 @@ function allocLFrame() {
function createLFrame(parent: LFrame|null): LFrame {
const lFrame: LFrame = {
currentTNode: null, //
isParent: true, //
lView: null!, //
tView: null!, //
selectedIndex: 0, //
contextLView: null!, //
elementDepthCount: 0, //
currentNamespace: null, //
currentDirectiveIndex: -1, //
bindingRootIndex: -1, //
bindingIndex: -1, //
currentQueryIndex: 0, //
parent: parent!, //
child: null, //
currentTNode: null,
isParent: true,
lView: null!,
tView: null!,
selectedIndex: 0,
contextLView: null!,
elementDepthCount: 0,
currentNamespace: null,
currentDirectiveIndex: -1,
bindingRootIndex: -1,
bindingIndex: -1,
currentQueryIndex: 0,
parent: parent!,
child: null,
inI18n: false,
};
parent !== null && (parent.child = lFrame); // link the new LFrame for reuse.
return lFrame;

View File

@ -7,10 +7,10 @@
*/
import {assertDefined, assertDomNode, assertGreaterThan, assertIndexInRange, assertLessThan} from '../../util/assert';
import {assertTNodeForLView} from '../assert';
import {assertTNode, assertTNodeForLView} from '../assert';
import {LContainer, TYPE} from '../interfaces/container';
import {LContext, MONKEY_PATCH_KEY_NAME} from '../interfaces/context';
import {TConstants, TNode, TNodeType} from '../interfaces/node';
import {TConstants, TNode} from '../interfaces/node';
import {isProceduralRenderer, RNode} from '../interfaces/renderer';
import {isLContainer, isLView} from '../interfaces/type_checks';
import {FLAGS, HEADER_OFFSET, HOST, LView, LViewFlags, PARENT, PREORDER_HOOK_FLAGS, RENDERER, TData, TRANSPLANTED_VIEWS_TO_REFRESH, TView} from '../interfaces/view';
@ -117,10 +117,13 @@ export function getNativeByTNodeOrNull(tNode: TNode|null, lView: LView): RNode|n
}
// fixme(misko): The return Type should be `TNode|null`
export function getTNode(tView: TView, index: number): TNode {
ngDevMode && assertGreaterThan(index, -1, 'wrong index for TNode');
ngDevMode && assertLessThan(index, tView.data.length, 'wrong index for TNode');
return tView.data[index + HEADER_OFFSET] as TNode;
ngDevMode && assertLessThan(index, tView.data.length - HEADER_OFFSET, 'wrong index for TNode');
const tNode = tView.data[index + HEADER_OFFSET] as TNode;
ngDevMode && tNode !== null && assertTNode(tNode);
return tNode;
}
/** Retrieves a value from any `LView` or `TData`. */

View File

@ -20,7 +20,7 @@ import {assertDefined, assertEqual, assertGreaterThan, assertLessThan} from '../
import {assertLContainer, assertNodeInjector} from './assert';
import {getParentInjectorLocation, NodeInjector} from './di';
import {addToViewTree, createLContainer, createLView, renderView} from './instructions/shared';
import {addToViewTree, createLContainer, createLView, createTNode, renderView} from './instructions/shared';
import {CONTAINER_HEADER_OFFSET, LContainer, NATIVE, VIEW_REFS} from './interfaces/container';
import {NodeInjectorOffset} from './interfaces/injector';
import {TContainerNode, TDirectiveHostNode, TElementContainerNode, TElementNode, TNode, TNodeType} from './interfaces/node';
@ -287,9 +287,9 @@ export function createContainerRef(
// Physical operation of adding the DOM nodes.
const beforeNode = getBeforeNodeForView(adjustedIdx, lContainer);
const renderer = lView[RENDERER];
const renderParent = nativeParentNode(renderer, lContainer[NATIVE] as RElement | RComment);
if (renderParent !== null) {
addViewToContainer(tView, lContainer[T_HOST], renderer, lView, renderParent, beforeNode);
const parentRNode = nativeParentNode(renderer, lContainer[NATIVE] as RElement | RComment);
if (parentRNode !== null) {
addViewToContainer(tView, lContainer[T_HOST], renderer, lView, parentRNode, beforeNode);
}
(viewRef as ViewRef<any>).attachToViewContainerRef(this);
@ -388,9 +388,14 @@ export function createContainerRef(
const hostNative = getNativeByTNode(hostTNode, hostView)!;
const parentOfHostNative = nativeParentNode(renderer, hostNative);
nativeInsertBefore(
renderer, parentOfHostNative!, commentNode, nativeNextSibling(renderer, hostNative));
renderer, parentOfHostNative!, commentNode, nativeNextSibling(renderer, hostNative),
false);
} else {
appendChild(hostView[TVIEW], hostView, commentNode, hostTNode);
// The TNode created here is bogus, in that it is not added to the TView. It is only created
// to allow us to create a dynamic Comment node.
const commentTNode = createTNode(
hostView[TVIEW], hostTNode.parent, TNodeType.Container, hostTNode.type, null, null);
appendChild(hostView[TVIEW], hostView, commentNode, commentTNode);
}
}

View File

@ -11,11 +11,14 @@ import {ChangeDetectorRef as viewEngine_ChangeDetectorRef} from '../change_detec
import {ViewContainerRef as viewEngine_ViewContainerRef} from '../linker/view_container_ref';
import {EmbeddedViewRef as viewEngine_EmbeddedViewRef, InternalViewRef as viewEngine_InternalViewRef} from '../linker/view_ref';
import {assertDefined} from '../util/assert';
import {checkNoChangesInRootView, checkNoChangesInternal, detectChangesInRootView, detectChangesInternal, markViewDirty, storeCleanupWithContext} from './instructions/shared';
import {CONTAINER_HEADER_OFFSET} from './interfaces/container';
import {TElementNode, TNode, TNodeType} from './interfaces/node';
import {icuContainerIterate} from './interfaces/i18n';
import {TElementNode, TIcuContainerNode, TNode, TNodeType} from './interfaces/node';
import {RNode} from './interfaces/renderer';
import {isLContainer} from './interfaces/type_checks';
import {CONTEXT, DECLARATION_COMPONENT_VIEW, FLAGS, HOST, LView, LViewFlags, T_HOST, TVIEW, TView} from './interfaces/view';
import {CONTEXT, DECLARATION_COMPONENT_VIEW, FLAGS, LView, LViewFlags, T_HOST, TVIEW, TView} from './interfaces/view';
import {assertNodeOfPossibleTypes} from './node_assert';
import {destroyLView, renderDetachView} from './node_manipulation';
import {getLViewParent} from './util/view_traversal_utils';
@ -346,8 +349,14 @@ function collectNativeNodes(
}
const tNodeType = tNode.type;
if (tNodeType === TNodeType.ElementContainer || tNodeType === TNodeType.IcuContainer) {
if (tNodeType === TNodeType.ElementContainer) {
collectNativeNodes(tView, lView, tNode.child, result);
} else if (tNodeType === TNodeType.IcuContainer) {
const nextRNode = icuContainerIterate(tNode as TIcuContainerNode, lView);
let rNode: RNode|null;
while (rNode = nextRNode()) {
result.push(rNode);
}
} else if (tNodeType === TNodeType.Projection) {
const componentView = lView[DECLARATION_COMPONENT_VIEW];
const componentHost = componentView[T_HOST] as TElementNode;

View File

@ -102,18 +102,25 @@ export function throwError(msg: string, actual?: any, expected?: any, comparison
export function assertDomNode(node: any): asserts node is Node {
// If we're in a worker, `Node` will not be defined.
assertEqual(
(typeof Node !== 'undefined' && node instanceof Node) ||
(typeof node === 'object' && node != null &&
node.constructor.name === 'WebWorkerRenderNode'),
true, `The provided value must be an instance of a DOM Node but got ${stringify(node)}`);
if (!(typeof Node !== 'undefined' && node instanceof Node) &&
!(typeof node === 'object' && node != null &&
node.constructor.name === 'WebWorkerRenderNode')) {
throwError(`The provided value must be an instance of a DOM Node but got ${stringify(node)}`);
}
}
export function assertIndexInRange(arr: any[], index: number) {
assertDefined(arr, 'Array must be defined.');
const maxLen = arr.length;
if (index < 0 || index > maxLen) {
if (index < 0 || index >= maxLen) {
throwError(`Index expected to be less than ${maxLen} but got ${index}`);
}
}
export function assertOneOf(value: any, ...validValues: any[]) {
if (validValues.indexOf(value) !== -1) return true;
throwError(`Expected value to be one of ${JSON.stringify(validValues)} but was ${
JSON.stringify(value)}.`);
}

View File

@ -12,10 +12,24 @@
export const enum CharCode {
UPPER_CASE = ~32, // & with this will make the char uppercase
SPACE = 32, // " "
EXCLAMATION = 33, // "!"
DOUBLE_QUOTE = 34, // "\""
HASH = 35, // "#"
SINGLE_QUOTE = 39, // "'"
OPEN_PAREN = 40, // "("
CLOSE_PAREN = 41, // ")"
STAR = 42, // "*"
SLASH = 47, // "/"
_0 = 48, // "0"
_1 = 49, // "1"
_2 = 50, // "2"
_3 = 51, // "3"
_4 = 52, // "4"
_5 = 53, // "5"
_6 = 54, // "6"
_7 = 55, // "7"
_8 = 56, // "8"
_9 = 57, // "9"
COLON = 58, // ":"
DASH = 45, // "-"
UNDERSCORE = 95, // "_"

View File

@ -752,7 +752,7 @@ export class DebugRenderer2 implements Renderer2 {
this.delegate.appendChild(parent, newChild);
}
insertBefore(parent: any, newChild: any, refChild: any): void {
insertBefore(parent: any, newChild: any, refChild: any, isMove?: boolean): void {
const debugEl = getDebugNode(parent);
const debugChildEl = getDebugNode(newChild);
const debugRefEl = getDebugNode(refChild)!;
@ -760,7 +760,7 @@ export class DebugRenderer2 implements Renderer2 {
debugEl.insertBefore(debugRefEl, debugChildEl);
}
this.delegate.insertBefore(parent, newChild, refChild);
this.delegate.insertBefore(parent, newChild, refChild, isMove);
}
removeChild(parent: any, oldChild: any): void {

View File

@ -64,7 +64,7 @@ onlyInIvy('Ivy specific').describe('Debug Representation', () => {
length: 1,
content: [{index: HEADER_OFFSET + 2, t: null, l: 'World'}]
});
expect(myComponentView.i18n).toEqual({
expect(myComponentView.expando).toEqual({
start: HEADER_OFFSET + 3,
end: HEADER_OFFSET + 4,
length: 1,
@ -74,8 +74,6 @@ onlyInIvy('Ivy specific').describe('Debug Representation', () => {
l: matchDomText('Hello World')
}]
});
expect(myComponentView.expando)
.toEqual({start: HEADER_OFFSET + 4, end: HEADER_OFFSET + 4, length: 0, content: []});
});
});
});

View File

@ -14,11 +14,8 @@ import localeEs from '@angular/common/locales/es';
import localeRo from '@angular/common/locales/ro';
import {computeMsgId} from '@angular/compiler';
import {Component, ContentChild, ContentChildren, Directive, ElementRef, HostBinding, Input, LOCALE_ID, NO_ERRORS_SCHEMA, Pipe, PipeTransform, QueryList, RendererFactory2, TemplateRef, Type, ViewChild, ViewContainerRef, ɵsetDocument} from '@angular/core';
import {getComponentDef} from '@angular/core/src/render3/definition';
import {setDelayProjection} from '@angular/core/src/render3/instructions/projection';
import {TI18n, TIcu} from '@angular/core/src/render3/interfaces/i18n';
import {DebugNode, HEADER_OFFSET, TVIEW} from '@angular/core/src/render3/interfaces/view';
import {getComponentLView, loadLContext} from '@angular/core/src/render3/util/discovery_utils';
import {getComponentLView} from '@angular/core/src/render3/util/discovery_utils';
import {TestBed} from '@angular/core/testing';
import {clearTranslations, loadTranslations} from '@angular/localize';
import {By, ɵDomRendererFactory2 as DomRendererFactory2} from '@angular/platform-browser';
@ -27,19 +24,19 @@ import {onlyInIvy} from '@angular/private/testing';
import {BehaviorSubject} from 'rxjs';
onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [AppComp, DirectiveWithTplRef, UppercasePipe],
// In some of the tests we use made-up tag names for better readability, however they'll
// cause validation errors. Add the `NO_ERRORS_SCHEMA` so that we don't have to declare
// dummy components for each one of them.
// In some of the tests we use made-up tag names for better readability, however
// they'll cause validation errors. Add the `NO_ERRORS_SCHEMA` so that we don't have
// to declare dummy components for each one of them.
schemas: [NO_ERRORS_SCHEMA],
});
});
afterEach(() => {
setDelayProjection(false);
clearTranslations();
});
@ -105,7 +102,8 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
{{ obj?.getA()?.b }}
</div>
`);
// the `obj` field is not yet defined, so 2nd and 3rd interpolations return empty strings
// the `obj` field is not yet defined, so 2nd and 3rd interpolations return empty
// strings
expect(fixture.nativeElement.innerHTML).toEqual(`<div> ANGULAR - - (fr) </div>`);
fixture.componentRef.instance.obj = {
@ -545,9 +543,9 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
TestBed.configureTestingModule({
providers: [
{provide: DOCUMENT, useFactory: _document, deps: []},
// TODO(FW-811): switch back to default server renderer (i.e. remove the line below)
// once it starts to support Ivy namespace format (URIs) correctly. For now, use
// `DomRenderer` that supports Ivy namespace format.
// TODO(FW-811): switch back to default server renderer (i.e. remove the line
// below) once it starts to support Ivy namespace format (URIs) correctly. For
// now, use `DomRenderer` that supports Ivy namespace format.
{provide: RendererFactory2, useClass: DomRendererFactory2}
],
});
@ -633,70 +631,15 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
jasmine.objectContaining({index: HEADER_OFFSET + 3, l: exclamation}),
]
});
expect(lViewDebug.i18n)
expect(lViewDebug.expando)
.toEqual(
{start: lViewDebug.vars.end, end: lViewDebug.expando.start, length: 0, content: []});
});
it('should create dynamic TNode for text nodes', () => {
const fixture =
initWithTemplate(AppComp, `<ng-container i18n>Hello <b>World</b>!</ng-container>`);
const lView = getComponentLView(fixture.componentInstance);
const hello_ = (fixture.nativeElement as Element).firstChild!;
const b = hello_.nextSibling!;
const world = b.firstChild!;
const exclamation = b.nextSibling!;
const container = exclamation.nextSibling!;
const lViewDebug = lView.debug!;
expect(lViewDebug.nodes.map(toTypeContent)).toEqual([
'ElementContainer(<!--ng-container-->)'
]);
// This assertion shows that the translated nodes are correctly linked into the TNode tree.
expect(lViewDebug.nodes[0].children.map(toTypeContent)).toEqual([
'Element(Hello )', 'Element(<b>)', 'Element(!)'
]);
// This assertion shows that the translated text is not part of decls
expect(lViewDebug.decls).toEqual({
start: HEADER_OFFSET,
end: HEADER_OFFSET + 3,
length: 3,
content: [
jasmine.objectContaining({index: HEADER_OFFSET + 0, l: container}),
jasmine.objectContaining({index: HEADER_OFFSET + 1}),
jasmine.objectContaining({index: HEADER_OFFSET + 2, l: b}),
]
});
// This assertion shows that the translated DOM elements (and corresponding TNode's are stored
// in i18n section of LView)
expect(lViewDebug.i18n).toEqual({
start: lViewDebug.vars.end,
end: lViewDebug.expando.start,
length: 3,
content: [
jasmine.objectContaining({index: HEADER_OFFSET + 3, l: hello_}),
jasmine.objectContaining({index: HEADER_OFFSET + 4, l: world}),
jasmine.objectContaining({index: HEADER_OFFSET + 5, l: exclamation}),
]
});
// This assertion shows the DOM operations which the i18n subsystem performed to update the
// DOM with translated text. The offsets in the debug text should match the offsets in the
// above assertions.
expect((lView[TVIEW]!.data[HEADER_OFFSET + 1]! as TI18n).create.debug).toEqual([
'lView[3] = document.createTextNode("Hello ")',
'(lView[0] as Element).appendChild(lView[3])',
'(lView[0] as Element).appendChild(lView[2])',
'lView[4] = document.createTextNode("World")',
'(lView[2] as Element).appendChild(lView[4])',
'setCurrentTNode(tView.data[2] as TNode)',
'lView[5] = document.createTextNode("!")',
'(lView[0] as Element).appendChild(lView[5])',
]);
});
describe('ICU', () => {
// In the case of ICUs we can't create TNodes for each ICU part, as different ICU instances
// may have different selections active and hence have different shape. In such a case
// a single `TIcuContainerNode` should be generated only.
// In the case of ICUs we can't create TNodes for each ICU part, as different ICU
// instances may have different selections active and hence have different shape. In
// such a case a single `TIcuContainerNode` should be generated only.
it('should create a single dynamic TNode for ICU', () => {
const fixture = initWithTemplate(AppComp, `
{count, plural,
@ -704,112 +647,42 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
=1 {one minute ago}
other {{{count}} minutes ago}
}
`);
`.trim());
const lView = getComponentLView(fixture.componentInstance);
const lViewDebug = lView.debug!;
fixture.detectChanges();
expect((fixture.nativeElement as Element).textContent).toEqual('just now');
const text_just_now = (fixture.nativeElement as Element).firstChild!;
const icuComment = text_just_now.nextSibling!;
expect(lViewDebug.nodes.map(toTypeContent)).toEqual(['IcuContainer(<!--ICU 3-->)']);
expect(lViewDebug.nodes.map(toTypeContent)).toEqual(['IcuContainer(<!--ICU 0:0-->)']);
// We want to ensure that the ICU container does not have any content!
// This is because the content is instance dependent and therefore can't be shared
// across `TNode`s.
expect(lViewDebug.nodes[0].children.map(toTypeContent)).toEqual([
'Element(just now)', // FIXME(misko): This should not be here. The content of the ICU is
// instance specific and as such can't be encoded in the tNodes.
]);
expect(lViewDebug.decls).toEqual({
start: HEADER_OFFSET,
end: HEADER_OFFSET + 1,
length: 1,
content: [
jasmine.objectContaining({
t: jasmine.objectContaining({
vars: 3, // one slot for: the `<!--ICU 3-->`
// one slot for: the last selected ICU case.
// one slot for: the actual text node to attach.
create: jasmine.any(Object),
update: jasmine.any(Object),
icus: [jasmine.any(Object)],
}),
l: null
}),
]
});
expect(((lViewDebug.decls.content[0].t as TI18n).create.debug)).toEqual([
'lView[3] = document.createComment("ICU 3")',
'(lView[0] as Element).appendChild(lView[3])',
]);
expect(((lViewDebug.decls.content[0].t as TI18n).update.debug)).toEqual([
'if (mask & 0b1) { icuSwitchCase(lView[3] as Comment, 0, `${lView[1]}`); }',
'if (mask & 0b11) { icuUpdateCase(lView[3] as Comment, 0); }',
]);
const tIcu = (lViewDebug.decls.content[0].t as TI18n).icus![0];
expect(tIcu.cases).toEqual(['0', '1', 'other']);
// Case: '0'
expect(tIcu.create[0].debug).toEqual([
'lView[5] = document.createTextNode("just now")',
'(lView[3] as Element).appendChild(lView[5])',
]);
expect(tIcu.remove[0].debug).toEqual(['(lView[0] as Element).remove(lView[5])']);
expect(tIcu.update[0].debug).toEqual([]);
// Case: '1'
expect(tIcu.create[1].debug).toEqual([
'lView[5] = document.createTextNode("one minute ago")',
'(lView[3] as Element).appendChild(lView[5])',
]);
expect(tIcu.remove[1].debug).toEqual(['(lView[0] as Element).remove(lView[5])']);
expect(tIcu.update[1].debug).toEqual([]);
// Case: 'other'
expect(tIcu.create[2].debug).toEqual([
'lView[5] = document.createTextNode("")',
'(lView[3] as Element).appendChild(lView[5])',
]);
expect(tIcu.remove[2].debug).toEqual(['(lView[0] as Element).remove(lView[5])']);
expect(tIcu.update[2].debug).toEqual([
'if (mask & 0b10) { (lView[5] as Text).textContent = `${lView[2]} minutes ago`; }'
]);
expect(lViewDebug.i18n).toEqual({
start: lViewDebug.vars.end,
end: lViewDebug.expando.start,
length: 3,
content: [
// ICU anchor `<!--ICU 3-->`.
jasmine.objectContaining({index: HEADER_OFFSET + 3, l: icuComment}),
// ICU `TIcu.currentCaseLViewIndex` storage location
jasmine.objectContaining({
index: HEADER_OFFSET + 4,
t: null,
l: 0, // The current ICU case
}),
jasmine.objectContaining({index: HEADER_OFFSET + 5, l: text_just_now}),
]
});
expect(lViewDebug.nodes[0].children.map(toTypeContent)).toEqual([]);
expect(fixture.nativeElement.innerHTML).toEqual('just now<!--ICU 0:0-->');
});
// FIXME(misko): re-enable and fix this use case.
xit('should support multiple ICUs', () => {
it('should support multiple ICUs', () => {
const fixture = initWithTemplate(AppComp, `
{count, plural,
=0 {just now}
=1 {one minute ago}
other {{{count}} minutes ago}
}
{count, plural,
=0 {just now}
=1 {one minute ago}
other {{{count}} minutes ago}
{name, select,
Angular {Mr. Angular}
other {Sir}
}
`);
const lView = getComponentLView(fixture.componentInstance);
expect(lView.debug!.nodes.map(toTypeContent)).toEqual(['IcuContainer(<!--ICU 3-->)']);
expect(lView.debug!.nodes.map(toTypeContent)).toEqual([
'IcuContainer(<!--ICU 0:0-->)',
'IcuContainer(<!--ICU 1:0-->)',
]);
// We want to ensure that the ICU container does not have any content!
// This is because the content is instance dependent and therefore can't be shared
// across `TNode`s.
expect(lView.debug!.nodes[0].children.map(toTypeContent)).toEqual([]);
expect(fixture.nativeElement.innerHTML)
.toEqual('just now<!--ICU 0:0-->Mr. Angular<!--ICU 1:0-->');
});
});
});
@ -905,19 +778,19 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
other {({{name}})}
}</div>`);
expect(fixture.nativeElement.innerHTML)
.toEqual(`<div>aucun <b>email</b>!<!--ICU 7--> - (Angular)<!--ICU 14--></div>`);
.toEqual(`<div>aucun <b>email</b>!<!--ICU 1:0--> - (Angular)<!--ICU 1:3--></div>`);
fixture.componentRef.instance.count = 4;
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML)
.toEqual(
`<div>4 <span title="Angular">emails</span><!--ICU 7--> - (Angular)<!--ICU 14--></div>`);
`<div>4 <span title="Angular">emails</span><!--ICU 1:0--> - (Angular)<!--ICU 1:3--></div>`);
fixture.componentRef.instance.count = 0;
fixture.componentRef.instance.name = 'John';
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML)
.toEqual(`<div>aucun <b>email</b>!<!--ICU 7--> - (John)<!--ICU 14--></div>`);
.toEqual(`<div>aucun <b>email</b>!<!--ICU 1:0--> - (John)<!--ICU 1:3--></div>`);
});
it('with custom interpolation config', () => {
@ -955,20 +828,32 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
}</span></div>`);
expect(fixture.nativeElement.innerHTML)
.toEqual(
`<div><span>aucun <b>email</b>!<!--ICU 9--></span> - <span>(Angular)<!--ICU 16--></span></div>`);
`<div>` +
`<span>aucun <b>email</b>!<!--ICU 1:0--></span>` +
` - ` +
`<span>(Angular)<!--ICU 1:3--></span>` +
`</div>`);
fixture.componentRef.instance.count = 4;
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML)
.toEqual(
`<div><span>4 <span title="Angular">emails</span><!--ICU 9--></span> - <span>(Angular)<!--ICU 16--></span></div>`);
`<div>` +
`<span>4 <span title="Angular">emails</span><!--ICU 1:0--></span>` +
` - ` +
`<span>(Angular)<!--ICU 1:3--></span>` +
`</div>`);
fixture.componentRef.instance.count = 0;
fixture.componentRef.instance.name = 'John';
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML)
.toEqual(
`<div><span>aucun <b>email</b>!<!--ICU 9--></span> - <span>(John)<!--ICU 16--></span></div>`);
`<div>` +
`<span>aucun <b>email</b>!<!--ICU 1:0--></span>` +
` - ` +
`<span>(John)<!--ICU 1:3--></span>` +
`</div>`);
});
it('inside template directives', () => {
@ -982,7 +867,7 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
other {({{name}})}
}</span></div>`);
expect(fixture.nativeElement.innerHTML)
.toEqual(`<div><span>(Angular)<!--ICU 4--></span><!--bindings={
.toEqual(`<div><span>(Angular)<!--ICU 0:0--></span><!--bindings={
"ng-reflect-ng-if": "true"
}--></div>`);
@ -1001,7 +886,7 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
const fixture = initWithTemplate(AppComp, `<ng-container i18n>{name, select,
other {({{name}})}
}</ng-container>`);
expect(fixture.nativeElement.innerHTML).toEqual(`(Angular)<!--ICU 4--><!--ng-container-->`);
expect(fixture.nativeElement.innerHTML).toEqual(`(Angular)<!--ICU 1:0--><!--ng-container-->`);
});
it('inside <ng-template>', () => {
@ -1036,12 +921,12 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
other {animals}
}!}
}</div>`);
expect(fixture.nativeElement.innerHTML).toEqual(`<div>zero<!--ICU 5--></div>`);
expect(fixture.nativeElement.innerHTML).toEqual(`<div>zero<!--ICU 1:1--></div>`);
fixture.componentRef.instance.count = 4;
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML)
.toEqual(`<div>4 animaux<!--nested ICU 0-->!<!--ICU 5--></div>`);
.toEqual(`<div>4 animaux<!--nested ICU 0-->!<!--ICU 1:1--></div>`);
});
it('nested with interpolations in "other" blocks', () => {
@ -1061,16 +946,16 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
}!}
other {other - {{count}}}
}</div>`);
expect(fixture.nativeElement.innerHTML).toEqual(`<div>zero<!--ICU 5--></div>`);
expect(fixture.nativeElement.innerHTML).toEqual(`<div>zero<!--ICU 1:1--></div>`);
fixture.componentRef.instance.count = 2;
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML)
.toEqual(`<div>2 animaux<!--nested ICU 0-->!<!--ICU 5--></div>`);
.toEqual(`<div>2 animaux<!--nested ICU 0-->!<!--ICU 1:1--></div>`);
fixture.componentRef.instance.count = 4;
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual(`<div>autre - 4<!--ICU 5--></div>`);
expect(fixture.nativeElement.innerHTML).toEqual(`<div>autre - 4<!--ICU 1:1--></div>`);
});
it('should return the correct plural form for ICU expressions when using "ro" locale', () => {
@ -1103,31 +988,31 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
=other {lots of emails}
}`);
expect(fixture.nativeElement.innerHTML).toEqual('no email<!--ICU 2-->');
expect(fixture.nativeElement.innerHTML).toEqual('no email<!--ICU 0:0-->');
// Change detection cycle, no model changes
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('no email<!--ICU 2-->');
expect(fixture.nativeElement.innerHTML).toEqual('no email<!--ICU 0:0-->');
fixture.componentInstance.count = 3;
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('a few emails<!--ICU 2-->');
expect(fixture.nativeElement.innerHTML).toEqual('a few emails<!--ICU 0:0-->');
fixture.componentInstance.count = 1;
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('one email<!--ICU 2-->');
expect(fixture.nativeElement.innerHTML).toEqual('one email<!--ICU 0:0-->');
fixture.componentInstance.count = 10;
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('a few emails<!--ICU 2-->');
expect(fixture.nativeElement.innerHTML).toEqual('a few emails<!--ICU 0:0-->');
fixture.componentInstance.count = 20;
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('lots of emails<!--ICU 2-->');
expect(fixture.nativeElement.innerHTML).toEqual('lots of emails<!--ICU 0:0-->');
fixture.componentInstance.count = 0;
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('no email<!--ICU 2-->');
expect(fixture.nativeElement.innerHTML).toEqual('no email<!--ICU 0:0-->');
});
it(`should return the correct plural form for ICU expressions when using "es" locale`, () => {
@ -1154,31 +1039,31 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
=other {lots of emails}
}`);
expect(fixture.nativeElement.innerHTML).toEqual('no email<!--ICU 2-->');
expect(fixture.nativeElement.innerHTML).toEqual('no email<!--ICU 0:0-->');
// Change detection cycle, no model changes
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('no email<!--ICU 2-->');
expect(fixture.nativeElement.innerHTML).toEqual('no email<!--ICU 0:0-->');
fixture.componentInstance.count = 3;
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('lots of emails<!--ICU 2-->');
expect(fixture.nativeElement.innerHTML).toEqual('lots of emails<!--ICU 0:0-->');
fixture.componentInstance.count = 1;
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('one email<!--ICU 2-->');
expect(fixture.nativeElement.innerHTML).toEqual('one email<!--ICU 0:0-->');
fixture.componentInstance.count = 10;
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('lots of emails<!--ICU 2-->');
expect(fixture.nativeElement.innerHTML).toEqual('lots of emails<!--ICU 0:0-->');
fixture.componentInstance.count = 20;
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('lots of emails<!--ICU 2-->');
expect(fixture.nativeElement.innerHTML).toEqual('lots of emails<!--ICU 0:0-->');
fixture.componentInstance.count = 0;
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toEqual('no email<!--ICU 2-->');
expect(fixture.nativeElement.innerHTML).toEqual('no email<!--ICU 0:0-->');
});
it('projection', () => {
@ -1273,12 +1158,12 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.innerHTML)
.toContain('<my-cmp><div>ONE<!--ICU 13--></div><!--container--></my-cmp>');
.toContain('<my-cmp><div>ONE<!--ICU 1:0--></div><!--container--></my-cmp>');
fixture.componentRef.instance.count = 2;
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.innerHTML)
.toContain('<my-cmp><div>OTHER<!--ICU 13--></div><!--container--></my-cmp>');
.toContain('<my-cmp><div>OTHER<!--ICU 1:0--></div><!--container--></my-cmp>');
// destroy component
fixture.componentInstance.condition = false;
@ -1290,7 +1175,7 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
fixture.componentInstance.count = 1;
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.innerHTML)
.toContain('<my-cmp><div>ONE<!--ICU 13--></div><!--container--></my-cmp>');
.toContain('<my-cmp><div>ONE<!--ICU 1:0--></div><!--container--></my-cmp>');
});
it('with nested ICU expression and inside a container when creating a view via vcr.createEmbeddedView',
@ -1362,12 +1247,12 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.innerHTML)
.toBe(
'<my-cmp><div>2 animals<!--nested ICU 0-->!<!--ICU 15--></div><!--container--></my-cmp>');
'<my-cmp><div>2 animals<!--nested ICU 0-->!<!--ICU 1:1--></div><!--container--></my-cmp>');
fixture.componentRef.instance.count = 1;
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.innerHTML)
.toBe('<my-cmp><div>ONE<!--ICU 15--></div><!--container--></my-cmp>');
.toBe('<my-cmp><div>ONE<!--ICU 1:1--></div><!--container--></my-cmp>');
});
it('with nested containers', () => {
@ -1602,7 +1487,8 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
fixture.componentInstance.count = 2;
fixture.detectChanges();
// check switching to an existing case after processing nested ICU without matching case
// check switching to an existing case after processing nested ICU without matching
// case
expect(fixture.nativeElement.textContent.trim()).toBe('deux (select) - deux (plural)');
fixture.componentInstance.count = 1;
@ -1651,26 +1537,17 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
expect(fixture.nativeElement.textContent.trim()).toBe('deux articles');
});
// FIXME(misko): re-enable and fix this use case. Root cause is that
// `addRemoveViewFromContainer` needs to understand ICU
xit('should handle select expressions without an `other` parameter inside a template', () => {
it('should handle select expressions without an `other` parameter inside a template', () => {
const fixture = initWithTemplate(AppComp, `
<ng-container *ngFor="let item of items">{item.value, select, 0 {A} 1 {B} 2 {C}}</ng-container>
`);
fixture.componentInstance.items = [{value: 0}, {value: 1}, {value: 1337}];
fixture.detectChanges();
const p = fixture.nativeElement.querySelector('p');
const lContext = loadLContext(p);
const lView = lContext.lView;
const nodeIndex = lContext.nodeIndex;
const tView = lView[TVIEW];
const i18n = tView.data[nodeIndex + 1] as unknown as TI18n;
expect(fixture.nativeElement.textContent.trim()).toBe('AB');
fixture.componentInstance.items[0].value = 2;
fixture.detectChanges();
expect(fixture.nativeElement.textContent.trim()).toBe('CB');
fail('testing');
});
it('should render an element whose case did not match initially', () => {
@ -1953,7 +1830,8 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
const fixture = initWithTemplate(AppComp, `
<div i18n-title title="{{ name | uppercase }} - {{ obj?.a?.b }} - {{ obj?.getA()?.b }}"></div>
`);
// the `obj` field is not yet defined, so 2nd and 3rd interpolations return empty strings
// the `obj` field is not yet defined, so 2nd and 3rd interpolations return empty
// strings
expect(fixture.nativeElement.firstChild.title).toEqual(`ANGULAR - - (fr)`);
fixture.componentRef.instance.obj = {
@ -2106,8 +1984,8 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
const innerDiv: HTMLElement = fixture.nativeElement.querySelector('div[inner]');
// Note that ideally we'd just compare the innerHTML here, but different browsers return
// the order of attributes differently. E.g. most browsers preserve the declaration order,
// but IE does not.
// the order of attributes differently. E.g. most browsers preserve the declaration
// order, but IE does not.
expect(outerDiv.getAttribute('title')).toBe('début 2 milieu 1 fin');
expect(outerDiv.getAttribute('class')).toBe('foo');
expect(outerDiv.textContent!.trim()).toBe('traduction: un email');
@ -2491,13 +2369,13 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML)
.toEqual(
`<child><div>Contenu enfant et projection depuis Parent<!--ICU 15--></div></child>`);
`<child><div>Contenu enfant et projection depuis Parent<!--ICU 1:0--></div></child>`);
fixture.componentRef.instance.name = 'angular';
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML)
.toEqual(
`<child><div>Contenu enfant et projection depuis Angular<!--ICU 15--></div></child>`);
`<child><div>Contenu enfant et projection depuis Angular<!--ICU 1:0--></div></child>`);
});
it(`shouldn't project deleted projections in i18n blocks`, () => {
@ -2850,6 +2728,301 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
expect(fixture.nativeElement.textContent).toContain('a b');
});
});
describe('viewContainerRef with i18n', () => {
it('should create ViewContainerRef with i18n', () => {
// This test demonstrates an issue with creating a `ViewContainerRef` and having i18n at the
// parent element. The reason this broke is that in this case the `ViewContainerRef` creates
// an dynamic anchor comment but uses `HostTNode` for it which is incorrect. `appendChild`
// then tries to add internationalization to the comment node and fails.
@Component({
template: `
<div i18n>before|<div myDir>inside</div>|after</div>
`
})
class MyApp {
}
@Directive({selector: '[myDir]'})
class MyDir {
constructor(vcRef: ViewContainerRef) {
myDir = this;
}
}
let myDir!: MyDir;
TestBed.configureTestingModule({declarations: [MyApp, MyDir]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
expect(myDir).toBeDefined();
expect(fixture.nativeElement.textContent).toEqual(`before|inside|after`);
});
});
it('should create ICU with attributes', () => {
// This test demonstrates an issue with setting attributes on ICU elements.
// NOTE: This test is extracted from g3.
@Component({
template: `
<h1 class="num-cart-items" i18n *ngIf="true">{
registerItemCount, plural,
=0 {Your cart}
=1 {Your cart <span class="item-count">(1 item)</span>}
other {
Your cart <span class="item-count">({{
registerItemCount
}} items)</span>
}
}</h1>`
})
class MyApp {
registerItemCount = 1;
}
TestBed.configureTestingModule({declarations: [MyApp]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual(`Your cart (1 item)`);
});
it('should not insertBeforeIndex non-projected content text', () => {
// This test demonstrates an issue with setting attributes on ICU elements.
// NOTE: This test is extracted from g3.
@Component({template: `<div i18n>before|<child>TextNotProjected</child>|after</div>`})
class MyApp {
}
@Component({
selector: 'child',
template: 'CHILD',
})
class Child {
}
TestBed.configureTestingModule({declarations: [MyApp, Child]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual(`before|CHILD|after`);
});
it('should create a pipe inside i18n block', () => {
// This test demonstrates an issue with i18n messing up `getCurrentTNode` which subsequently
// breaks the DI. The issue is that the `i18nStartFirstCreatePass` would create placeholder
// NODES, and than leave `getCurrentTNode` in undetermined state which would then break DI.
// NOTE: This test is extracted from g3.
@Component({
template: `
<div i18n [title]="null | async"><div>A</div></div>
<div i18n>{{(null | async)||'B'}}<div></div></div>`
})
class MyApp {
}
TestBed.configureTestingModule({declarations: [MyApp]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual(`AB`);
});
it('should copy injector information unto placeholder', () => {
// This test demonstrates an issue with i18n Placeholders loosing `injectorIndex` information.
// NOTE: This test is extracted from g3.
@Component({
template: `
<parent i18n>
<middle>
<child>Text</child>
</middle>
</parent>`
})
class MyApp {
}
@Component({selector: 'parent'})
class Parent {
}
@Component({selector: 'middle'})
class Middle {
}
@Component({selector: 'child'})
class Child {
constructor(public middle: Middle) {
child = this;
}
}
let child!: Child;
TestBed.configureTestingModule({declarations: [MyApp, Parent, Middle, Child]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
expect(child.middle).toBeInstanceOf(Middle);
});
it('should allow container in gotClosestRElement', () => {
// A second iteration of the loop will have `Container` `TNode`s pass through the system.
// NOTE: This test is extracted from g3.
@Component({
template: `
<div *ngFor="let i of [1,2]">
<ng-template #tmpl i18n><span *ngIf="true">X</span></ng-template>
<span [ngTemplateOutlet]="tmpl"></span>
</div>`
})
class MyApp {
}
TestBed.configureTestingModule({declarations: [MyApp]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual(`XX`);
});
it('should link text after ICU', () => {
// i18n block must restore the current `currentTNode` so that trailing text node can link to it.
// NOTE: This test is extracted from g3.
@Component({
template: `
<ng-container *ngFor="let index of [1, 2]">
{{'['}}
{index, plural, =1 {1} other {*}}
{index, plural, =1 {one} other {many}}
{{'-'}}
<span>+</span>
{{'-'}}
{index, plural, =1 {first} other {rest}}
{{']'}}
</ng-container>
/
<ng-container *ngFor="let index of [1, 2]" i18n>
{{'['}}
{index, plural, =1 {1} other {*}}
{index, plural, =1 {one} other {many}}
{{'-'}}
<span>+</span>
{{'-'}}
{index, plural, =1 {first} other {rest}}
{{']'}}
</ng-container>
`
})
class MyApp {
}
TestBed.configureTestingModule({declarations: [MyApp]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
const textContent = fixture.nativeElement.textContent as string;
expect(textContent.split('/').map(s => s.trim())).toEqual([
'[ 1 one - + - first ] [ * many - + - rest ]',
'[ 1 one - + - first ] [ * many - + - rest ]',
]);
});
it('should ignore non-instantiated ICUs on update', () => {
// Demonstrates an issue of same selector expression used in nested ICUs, causes non
// instantiated nested ICUs to be updated.
// NOTE: This test is extracted from g3.
@Component({
template: `
before|
{ retention.unit, select,
SECONDS {
{retention.durationInUnits, plural,
=1 {1 second}
other {{{retention.durationInUnits}} seconds}
}
}
DAYS {
{retention.durationInUnits, plural,
=1 {1 day}
other {{{retention.durationInUnits}} days}
}
}
MONTHS {
{retention.durationInUnits, plural,
=1 {1 month}
other {{{retention.durationInUnits}} months}
}
}
YEARS {
{retention.durationInUnits, plural,
=1 {1 year}
other {{{retention.durationInUnits}} years}
}
}
other {}
}
|after.
`
})
class MyApp {
retention = {
durationInUnits: 10,
unit: 'SECONDS',
};
}
TestBed.configureTestingModule({declarations: [MyApp]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
const textContent = fixture.nativeElement.textContent as string;
expect(textContent.replace(/\s+/g, ' ').trim()).toEqual(`before| 10 seconds |after.`);
});
it('should render attributes defined in ICUs', () => {
// NOTE: This test is extracted from g3.
@Component({
template: `
<div i18n>{
parameters.length,
plural,
=1 {Affects parameter <span class="parameter-name" attr="should_be_present">{{parameters[0].name}}</span>}
other {Affects {{parameters.length}} parameters, including <span
class="parameter-name">{{parameters[0].name}}</span>}
}</div>
`
})
class MyApp {
parameters = [{name: 'void_abt_param'}];
}
TestBed.configureTestingModule({declarations: [MyApp]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
const span = (fixture.nativeElement as HTMLElement).querySelector('span')!;
expect(span.getAttribute('attr')).toEqual('should_be_present');
expect(span.getAttribute('class')).toEqual('parameter-name');
});
it('should support different ICUs cases for each *ngFor iteration', () => {
@Component({
template: `
<ul i18n>
<li *ngFor="let item of items">{
item, plural,
=1 {<b>one</b>}
=2 {<i>two</i>}
},</li>
</ul>`
})
class MyApp {
items = [1, 2];
}
TestBed.configureTestingModule({declarations: [MyApp]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual(`one,two,`);
fixture.componentInstance.items = [2, 1];
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual(`two,one,`);
});
});
function initWithTemplate(compType: Type<any>, template: string) {

View File

@ -2510,6 +2510,33 @@ describe('animation tests', function() {
});
});
it('should not animate i18n insertBefore', () => {
// I18n uses `insertBefore` API to insert nodes in correct order. Animation assumes that
// any `insertBefore` is a move and tries to animate it.
// NOTE: This test was extracted from `g3`
@Component({
template: `<div i18n>Hello <span>World</span>!</div>`,
animations: [
trigger(
'myAnimation',
[
transition('* => *', [animate(1000)]),
]),
]
})
class Cmp {
}
TestBed.configureTestingModule({declarations: [Cmp]});
const fixture = TestBed.createComponent(Cmp);
fixture.detectChanges();
const players = getLog();
const span = fixture.debugElement.nativeElement.querySelector('span');
expect(span.innerText).toEqual('World');
// We should not insert `ng-star-inserted` into the span class.
expect(span.className).not.toContain('ng-star-inserted');
});
describe('animation listeners', () => {
it('should trigger a `start` state change listener for when the animation changes state from void => state',
fakeAsync(() => {

View File

@ -155,9 +155,6 @@
{
"name": "generatePropertyAliases"
},
{
"name": "isInCheckNoChangesMode"
},
{
"name": "getClosureSafeProperty"
},
@ -173,6 +170,9 @@
{
"name": "getCurrentTNode"
},
{
"name": "getCurrentTNodePlaceholderOk"
},
{
"name": "getFirstLContainer"
},
@ -245,6 +245,9 @@
{
"name": "isCurrentTNodeParent"
},
{
"name": "isInCheckNoChangesMode"
},
{
"name": "isInlineTemplate"
},
@ -281,6 +284,9 @@
{
"name": "nativeAppendOrInsertBefore"
},
{
"name": "nativeInsertBefore"
},
{
"name": "nextNgElementId"
},
@ -344,6 +350,9 @@
{
"name": "setUpAttributes"
},
{
"name": "unwrapRNode"
},
{
"name": "updateTransplantedViewCount"
},

View File

@ -797,6 +797,9 @@
{
"name": "createDirectivesInstances"
},
{
"name": "createElementNode"
},
{
"name": "createElementRef"
},
@ -815,6 +818,9 @@
{
"name": "createPlatformFactory"
},
{
"name": "createTNode"
},
{
"name": "createTView"
},
@ -857,9 +863,6 @@
{
"name": "domRendererFactory3"
},
{
"name": "elementCreate"
},
{
"name": "empty"
},
@ -947,9 +950,6 @@
{
"name": "generatePropertyAliases"
},
{
"name": "isInCheckNoChangesMode"
},
{
"name": "getClosureSafeProperty"
},
@ -965,6 +965,9 @@
{
"name": "getCurrentTNode"
},
{
"name": "getCurrentTNodePlaceholderOk"
},
{
"name": "getDOM"
},
@ -1097,6 +1100,9 @@
{
"name": "hostReportError"
},
{
"name": "icuContainerIterate"
},
{
"name": "identity"
},
@ -1181,6 +1187,9 @@
{
"name": "isFunction"
},
{
"name": "isInCheckNoChangesMode"
},
{
"name": "isInlineTemplate"
},
@ -1484,9 +1493,6 @@
{
"name": "setBindingRootForHostBindings"
},
{
"name": "setIsInCheckNoChangesMode"
},
{
"name": "setCurrentDirectiveIndex"
},
@ -1514,6 +1520,9 @@
{
"name": "setInputsFromAttrs"
},
{
"name": "setIsInCheckNoChangesMode"
},
{
"name": "setLocaleId"
},

View File

@ -107,9 +107,6 @@
{
"name": "extractPipeDef"
},
{
"name": "isInCheckNoChangesMode"
},
{
"name": "getClosureSafeProperty"
},
@ -122,6 +119,9 @@
{
"name": "getCurrentTNode"
},
{
"name": "getCurrentTNodePlaceholderOk"
},
{
"name": "getFirstLContainer"
},
@ -158,6 +158,9 @@
{
"name": "invertObject"
},
{
"name": "isInCheckNoChangesMode"
},
{
"name": "isProceduralRenderer"
},
@ -173,6 +176,9 @@
{
"name": "nativeAppendOrInsertBefore"
},
{
"name": "nativeInsertBefore"
},
{
"name": "nextNgElementId"
},
@ -218,10 +224,16 @@
{
"name": "setSelectedIndex"
},
{
"name": "unwrapRNode"
},
{
"name": "updateTransplantedViewCount"
},
{
"name": "viewAttachedToChangeDetector"
},
{
"name": "ɵɵtext"
}
]

View File

@ -1031,6 +1031,9 @@
{
"name": "createContainerRef"
},
{
"name": "createElementNode"
},
{
"name": "createElementRef"
},
@ -1064,6 +1067,9 @@
{
"name": "createRouterScroller"
},
{
"name": "createTNode"
},
{
"name": "createTView"
},
@ -1139,9 +1145,6 @@
{
"name": "domRendererFactory3"
},
{
"name": "elementCreate"
},
{
"name": "empty"
},
@ -1259,9 +1262,6 @@
{
"name": "getBootstrapListener"
},
{
"name": "isInCheckNoChangesMode"
},
{
"name": "getClosureSafeProperty"
},
@ -1280,6 +1280,9 @@
{
"name": "getCurrentTNode"
},
{
"name": "getCurrentTNodePlaceholderOk"
},
{
"name": "getDOM"
},
@ -1439,6 +1442,9 @@
{
"name": "hostReportError"
},
{
"name": "icuContainerIterate"
},
{
"name": "identity"
},
@ -1517,6 +1523,9 @@
{
"name": "isFunction"
},
{
"name": "isInCheckNoChangesMode"
},
{
"name": "isInlineTemplate"
},
@ -1817,9 +1826,6 @@
{
"name": "setBindingRootForHostBindings"
},
{
"name": "setIsInCheckNoChangesMode"
},
{
"name": "setCurrentDirectiveIndex"
},
@ -1847,6 +1853,9 @@
{
"name": "setInputsFromAttrs"
},
{
"name": "setIsInCheckNoChangesMode"
},
{
"name": "setLocaleId"
},

View File

@ -257,6 +257,9 @@
{
"name": "createLView"
},
{
"name": "createTNode"
},
{
"name": "createTView"
},
@ -329,9 +332,6 @@
{
"name": "generatePropertyAliases"
},
{
"name": "isInCheckNoChangesMode"
},
{
"name": "getClosureSafeProperty"
},
@ -347,6 +347,9 @@
{
"name": "getCurrentTNode"
},
{
"name": "getCurrentTNodePlaceholderOk"
},
{
"name": "getDebugContext"
},
@ -440,6 +443,9 @@
{
"name": "hasTagAndTypeMatch"
},
{
"name": "icuContainerIterate"
},
{
"name": "includeViewProviders"
},
@ -485,6 +491,9 @@
{
"name": "isDirectiveHost"
},
{
"name": "isInCheckNoChangesMode"
},
{
"name": "isInlineTemplate"
},
@ -641,9 +650,6 @@
{
"name": "setBindingRootForHostBindings"
},
{
"name": "setIsInCheckNoChangesMode"
},
{
"name": "setCurrentDirectiveIndex"
},
@ -668,6 +674,9 @@
{
"name": "setInputsFromAttrs"
},
{
"name": "setIsInCheckNoChangesMode"
},
{
"name": "setSelectedIndex"
},

View File

@ -0,0 +1,183 @@
/**
* @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 {addTNodeAndUpdateInsertBeforeIndex} from '@angular/core/src/render3/i18n/i18n_insert_before_index';
import {createTNode} from '@angular/core/src/render3/instructions/shared';
import {TNode, TNodeType} from '@angular/core/src/render3/interfaces/node';
import {matchTNode} from '../matchers';
describe('addTNodeAndUpdateInsertBeforeIndex', () => {
function tNode(index: number, type: TNodeType, insertBeforeIndex: number|null = null): TNode {
const tNode = createTNode(null!, null, type, index, null, null);
tNode.insertBeforeIndex = insertBeforeIndex;
return tNode;
}
function tPlaceholderElementNode(index: number, insertBeforeIndex: number|null = null) {
return tNode(index, TNodeType.Placeholder, insertBeforeIndex);
}
function tI18NTextNode(index: number, insertBeforeIndex: number|null = null) {
return tNode(index, TNodeType.Element, insertBeforeIndex);
}
it('should add first node', () => {
const previousTNodes: TNode[] = [];
addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tPlaceholderElementNode(20));
expect(previousTNodes).toEqual([
matchTNode({index: 20, insertBeforeIndex: null}),
]);
});
describe('when adding a placeholder', () => {
describe('whose index is greater than those already there', () => {
it('should not update the `insertBeforeIndex` values', () => {
const previousTNodes: TNode[] = [
tPlaceholderElementNode(20),
tPlaceholderElementNode(21),
];
addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tPlaceholderElementNode(22));
expect(previousTNodes).toEqual([
matchTNode({index: 20, insertBeforeIndex: null}),
matchTNode({index: 21, insertBeforeIndex: null}),
matchTNode({index: 22, insertBeforeIndex: null}),
]);
});
});
describe('whose index is smaller than current nodes', () => {
it('should update the previous insertBeforeIndex', () => {
const previousTNodes: TNode[] = [
tPlaceholderElementNode(20),
tPlaceholderElementNode(21),
];
addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tPlaceholderElementNode(19));
expect(previousTNodes).toEqual([
matchTNode({index: 20, insertBeforeIndex: 19}),
matchTNode({index: 21, insertBeforeIndex: 19}),
matchTNode({index: 19, insertBeforeIndex: null}),
]);
});
it('should not update the previous insertBeforeIndex if it is already set', () => {
const previousTNodes: TNode[] = [
tPlaceholderElementNode(20, 19),
tPlaceholderElementNode(21, 19),
tPlaceholderElementNode(19),
];
addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tPlaceholderElementNode(18));
expect(previousTNodes).toEqual([
matchTNode({index: 20, insertBeforeIndex: 19}),
matchTNode({index: 21, insertBeforeIndex: 19}),
matchTNode({index: 19, insertBeforeIndex: 18}),
matchTNode({index: 18, insertBeforeIndex: null}),
]);
});
it('should not update the previous insertBeforeIndex if it is created after', () => {
const previousTNodes: TNode[] = [
tPlaceholderElementNode(20, 15),
tPlaceholderElementNode(21, 15),
tPlaceholderElementNode(15),
];
addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tPlaceholderElementNode(18));
expect(previousTNodes).toEqual([
matchTNode({index: 20, insertBeforeIndex: 15}),
matchTNode({index: 21, insertBeforeIndex: 15}),
matchTNode({index: 15, insertBeforeIndex: null}),
matchTNode({index: 18, insertBeforeIndex: null}),
]);
});
});
});
describe('when adding a i18nText', () => {
describe('whose index is greater than those already there', () => {
it('should not update the `insertBeforeIndex` values', () => {
const previousTNodes: TNode[] = [
tPlaceholderElementNode(20),
tPlaceholderElementNode(21),
];
addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tI18NTextNode(22));
expect(previousTNodes).toEqual([
matchTNode({index: 20, insertBeforeIndex: 22}),
matchTNode({index: 21, insertBeforeIndex: 22}),
matchTNode({index: 22, insertBeforeIndex: null}),
]);
});
});
describe('whose index is smaller than current nodes', () => {
it('should update the previous insertBeforeIndex', () => {
const previousTNodes: TNode[] = [
tPlaceholderElementNode(20),
tPlaceholderElementNode(21),
];
addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tI18NTextNode(19));
expect(previousTNodes).toEqual([
matchTNode({index: 20, insertBeforeIndex: 19}),
matchTNode({index: 21, insertBeforeIndex: 19}),
matchTNode({index: 19, insertBeforeIndex: null}),
]);
});
it('should not update the previous insertBeforeIndex if it is already set', () => {
const previousTNodes: TNode[] = [
tPlaceholderElementNode(20, 19),
tPlaceholderElementNode(21, 19),
tPlaceholderElementNode(19),
];
addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tI18NTextNode(18));
expect(previousTNodes).toEqual([
matchTNode({index: 20, insertBeforeIndex: 19}),
matchTNode({index: 21, insertBeforeIndex: 19}),
matchTNode({index: 19, insertBeforeIndex: 18}),
matchTNode({index: 18, insertBeforeIndex: null}),
]);
});
it('should not update the previous insertBeforeIndex if it is created after', () => {
const previousTNodes: TNode[] = [
tPlaceholderElementNode(20, 15),
tPlaceholderElementNode(21, 15),
tPlaceholderElementNode(15),
];
addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tI18NTextNode(18));
expect(previousTNodes).toEqual([
matchTNode({index: 20, insertBeforeIndex: 15}),
matchTNode({index: 21, insertBeforeIndex: 15}),
matchTNode({index: 15, insertBeforeIndex: 18}),
matchTNode({index: 18, insertBeforeIndex: null}),
]);
});
});
});
describe('scenario', () => {
it('should rearrange the nodes', () => {
const previousTNodes: TNode[] = [];
addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tPlaceholderElementNode(22));
addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tPlaceholderElementNode(28));
addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tPlaceholderElementNode(24));
addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tPlaceholderElementNode(25));
addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tI18NTextNode(29));
addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tPlaceholderElementNode(23));
addTNodeAndUpdateInsertBeforeIndex(previousTNodes, tPlaceholderElementNode(27));
expect(previousTNodes).toEqual([
matchTNode({index: 22, insertBeforeIndex: 29}),
matchTNode({index: 28, insertBeforeIndex: 24}),
matchTNode({index: 24, insertBeforeIndex: 29}),
matchTNode({index: 25, insertBeforeIndex: 29}),
matchTNode({index: 29, insertBeforeIndex: null}),
matchTNode({index: 23, insertBeforeIndex: null}),
matchTNode({index: 27, insertBeforeIndex: null}),
]);
});
});
});

View File

@ -0,0 +1,289 @@
/**
* @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 {ɵɵi18nApply, ɵɵi18nExp} from '@angular/core';
import {applyCreateOpCodes} from '@angular/core/src/render3/i18n/i18n_apply';
import {i18nStartFirstCreatePass} from '@angular/core/src/render3/i18n/i18n_parse';
import {getTIcu} from '@angular/core/src/render3/i18n/i18n_util';
import {IcuType, TI18n} from '@angular/core/src/render3/interfaces/i18n';
import {HEADER_OFFSET} from '@angular/core/src/render3/interfaces/view';
import {expect} from '@angular/core/testing/src/testing_internal';
import {matchTI18n, matchTIcu} from '../matchers';
import {debugMatch} from '../utils';
import {ViewFixture} from '../view_fixture';
describe('i18n_parse', () => {
let fixture: ViewFixture;
beforeEach(() => fixture = new ViewFixture({decls: 1, vars: 1}));
describe('icu', () => {
it('should parse simple text', () => {
const tI18n = toT18n('some text');
expect(tI18n).toEqual(matchTI18n({
create: debugMatch([
'lView[22] = document.createText("some text");',
'parent.appendChild(lView[22]);',
]),
update: [],
}));
fixture.apply(() => applyCreateOpCodes(fixture.lView, tI18n.create, fixture.host, null));
expect(fixture.host.innerHTML).toEqual('some text');
});
it('should parse simple ICU', () => {
// TData | LView
// ---------------------------+-------------------------------
// ----- DECL -----
// 20: TI18n |
// ----- VARS -----
// 21: Binding for ICU |
// ----- EXPANDO -----
// 22: null | #text(before|)
// 23: TIcu | <!-- ICU 0:0 -->
// 24: null | currently selected ICU case
// 25: null | #text(caseA)
// 26: null | #text(otherCase)
// 27: null | #text(|after)
const tI18n = toT18n(`before|{
<EFBFBD>0<EFBFBD>, select,
A {caseA}
other {otherCase}
}|after`);
expect(tI18n).toEqual(matchTI18n({
create: debugMatch([
'lView[22] = document.createText("before|");',
'parent.appendChild(lView[22]);',
'lView[23] = document.createComment("ICU 0:0");',
'parent.appendChild(lView[23]);',
'lView[27] = document.createText("|after");',
'parent.appendChild(lView[27]);',
]),
update: debugMatch([
'if (mask & 0b1) { icuSwitchCase(23, `${lView[i-1]}`); }',
])
}));
expect(getTIcu(fixture.tView, 23)).toEqual(matchTIcu({
type: IcuType.select,
anchorIdx: 23,
currentCaseLViewIndex: 24,
cases: ['A', 'other'],
create: [
debugMatch([
'lView[25] = document.createTextNode("caseA")',
'(lView[0] as Element).appendChild(lView[25])'
]),
debugMatch([
'lView[26] = document.createTextNode("otherCase")',
'(lView[0] as Element).appendChild(lView[26])',
])
],
update: [
debugMatch([]),
debugMatch([]),
],
remove: [
debugMatch(['(lView[0] as Element).remove(lView[25])']),
debugMatch(['(lView[0] as Element).remove(lView[26])'])
],
}));
fixture.apply(() => {
applyCreateOpCodes(fixture.lView, tI18n.create, fixture.host, null);
expect(fixture.host.innerHTML).toEqual('before|<!--ICU 0:0-->|after');
});
fixture.apply(() => {
ɵɵi18nExp('A');
ɵɵi18nApply(0); // index 0 + HEADER_OFFSET = 20;
expect(fixture.host.innerHTML).toEqual('before|caseA<!--ICU 0:0-->|after');
});
fixture.apply(() => {
ɵɵi18nExp('x');
ɵɵi18nApply(0); // index 0 + HEADER_OFFSET = 20;
expect(fixture.host.innerHTML).toEqual('before|otherCase<!--ICU 0:0-->|after');
});
fixture.apply(() => {
ɵɵi18nExp('A');
ɵɵi18nApply(0); // index 0 + HEADER_OFFSET = 20;
expect(fixture.host.innerHTML).toEqual('before|caseA<!--ICU 0:0-->|after');
});
});
it('should parse HTML in ICU', () => {
const tI18n = toT18n(`{
<EFBFBD>0<EFBFBD>, select,
A {Hello <b>world<i>!</i></b>}
other {<div>{<EFBFBD>0<EFBFBD>, select, 0 {nested0} other {nestedOther}}</div>}
}`);
fixture.apply(() => {
applyCreateOpCodes(fixture.lView, tI18n.create, fixture.host, null);
expect(fixture.host.innerHTML).toEqual('<!--ICU 0:0-->');
});
fixture.apply(() => {
ɵɵi18nExp('A');
ɵɵi18nApply(0); // index 0 + HEADER_OFFSET = 20;
expect(fixture.host.innerHTML).toEqual('Hello <b>world<i>!</i></b><!--ICU 0:0-->');
});
fixture.apply(() => {
ɵɵi18nExp('x');
ɵɵi18nApply(0); // index 0 + HEADER_OFFSET = 20;
expect(fixture.host.innerHTML)
.toEqual('<div>nestedOther<!--nested ICU 0--></div><!--ICU 0:0-->');
});
fixture.apply(() => {
ɵɵi18nExp('A');
ɵɵi18nApply(0); // index 0 + HEADER_OFFSET = 20;
expect(fixture.host.innerHTML).toEqual('Hello <b>world<i>!</i></b><!--ICU 0:0-->');
});
});
it('should parse nested ICU', () => {
fixture = new ViewFixture({decls: 1, vars: 3});
// TData | LView
// ---------------------------+-------------------------------
// ----- DECL -----
// 20: TI18n |
// ----- VARS -----
// 21: Binding for parent ICU |
// 22: Binding for child ICU |
// 23: Binding for child ICU |
// ----- EXPANDO -----
// 24: TIcu (parent) | <!-- ICU 0:0 -->
// 25: null | currently selected ICU case
// 26: null | #text( parentA )
// 27: TIcu (child) | <!-- nested ICU 0 -->
// 28: null | currently selected ICU case
// 29: null | #text(nested0)
// 30: null | #text({{<7B>2<EFBFBD>}})
// 31: null | #text( )
// 32: null | #text( parentOther )
const tI18n = toT18n(`{
<EFBFBD>0<EFBFBD>, select,
A {parentA {<EFBFBD>1<EFBFBD>, select, 0 {nested0} other {<EFBFBD>2<EFBFBD>}}!}
other {parentOther}
}`);
expect(tI18n).toEqual(matchTI18n({
create: debugMatch([
'lView[24] = document.createComment("ICU 0:0");',
'parent.appendChild(lView[24]);',
]),
update: debugMatch([
'if (mask & 0b1) { icuSwitchCase(24, `${lView[i-1]}`); }',
'if (mask & 0b10) { icuSwitchCase(27, `${lView[i-2]}`); }',
'if (mask & 0b100) { icuUpdateCase(27); }',
]),
}));
expect(getTIcu(fixture.tView, 24)).toEqual(matchTIcu({
type: IcuType.select,
anchorIdx: 24,
currentCaseLViewIndex: 25,
cases: ['A', 'other'],
create: [
debugMatch([
'lView[26] = document.createTextNode("parentA ")',
'(lView[0] as Element).appendChild(lView[26])',
'lView[27] = document.createComment("nested ICU 0")',
'(lView[0] as Element).appendChild(lView[27])',
'lView[31] = document.createTextNode("!")',
'(lView[0] as Element).appendChild(lView[31])',
]),
debugMatch([
'lView[32] = document.createTextNode("parentOther")',
'(lView[0] as Element).appendChild(lView[32])',
])
],
update: [
debugMatch([]),
debugMatch([]),
],
remove: [
debugMatch([
'(lView[0] as Element).remove(lView[26])',
'removeNestedICU(27)',
'(lView[0] as Element).remove(lView[27])',
'(lView[0] as Element).remove(lView[31])',
]),
debugMatch([
'(lView[0] as Element).remove(lView[32])',
])
],
}));
expect(getTIcu(fixture.tView, 27)).toEqual(matchTIcu({
type: IcuType.select,
anchorIdx: 27,
currentCaseLViewIndex: 28,
cases: ['0', 'other'],
create: [
debugMatch([
'lView[29] = document.createTextNode("nested0")',
'(lView[0] as Element).appendChild(lView[29])'
]),
debugMatch([
'lView[30] = document.createTextNode("")',
'(lView[0] as Element).appendChild(lView[30])',
])
],
update: [
debugMatch([]),
debugMatch([
'if (mask & 0b100) { (lView[30] as Text).textContent = `${lView[i-3]}`; }',
]),
],
remove: [
debugMatch(['(lView[0] as Element).remove(lView[29])']),
debugMatch(['(lView[0] as Element).remove(lView[30])'])
],
}));
fixture.apply(() => {
applyCreateOpCodes(fixture.lView, tI18n.create, fixture.host, null);
expect(fixture.host.innerHTML).toEqual('<!--ICU 0:0-->');
});
fixture.apply(() => {
ɵɵi18nExp('A');
ɵɵi18nExp('0');
ɵɵi18nExp('value1');
ɵɵi18nApply(0); // index 0 + HEADER_OFFSET = 20;
expect(fixture.host.innerHTML).toEqual('parentA nested0<!--nested ICU 0-->!<!--ICU 0:0-->');
});
fixture.apply(() => {
ɵɵi18nExp('A');
ɵɵi18nExp('x');
ɵɵi18nExp('value1');
ɵɵi18nApply(0); // index 0 + HEADER_OFFSET = 20;
expect(fixture.host.innerHTML).toEqual('parentA value1<!--nested ICU 0-->!<!--ICU 0:0-->');
});
fixture.apply(() => {
ɵɵi18nExp('x');
ɵɵi18nExp('x');
ɵɵi18nExp('value2');
ɵɵi18nApply(0); // index 0 + HEADER_OFFSET = 20;
expect(fixture.host.innerHTML).toEqual('parentOther<!--ICU 0:0-->');
});
fixture.apply(() => {
ɵɵi18nExp('A');
ɵɵi18nExp('A');
ɵɵi18nExp('value2');
ɵɵi18nApply(0); // index 0 + HEADER_OFFSET = 20;
expect(fixture.host.innerHTML).toEqual('parentA value2<!--nested ICU 0-->!<!--ICU 0:0-->');
});
});
});
function toT18n(text: string) {
const tNodeIndex = 0;
fixture.enterView();
i18nStartFirstCreatePass(fixture.tView, 0, fixture.lView, tNodeIndex, text, -1);
fixture.leaveView();
const tI18n = fixture.tView.data[tNodeIndex + HEADER_OFFSET] as TI18n;
expect(tI18n).toEqual(matchTI18n({}));
return tI18n;
}
});

View File

@ -7,41 +7,44 @@
*/
import {ɵɵi18nAttributes, ɵɵi18nPostprocess, ɵɵi18nStart} from '@angular/core';
import {getTranslationForTemplate} from '@angular/core/src/render3/i18n/i18n_parse';
import {setDelayProjection, ɵɵelementEnd, ɵɵelementStart} from '../../../src/render3/instructions/all';
import {I18nUpdateOpCodes, TI18n, TIcu} from '../../../src/render3/interfaces/i18n';
import {TConstants} from '../../../src/render3/interfaces/node';
import {HEADER_OFFSET, LView, TVIEW} from '../../../src/render3/interfaces/view';
import {ɵɵi18n} from '@angular/core/src/core';
import {getTranslationForTemplate, i18nStartFirstCreatePass} from '@angular/core/src/render3/i18n/i18n_parse';
import {getTIcu} from '@angular/core/src/render3/i18n/i18n_util';
import {TElementNode, TNodeType} from '@angular/core/src/render3/interfaces/node';
import {getCurrentTNode} from '@angular/core/src/render3/state';
import {ɵɵelementEnd, ɵɵelementStart} from '../../../src/render3/instructions/all';
import {I18nCreateOpCode, I18nUpdateOpCodes, TI18n, TIcu} from '../../../src/render3/interfaces/i18n';
import {HEADER_OFFSET, LView, TVIEW, TView} from '../../../src/render3/interfaces/view';
import {getNativeByIndex} from '../../../src/render3/util/view_utils';
import {matchTNode} from '../matchers';
import {TemplateFixture} from '../render_util';
import {debugMatch} from '../utils';
import {ViewFixture} from '../view_fixture';
describe('Runtime i18n', () => {
afterEach(() => {
setDelayProjection(false);
});
describe('getTranslationForTemplate', () => {
it('should crop messages for the selected template', () => {
let message = `simple text`;
expect(getTranslationForTemplate(message)).toEqual(message);
expect(getTranslationForTemplate(message, -1)).toEqual(message);
message = `Hello <20>0<EFBFBD>!`;
expect(getTranslationForTemplate(message)).toEqual(message);
expect(getTranslationForTemplate(message, -1)).toEqual(message);
message = `Hello <20>#2<><32>0<EFBFBD><30>/#2<>!`;
expect(getTranslationForTemplate(message)).toEqual(message);
expect(getTranslationForTemplate(message, -1)).toEqual(message);
// Embedded sub-templates
message = `<EFBFBD>0<EFBFBD> is rendered as: <20>*2:1<>before<72>*1:2<>middle<6C>/*1:2<>after<65>/*2:1<>!`;
expect(getTranslationForTemplate(message)).toEqual('<27>0<EFBFBD> is rendered as: <20>*2:1<><31>/*2:1<>!');
expect(getTranslationForTemplate(message, -1)).toEqual('<27>0<EFBFBD> is rendered as: <20>*2:1<><31>/*2:1<>!');
expect(getTranslationForTemplate(message, 1)).toEqual('before<72>*1:2<><32>/*1:2<>after');
expect(getTranslationForTemplate(message, 2)).toEqual('middle');
// Embedded & sibling sub-templates
message =
`<EFBFBD>0<EFBFBD> is rendered as: <20>*2:1<>before<72>*1:2<>middle<6C>/*1:2<>after<65>/*2:1<> and also <20>*4:3<>before<72>*1:4<>middle<6C>/*1:4<>after<65>/*4:3<>!`;
expect(getTranslationForTemplate(message))
expect(getTranslationForTemplate(message, -1))
.toEqual('<27>0<EFBFBD> is rendered as: <20>*2:1<><31>/*2:1<> and also <20>*4:3<><33>/*4:3<>!');
expect(getTranslationForTemplate(message, 1)).toEqual('before<72>*1:2<><32>/*1:2<>after');
expect(getTranslationForTemplate(message, 2)).toEqual('middle');
@ -51,20 +54,19 @@ describe('Runtime i18n', () => {
it('should throw if the template is malformed', () => {
const message = `<EFBFBD>*2:1<>message!`;
expect(() => getTranslationForTemplate(message)).toThrowError(/Tag mismatch/);
expect(() => getTranslationForTemplate(message, -1)).toThrowError(/Tag mismatch/);
});
});
let tView: TView;
function getOpCodes(
messageOrAtrs: string|string[], createTemplate: () => void, updateTemplate: (() => void)|null,
nbDecls: number, index: number): TI18n|I18nUpdateOpCodes {
const fixture = new TemplateFixture({
create: createTemplate,
update: updateTemplate || undefined,
decls: nbDecls,
consts: [messageOrAtrs]
});
const tView = fixture.hostView[TVIEW];
messageOrAtrs: string|string[], createTemplate: () => void,
updateTemplate: (() => void)|undefined, nbDecls: number, index: number): TI18n|
I18nUpdateOpCodes {
const fixture = new TemplateFixture(
{create: createTemplate, update: updateTemplate, decls: nbDecls, consts: [messageOrAtrs]});
tView = fixture.hostView[TVIEW];
return tView.data[index + HEADER_OFFSET] as TI18n;
}
@ -72,19 +74,19 @@ describe('Runtime i18n', () => {
it('for text', () => {
const message = 'simple text';
const nbConsts = 1;
const index = 0;
const index = 1;
const opCodes = getOpCodes(message, () => {
ɵɵelementStart(0, 'div');
ɵɵi18nStart(index, 0);
}, null, nbConsts, index) as TI18n;
ɵɵelementEnd();
}, undefined, nbConsts, index) as TI18n;
expect(opCodes).toEqual({
vars: 1,
create: debugMatch([
'lView[1] = document.createTextNode("simple text")',
'(lView[0] as Element).appendChild(lView[1])'
`lView[${HEADER_OFFSET + 1}] = document.createText("simple text");`,
`parent.appendChild(lView[${HEADER_OFFSET + 1}]);`,
]),
update: [],
icus: null
});
});
@ -95,29 +97,23 @@ describe('Runtime i18n', () => {
const nbConsts = 4;
const index = 1;
const opCodes = getOpCodes(message, () => {
ɵɵelementStart(0, 'div');
ɵɵi18nStart(index, 0);
}, null, nbConsts, index);
ɵɵelementEnd();
}, undefined, nbConsts, index);
expect(opCodes).toEqual({
vars: 5,
create: debugMatch([
'lView[4] = document.createTextNode("Hello ")',
'(lView[1] as Element).appendChild(lView[4])',
'(lView[1] as Element).appendChild(lView[2])',
'lView[5] = document.createTextNode("world")',
'(lView[2] as Element).appendChild(lView[5])',
'setCurrentTNode(tView.data[2] as TNode)',
'lView[6] = document.createTextNode(" and ")',
'(lView[1] as Element).appendChild(lView[6])',
'(lView[1] as Element).appendChild(lView[3])',
'lView[7] = document.createTextNode("universe")',
'(lView[3] as Element).appendChild(lView[7])',
'setCurrentTNode(tView.data[3] as TNode)',
'lView[8] = document.createTextNode("!")',
'(lView[1] as Element).appendChild(lView[8])',
`lView[${HEADER_OFFSET + 4}] = document.createText("Hello ");`,
`parent.appendChild(lView[${HEADER_OFFSET + 4}]);`,
`lView[${HEADER_OFFSET + 5}] = document.createText("world");`,
`lView[${HEADER_OFFSET + 6}] = document.createText(" and ");`,
`parent.appendChild(lView[${HEADER_OFFSET + 6}]);`,
`lView[${HEADER_OFFSET + 7}] = document.createText("universe");`,
`lView[${HEADER_OFFSET + 8}] = document.createText("!");`,
`parent.appendChild(lView[${HEADER_OFFSET + 8}]);`,
]),
update: [],
icus: null
});
});
@ -126,22 +122,23 @@ describe('Runtime i18n', () => {
const nbConsts = 2;
const index = 1;
const opCodes = getOpCodes(message, () => {
ɵɵelementStart(0, 'div');
ɵɵi18nStart(index, 0);
}, null, nbConsts, index);
ɵɵelementEnd();
}, undefined, nbConsts, index);
expect((opCodes as any).update.debug).toEqual([
'if (mask & 0b1) { (lView[2] as Text).textContent = `Hello ${lView[1]}!`; }'
'if (mask & 0b1) { (lView[22] as Text).textContent = `Hello ${lView[i-1]}!`; }'
]);
expect(opCodes).toEqual({
vars: 1,
create: debugMatch([
'lView[2] = document.createTextNode("")',
'(lView[1] as Element).appendChild(lView[2])',
`lView[${HEADER_OFFSET + 2}] = document.createText("");`,
`parent.appendChild(lView[${HEADER_OFFSET + 2}]);`,
]),
update: debugMatch([
'if (mask & 0b1) { (lView[22] as Text).textContent = `Hello ${lView[i-1]}!`; }',
]),
update: debugMatch(
['if (mask & 0b1) { (lView[2] as Text).textContent = `Hello ${lView[1]}!`; }']),
icus: null
});
});
@ -150,18 +147,19 @@ describe('Runtime i18n', () => {
const nbConsts = 2;
const index = 1;
const opCodes = getOpCodes(message, () => {
ɵɵelementStart(0, 'div');
ɵɵi18nStart(index, 0);
}, null, nbConsts, index);
ɵɵelementEnd();
}, undefined, nbConsts, index);
expect(opCodes).toEqual({
vars: 1,
create: debugMatch([
'lView[2] = document.createTextNode("")', '(lView[1] as Element).appendChild(lView[2])'
`lView[${HEADER_OFFSET + 2}] = document.createText("");`,
`parent.appendChild(lView[${HEADER_OFFSET + 2}]);`,
]),
update: debugMatch([
'if (mask & 0b11) { (lView[2] as Text).textContent = `Hello ${lView[1]} and ${lView[2]}, again ${lView[1]}!`; }'
'if (mask & 0b11) { (lView[22] as Text).textContent = `Hello ${lView[i-1]} and ${lView[i-2]}, again ${lView[i-1]}!`; }',
]),
icus: null
});
});
@ -182,63 +180,56 @@ describe('Runtime i18n', () => {
let nbConsts = 3;
let index = 1;
let opCodes = getOpCodes(message, () => {
ɵɵelementStart(0, 'div');
ɵɵi18nStart(index, 0);
}, null, nbConsts, index);
ɵɵelementEnd();
}, undefined, nbConsts, index);
expect(opCodes).toEqual({
vars: 2,
create: debugMatch([
'lView[3] = document.createTextNode("")', '(lView[1] as Element).appendChild(lView[3])',
'(lView[1] as Element).appendChild(lView[16381])',
'lView[4] = document.createTextNode("!")', '(lView[1] as Element).appendChild(lView[4])'
`lView[${HEADER_OFFSET + 3}] = document.createText("");`,
`parent.appendChild(lView[${HEADER_OFFSET + 3}]);`,
`lView[${HEADER_OFFSET + 4}] = document.createText("!");`,
`parent.appendChild(lView[${HEADER_OFFSET + 4}]);`,
]),
update: debugMatch([
'if (mask & 0b1) { (lView[3] as Text).textContent = `${lView[1]} is rendered as: `; }'
'if (mask & 0b1) { (lView[23] as Text).textContent = `${lView[i-1]} is rendered as: `; }',
]),
icus: null
});
/**** First sub-template ****/
// <20>#1:1<>before<72>*2:2<>middle<6C>/*2:2<>after<65>/#1:1<>
nbConsts = 3;
index = 0;
index = 1;
opCodes = getOpCodes(message, () => {
ɵɵelementStart(0, 'div');
ɵɵi18nStart(index, 0, 1);
}, null, nbConsts, index);
}, undefined, nbConsts, index);
expect(opCodes).toEqual({
vars: 2,
create: debugMatch([
'(lView[0] as Element).appendChild(lView[1])',
'lView[3] = document.createTextNode("before")',
'(lView[1] as Element).appendChild(lView[3])',
'(lView[1] as Element).appendChild(lView[16381])',
'lView[4] = document.createTextNode("after")',
'(lView[1] as Element).appendChild(lView[4])', 'setCurrentTNode(tView.data[1] as TNode)'
`lView[${HEADER_OFFSET + 3}] = document.createText("before");`,
`lView[${HEADER_OFFSET + 4}] = document.createText("after");`,
]),
update: [],
icus: null
});
/**** Second sub-template ****/
// middle
nbConsts = 2;
index = 0;
index = 1;
opCodes = getOpCodes(message, () => {
ɵɵelementStart(0, 'div');
ɵɵi18nStart(index, 0, 2);
}, null, nbConsts, index);
}, undefined, nbConsts, index);
expect(opCodes).toEqual({
vars: 1,
create: debugMatch([
'(lView[0] as Element).appendChild(lView[1])',
'lView[2] = document.createTextNode("middle")',
'(lView[1] as Element).appendChild(lView[2])', 'setCurrentTNode(tView.data[1] as TNode)'
`lView[${HEADER_OFFSET + 2}] = document.createText("middle");`,
]),
update: [],
icus: null
});
});
@ -248,82 +239,81 @@ describe('Runtime i18n', () => {
=1 {one <i>email</i>}
other {<EFBFBD>0<EFBFBD> <span title="<22>1<EFBFBD>">emails</span>}
}`;
const nbConsts = 1;
const index = 0;
const nbConsts = 2;
const index = 1;
const opCodes = getOpCodes(message, () => {
ɵɵelementStart(0, 'div');
ɵɵi18nStart(index, 0);
}, null, nbConsts, index) as TI18n;
ɵɵelementEnd();
}, undefined, nbConsts, index) as TI18n;
expect(opCodes).toEqual({
vars: 6,
update: debugMatch([
'if (mask & 0b1) { icuSwitchCase(lView[1] as Comment, 0, `${lView[1]}`); }',
'if (mask & 0b11) { icuUpdateCase(lView[1] as Comment, 0); }',
]),
create: debugMatch([
'lView[1] = document.createComment("ICU 1")',
'(lView[0] as Element).appendChild(lView[1])',
`lView[${HEADER_OFFSET + 2}] = document.createComment("ICU 1:0");`,
`parent.appendChild(lView[${HEADER_OFFSET + 2}]);`,
]),
icus: [<TIcu>{
type: 1,
currentCaseLViewIndex: 22,
vars: [5, 4, 4],
childIcus: [[], [], []],
cases: ['0', '1', 'other'],
create: [
debugMatch([
'lView[3] = document.createTextNode("no ")',
'(lView[1] as Element).appendChild(lView[3])',
'lView[4] = document.createElement("b")',
'(lView[1] as Element).appendChild(lView[4])',
'(lView[4] as Element).setAttribute("title", "none")',
'lView[5] = document.createTextNode("emails")',
'(lView[4] as Element).appendChild(lView[5])',
'lView[6] = document.createTextNode("!")',
'(lView[1] as Element).appendChild(lView[6])',
]),
debugMatch([
'lView[3] = document.createTextNode("one ")',
'(lView[1] as Element).appendChild(lView[3])',
'lView[4] = document.createElement("i")',
'(lView[1] as Element).appendChild(lView[4])',
'lView[5] = document.createTextNode("email")',
'(lView[4] as Element).appendChild(lView[5])',
]),
debugMatch([
'lView[3] = document.createTextNode("")',
'(lView[1] as Element).appendChild(lView[3])',
'lView[4] = document.createElement("span")',
'(lView[1] as Element).appendChild(lView[4])',
'lView[5] = document.createTextNode("emails")',
'(lView[4] as Element).appendChild(lView[5])',
])
],
remove: [
debugMatch([
'(lView[0] as Element).remove(lView[3])',
'(lView[0] as Element).remove(lView[5])',
'(lView[0] as Element).remove(lView[4])',
'(lView[0] as Element).remove(lView[6])',
]),
debugMatch([
'(lView[0] as Element).remove(lView[3])',
'(lView[0] as Element).remove(lView[5])',
'(lView[0] as Element).remove(lView[4])',
]),
debugMatch([
'(lView[0] as Element).remove(lView[3])',
'(lView[0] as Element).remove(lView[5])',
'(lView[0] as Element).remove(lView[4])',
])
],
update: [
debugMatch([]), debugMatch([]), debugMatch([
'if (mask & 0b1) { (lView[3] as Text).textContent = `${lView[1]} `; }',
'if (mask & 0b10) { (lView[4] as Element).setAttribute(\'title\', `${lView[2]}`); }'
])
]
}]
update: debugMatch([
'if (mask & 0b1) { icuSwitchCase(22, `${lView[i-1]}`); }',
'if (mask & 0b1) { icuUpdateCase(22); }',
]),
});
expect(getTIcu(tView, 22)).toEqual(<TIcu>{
type: 1,
currentCaseLViewIndex: 23,
anchorIdx: 22,
cases: ['0', '1', 'other'],
create: [
debugMatch([
`lView[${HEADER_OFFSET + 4}] = document.createTextNode("no ")`,
`(lView[${HEADER_OFFSET + 0}] as Element).appendChild(lView[${HEADER_OFFSET + 4}])`,
'lView[25] = document.createElement("b")',
`(lView[${HEADER_OFFSET + 0}] as Element).appendChild(lView[${HEADER_OFFSET + 5}])`,
'(lView[25] as Element).setAttribute("title", "none")',
`lView[${HEADER_OFFSET + 6}] = document.createTextNode("emails")`,
`(lView[${HEADER_OFFSET + 5}] as Element).appendChild(lView[${HEADER_OFFSET + 6}])`,
`lView[${HEADER_OFFSET + 7}] = document.createTextNode("!")`,
`(lView[${HEADER_OFFSET + 0}] as Element).appendChild(lView[${HEADER_OFFSET + 7}])`,
]),
debugMatch([
`lView[${HEADER_OFFSET + 8}] = document.createTextNode("one ")`,
`(lView[${HEADER_OFFSET + 0}] as Element).appendChild(lView[${HEADER_OFFSET + 8}])`,
'lView[29] = document.createElement("i")',
`(lView[${HEADER_OFFSET + 0}] as Element).appendChild(lView[${HEADER_OFFSET + 9}])`,
'lView[30] = document.createTextNode("email")',
'(lView[29] as Element).appendChild(lView[30])',
]),
debugMatch([
'lView[31] = document.createTextNode("")',
'(lView[20] as Element).appendChild(lView[31])',
'lView[32] = document.createElement("span")',
'(lView[20] as Element).appendChild(lView[32])',
'lView[33] = document.createTextNode("emails")',
'(lView[32] as Element).appendChild(lView[33])',
]),
],
remove: [
debugMatch([
'(lView[0] as Element).remove(lView[24])',
'(lView[0] as Element).remove(lView[25])',
'(lView[0] as Element).remove(lView[27])',
]),
debugMatch([
'(lView[0] as Element).remove(lView[28])',
'(lView[0] as Element).remove(lView[29])',
]),
debugMatch([
'(lView[0] as Element).remove(lView[31])',
'(lView[0] as Element).remove(lView[32])',
]),
],
update: [
debugMatch([]),
debugMatch([]),
debugMatch([
'if (mask & 0b1) { (lView[31] as Text).textContent = `${lView[i-1]} `; }',
'if (mask & 0b10) { (lView[32] as Element).setAttribute(\'title\', `${lView[i-2]}`); }',
]),
]
});
});
@ -336,91 +326,91 @@ describe('Runtime i18n', () => {
other {animals}
}!}
}`;
const nbConsts = 1;
const index = 0;
const nbConsts = 2;
const index = 1;
const opCodes = getOpCodes(message, () => {
ɵɵi18nStart(index, 0);
}, null, nbConsts, index);
ɵɵelementStart(0, 'div');
ɵɵi18n(index, 0);
ɵɵelementEnd();
}, undefined, nbConsts, index);
expect(opCodes).toEqual({
vars: 9,
create: debugMatch([
'lView[1] = document.createComment("ICU 1")',
'(lView[0] as Element).appendChild(lView[1])'
`lView[${HEADER_OFFSET + 2}] = document.createComment("ICU 1:0");`,
`parent.appendChild(lView[${HEADER_OFFSET + 2}]);`,
]),
update: debugMatch([
'if (mask & 0b1) { icuSwitchCase(lView[1] as Comment, 1, `${lView[1]}`); }',
'if (mask & 0b11) { icuUpdateCase(lView[1] as Comment, 1); }'
'if (mask & 0b1) { icuSwitchCase(22, `${lView[i-1]}`); }',
'if (mask & 0b10) { icuSwitchCase(26, `${lView[i-2]}`); }',
'if (mask & 0b1) { icuUpdateCase(22); }',
]),
icus: [
{
type: 0,
vars: [2, 2, 2],
currentCaseLViewIndex: 26,
childIcus: [[], [], []],
cases: ['cat', 'dog', 'other'],
create: [
debugMatch([
'lView[7] = document.createTextNode("cats")',
'(lView[4] as Element).appendChild(lView[7])'
]),
debugMatch([
'lView[7] = document.createTextNode("dogs")',
'(lView[4] as Element).appendChild(lView[7])'
]),
debugMatch([
'lView[7] = document.createTextNode("animals")',
'(lView[4] as Element).appendChild(lView[7])'
]),
],
remove: [
debugMatch(['(lView[0] as Element).remove(lView[7])']),
debugMatch(['(lView[0] as Element).remove(lView[7])']),
debugMatch(['(lView[0] as Element).remove(lView[7])'])
],
update: [
debugMatch([]),
debugMatch([]),
debugMatch([]),
]
},
{
type: 1,
vars: [2, 6],
childIcus: [[], [0]],
currentCaseLViewIndex: 22,
cases: ['0', 'other'],
create: [
debugMatch([
'lView[3] = document.createTextNode("zero")',
'(lView[1] as Element).appendChild(lView[3])'
]),
debugMatch([
'lView[3] = document.createTextNode("")',
'(lView[1] as Element).appendChild(lView[3])',
'lView[4] = document.createComment("nested ICU 0")',
'(lView[1] as Element).appendChild(lView[4])',
'lView[5] = document.createTextNode("!")',
'(lView[1] as Element).appendChild(lView[5])'
]),
],
remove: [
debugMatch(['(lView[0] as Element).remove(lView[3])']),
debugMatch([
'(lView[0] as Element).remove(lView[3])', '(lView[0] as Element).remove(lView[5])',
'removeNestedICU(0)', '(lView[0] as Element).remove(lView[4])'
]),
],
update: [
debugMatch([]),
debugMatch([
'if (mask & 0b1) { (lView[3] as Text).textContent = `${lView[1]} `; }',
'if (mask & 0b10) { icuSwitchCase(lView[4] as Comment, 0, `${lView[2]}`); }',
'if (mask & 0b10) { icuUpdateCase(lView[4] as Comment, 0); }'
]),
]
}
]
});
expect(getTIcu(tView, 22)).toEqual({
type: 1,
anchorIdx: 22,
currentCaseLViewIndex: 23,
cases: ['0', 'other'],
create: [
debugMatch([
`lView[${HEADER_OFFSET + 4}] = document.createTextNode("zero")`,
`(lView[${HEADER_OFFSET + 0}] as Element).appendChild(lView[${HEADER_OFFSET + 4}])`,
]),
debugMatch([
`lView[${HEADER_OFFSET + 5}] = document.createTextNode("")`,
`(lView[${HEADER_OFFSET + 0}] as Element).appendChild(lView[${HEADER_OFFSET + 5}])`,
'lView[26] = document.createComment("nested ICU 0")',
`(lView[${HEADER_OFFSET + 0}] as Element).appendChild(lView[${HEADER_OFFSET + 6}])`,
'lView[31] = document.createTextNode("!")',
'(lView[20] as Element).appendChild(lView[31])',
]),
],
update: [
debugMatch([]),
debugMatch([
'if (mask & 0b1) { (lView[25] as Text).textContent = `${lView[i-1]} `; }',
]),
],
remove: [
debugMatch([
'(lView[0] as Element).remove(lView[24])',
]),
debugMatch([
'(lView[0] as Element).remove(lView[25])',
'removeNestedICU(26)',
'(lView[0] as Element).remove(lView[26])',
'(lView[0] as Element).remove(lView[31])',
]),
],
});
expect(tView.data[26]).toEqual({
type: 0,
anchorIdx: 26,
currentCaseLViewIndex: 27,
cases: ['cat', 'dog', 'other'],
create: [
debugMatch([
`lView[${HEADER_OFFSET + 8}] = document.createTextNode("cats")`,
`(lView[${HEADER_OFFSET + 0}] as Element).appendChild(lView[${HEADER_OFFSET + 8}])`,
]),
debugMatch([
`lView[${HEADER_OFFSET + 9}] = document.createTextNode("dogs")`,
`(lView[${HEADER_OFFSET + 0}] as Element).appendChild(lView[${HEADER_OFFSET + 9}])`,
]),
debugMatch([
'lView[30] = document.createTextNode("animals")',
'(lView[20] as Element).appendChild(lView[30])',
]),
],
update: [
debugMatch([]),
debugMatch([]),
debugMatch([]),
],
remove: [
debugMatch(['(lView[0] as Element).remove(lView[28])']),
debugMatch(['(lView[0] as Element).remove(lView[29])']),
debugMatch(['(lView[0] as Element).remove(lView[30])'])
],
});
});
});
@ -459,10 +449,10 @@ describe('Runtime i18n', () => {
ɵɵelementStart(0, 'div');
ɵɵi18nAttributes(index, 0);
ɵɵelementEnd();
}, null, nbConsts, index);
}, undefined, nbConsts, index);
expect(opCodes).toEqual(debugMatch([
'if (mask & 0b1) { (lView[0] as Element).setAttribute(\'title\', `Hello ${lView[1]}!`); }'
'if (mask & 0b1) { (lView[20] as Element).setAttribute(\'title\', `Hello ${lView[i-1]}!`); }',
]));
});
@ -475,10 +465,10 @@ describe('Runtime i18n', () => {
ɵɵelementStart(0, 'div');
ɵɵi18nAttributes(index, 0);
ɵɵelementEnd();
}, null, nbConsts, index);
}, undefined, nbConsts, index);
expect(opCodes).toEqual(debugMatch([
'if (mask & 0b11) { (lView[0] as Element).setAttribute(\'title\', `Hello ${lView[1]} and ${lView[2]}, again ${lView[1]}!`); }'
'if (mask & 0b11) { (lView[20] as Element).setAttribute(\'title\', `Hello ${lView[i-1]} and ${lView[i-2]}, again ${lView[i-1]}!`); }',
]));
});
@ -491,11 +481,11 @@ describe('Runtime i18n', () => {
ɵɵelementStart(0, 'div');
ɵɵi18nAttributes(index, 0);
ɵɵelementEnd();
}, null, nbConsts, index);
}, undefined, nbConsts, index);
expect(opCodes).toEqual(debugMatch([
'if (mask & 0b1) { (lView[0] as Element).setAttribute(\'title\', `Hello ${lView[1]}!`); }',
'if (mask & 0b1) { (lView[0] as Element).setAttribute(\'aria-label\', `Hello ${lView[1]}!`); }'
'if (mask & 0b1) { (lView[20] as Element).setAttribute(\'title\', `Hello ${lView[i-1]}!`); }',
'if (mask & 0b1) { (lView[20] as Element).setAttribute(\'aria-label\', `Hello ${lView[i-1]}!`); }',
]));
});
});
@ -638,4 +628,125 @@ describe('Runtime i18n', () => {
.toThrowError();
});
});
});
describe('i18nStartFirstCreatePass', () => {
let fixture: ViewFixture;
let divTNode: TElementNode;
const DECLS = 20;
const VARS = 10;
beforeEach(() => {
fixture = new ViewFixture({decls: DECLS, vars: VARS});
fixture.enterView();
ɵɵelementStart(0, 'div');
divTNode = getCurrentTNode() as TElementNode;
});
afterEach(ViewFixture.cleanUp);
function i18nRangeOffset(offset: number): number {
return HEADER_OFFSET + DECLS + VARS + offset;
}
function i18nRangeOffsetOpcode(
offset: number,
{appendLater, comment}: {appendLater?: boolean, comment?: boolean} = {}): number {
let index = i18nRangeOffset(offset) << I18nCreateOpCode.SHIFT;
if (!appendLater) {
index |= I18nCreateOpCode.APPEND_EAGERLY;
}
if (comment) {
index |= I18nCreateOpCode.COMMENT;
}
return index;
}
it('should process text node with no siblings and no children', () => {
i18nStartFirstCreatePass(fixture.tView, 0, fixture.lView, 1, 'Hello World!', -1);
const ti18n = fixture.tView.data[HEADER_OFFSET + 1] as TI18n;
// Expect that we only create the `Hello World!` text node and nothing else.
expect(ti18n.create).toEqual([
i18nRangeOffsetOpcode(0), 'Hello World!', //
]);
const lViewDebug = fixture.lView.debug!;
expect(lViewDebug.template).toEqual('<div>#text</div>');
});
it('should process text with a child node', () => {
i18nStartFirstCreatePass(fixture.tView, 0, fixture.lView, 1, 'Hello <20>#2<><32>/#2<>!', -1);
const ti18n = fixture.tView.data[HEADER_OFFSET + 1] as TI18n;
expect(ti18n.create).toEqual([
i18nRangeOffsetOpcode(0), 'Hello ', //
i18nRangeOffsetOpcode(1), '!', //
]);
// Leave behind `Placeholder` to be picked up by `TNode` creation.
expect(fixture.tView.data[HEADER_OFFSET + 2]).toEqual(matchTNode({
type: TNodeType.Placeholder,
// It should insert itself in front of "!"
insertBeforeIndex: i18nRangeOffset(1),
}));
const lViewDebug = fixture.lView.debug!;
expect(lViewDebug.template).toEqual('<div>#text<Placeholder></Placeholder>#text</div>');
});
it('should process text with a child node that has text', () => {
i18nStartFirstCreatePass(fixture.tView, 0, fixture.lView, 1, 'Hello <20>#2<>World<6C>/#2<>!', -1);
const ti18n = fixture.tView.data[HEADER_OFFSET + 1] as TI18n;
expect(ti18n.create).toEqual([
i18nRangeOffsetOpcode(0), 'Hello ', //
i18nRangeOffsetOpcode(1, {appendLater: true}), 'World', //
i18nRangeOffsetOpcode(2), '!', //
]);
// Leave behind `Placeholder` to be picked up by `TNode` creation.
expect(fixture.tView.data[HEADER_OFFSET + 2]).toEqual(matchTNode({
type: TNodeType.Placeholder,
insertBeforeIndex: [
i18nRangeOffset(2), // It should insert itself in front of "!"
i18nRangeOffset(1), // It should append "World"
]
}));
});
it('should process text with a child node that has text and with bindings', () => {
i18nStartFirstCreatePass(
fixture.tView, 0, fixture.lView, 1,
'<27>0<EFBFBD> <20>#2<><32>1<EFBFBD><31>/#2<>!' /* {{salutation}} <b>{{name}}</b>! */, -1);
const ti18n = fixture.tView.data[HEADER_OFFSET + 1] as TI18n;
expect(ti18n.create).toEqual([
i18nRangeOffsetOpcode(0), '', // 1 is saved for binding
i18nRangeOffsetOpcode(1, {appendLater: true}), '', // 3 is saved for binding
i18nRangeOffsetOpcode(2), '!', //
]);
// Leave behind `insertBeforeIndex` to be picked up by `TNode` creation.
expect(fixture.tView.data[HEADER_OFFSET + 2]).toEqual(matchTNode({
type: TNodeType.Placeholder,
insertBeforeIndex: [
i18nRangeOffset(2), // It should insert itself in front of "!"
i18nRangeOffset(1), // It should append child text node "{{name}}"
],
}));
expect(ti18n.update).toEqual(debugMatch([
'if (mask & 0b1) { (lView[50] as Text).textContent = `${lView[i-1]} `; }',
'if (mask & 0b10) { (lView[51] as Text).textContent = `${lView[i-2]}`; }'
]));
const lViewDebug = fixture.lView.debug!;
expect(lViewDebug.template).toEqual('<div>#text<Placeholder>#text</Placeholder>#text</div>');
});
it('should process text with a child template', () => {
i18nStartFirstCreatePass(fixture.tView, 0, fixture.lView, 1, 'Hello <20>*2:1<>World<6C>/*2:1<>!', -1);
const ti18n = fixture.tView.data[HEADER_OFFSET + 1] as TI18n;
expect(ti18n.create.debug).toEqual([
'lView[50] = document.createText("Hello ");',
'parent.appendChild(lView[50]);',
'lView[51] = document.createText("!");',
'parent.appendChild(lView[51]);',
]);
// Leave behind `Placeholder` to be picked up by `TNode` creation.
// It should insert itself in front of "!"
expect(fixture.tView.data[HEADER_OFFSET + 2]).toEqual(matchTNode({
type: TNodeType.Placeholder,
insertBeforeIndex: 51,
}));
});
});
});

View File

@ -6,8 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
import {i18nMutateOpCodesToString, i18nUpdateOpCodesToString} from '@angular/core/src/render3/i18n/i18n_debug';
import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nUpdateOpCode} from '@angular/core/src/render3/interfaces/i18n';
import {i18nCreateOpCodesToString, i18nMutateOpCodesToString, i18nUpdateOpCodesToString} from '@angular/core/src/render3/i18n/i18n_debug';
import {COMMENT_MARKER, ELEMENT_MARKER, I18nCreateOpCode, I18nMutateOpCode, I18nUpdateOpCode} from '@angular/core/src/render3/interfaces/i18n';
describe('i18n debug', () => {
describe('i18nUpdateOpCodesToString', () => {
@ -25,7 +25,7 @@ describe('i18n debug', () => {
1 << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Text,
]))
.toEqual(
['if (mask & 0b11) { (lView[1] as Text).textContent = `pre ${lView[4]} post`; }']);
['if (mask & 0b11) { (lView[1] as Text).textContent = `pre ${lView[i-4]} post`; }']);
});
it('should print Attribute opCode', () => {
@ -42,23 +42,21 @@ describe('i18n debug', () => {
'title', (v) => v,
]))
.toEqual([
'if (mask & 0b1) { (lView[1] as Element).setAttribute(\'title\', `pre ${lView[4]} in ${lView[3]} post`); }',
'if (mask & 0b10) { (lView[1] as Element).setAttribute(\'title\', (function (v) { return v; })(`pre ${lView[4]} in ${lView[3]} post`)); }'
'if (mask & 0b1) { (lView[1] as Element).setAttribute(\'title\', `pre ${lView[i-4]} in ${lView[i-3]} post`); }',
'if (mask & 0b10) { (lView[1] as Element).setAttribute(\'title\', (function (v) { return v; })(`pre ${lView[i-4]} in ${lView[i-3]} post`)); }'
]);
});
it('should print icuSwitch opCode', () => {
expect(i18nUpdateOpCodesToString([
0b100, 2, -5, 12 << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.IcuSwitch,
2 // FIXME(misko): Should be part of IcuSwitch
])).toEqual(['if (mask & 0b100) { icuSwitchCase(lView[12] as Comment, 2, `${lView[5]}`); }']);
0b100, 2, -5, 12 << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.IcuSwitch
])).toEqual(['if (mask & 0b100) { icuSwitchCase(12, `${lView[i-5]}`); }']);
});
it('should print icuUpdate opCode', () => {
expect(i18nUpdateOpCodesToString([
0b1000, 2, 13 << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.IcuUpdate,
3 // FIXME(misko): should be part of IcuUpdate
])).toEqual(['if (mask & 0b1000) { icuUpdateCase(lView[13] as Comment, 3); }']);
0b1000, 1, 13 << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.IcuUpdate
])).toEqual(['if (mask & 0b1000) { icuUpdateCase(13); }']);
});
});
@ -67,14 +65,6 @@ describe('i18n debug', () => {
expect(i18nMutateOpCodesToString([])).toEqual([]);
});
it('should print Move', () => {
expect(i18nMutateOpCodesToString([
1 << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select,
2 << I18nMutateOpCode.SHIFT_PARENT | 0 << I18nMutateOpCode.SHIFT_REF |
I18nMutateOpCode.AppendChild,
])).toEqual(['(lView[2] as Element).appendChild(lView[1])']);
});
it('should print text AppendChild', () => {
expect(i18nMutateOpCodesToString([
'xyz', 0,
@ -125,16 +115,34 @@ describe('i18n debug', () => {
])).toEqual(['(lView[1] as Element).setAttribute("attr", "value")']);
});
it('should print ElementEnd', () => {
expect(i18nMutateOpCodesToString([
1 << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.ElementEnd,
])).toEqual(['setCurrentTNode(tView.data[1] as TNode)']);
});
it('should print RemoveNestedIcu', () => {
expect(i18nMutateOpCodesToString([
1 << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.RemoveNestedIcu,
])).toEqual(['removeNestedICU(1)']);
});
});
describe('i18nCreateOpCodesToString', () => {
it('should print nothing', () => {
expect(i18nCreateOpCodesToString([])).toEqual([]);
});
it('should print text/comment creation', () => {
expect(i18nCreateOpCodesToString([
10 << I18nCreateOpCode.SHIFT, 'text at 10', //
11 << I18nCreateOpCode.SHIFT | I18nCreateOpCode.APPEND_EAGERLY, 'text at 11, append', //
12 << I18nCreateOpCode.SHIFT | I18nCreateOpCode.COMMENT, 'comment at 12', //
13 << I18nCreateOpCode.SHIFT | I18nCreateOpCode.COMMENT | I18nCreateOpCode.APPEND_EAGERLY,
'comment at 13, append', //
]))
.toEqual([
'lView[10] = document.createText("text at 10");',
'lView[11] = document.createText("text at 11, append");',
'parent.appendChild(lView[11]);',
'lView[12] = document.createComment("comment at 12");',
'lView[13] = document.createComment("comment at 13, append");',
'parent.appendChild(lView[13]);',
]);
});
});
});

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {TI18n} from '@angular/core/src/render3/interfaces/i18n';
import {TI18n, TIcu} from '@angular/core/src/render3/interfaces/i18n';
import {TNode} from '@angular/core/src/render3/interfaces/node';
import {TView} from '@angular/core/src/render3/interfaces/view';
@ -75,10 +75,26 @@ export function isTI18n(obj: any): obj is TI18n {
return isShapeOf<TI18n>(obj, ShapeOfTI18n);
}
const ShapeOfTI18n: ShapeOf<TI18n> = {
vars: true,
create: true,
update: true,
icus: true,
};
/**
* Determines if `obj` matches the shape `TIcu`.
* @param obj
*/
export function isTIcu(obj: any): obj is TIcu {
return isShapeOf<TIcu>(obj, ShapeOfTIcu);
}
const ShapeOfTIcu: ShapeOf<TIcu> = {
type: true,
anchorIdx: true,
currentCaseLViewIndex: true,
cases: true,
create: true,
remove: true,
update: true
};
@ -133,6 +149,7 @@ export function isTNode(obj: any): obj is TNode {
const ShapeOfTNode: ShapeOf<TNode> = {
type: true,
index: true,
insertBeforeIndex: true,
injectorIndex: true,
directiveStart: true,
directiveEnd: true,

View File

@ -6,11 +6,11 @@
* found in the LICENSE file at https://angular.io/license
*/
import {TI18n} from '@angular/core/src/render3/interfaces/i18n';
import {I18nDebug, I18nMutateOpCodes, TI18n, TIcu} from '@angular/core/src/render3/interfaces/i18n';
import {TNode} from '@angular/core/src/render3/interfaces/node';
import {TView} from '@angular/core/src/render3/interfaces/view';
import {isDOMElement, isDOMText, isTI18n, isTNode, isTView} from './is_shape_of';
import {isDOMElement, isDOMText, isTI18n, isTIcu, isTNode, isTView} from './is_shape_of';
/**
@ -37,22 +37,17 @@ export function matchObjectShape<T>(
return true;
};
matcher.jasmineToString = function() {
return `${toString(_actual, false)} != ${toString(expected, true)})`;
let errors: string[] = [];
if (!_actual || typeof _actual !== 'object') {
return `Expecting ${jasmine.pp(expect)} got ${jasmine.pp(_actual)}`;
}
for (const key in expected) {
if (expected.hasOwnProperty(key) && !jasmine.matchersUtil.equals(_actual[key], expected[key]))
errors.push(`\n property obj.${key} to equal ${expected[key]} but got ${_actual[key]}`);
}
return errors.join('\n');
};
function toString(obj: any, isExpected: boolean) {
if (isExpected || shapePredicate(obj)) {
const props =
Object.keys(expected).map(key => `${key}: ${JSON.stringify((obj as any)[key])}`);
if (isExpected === false) {
// Push something to let the user know that there may be other ignored properties in actual
props.push('...');
}
return `${name}({${props.length === 0 ? '' : '\n ' + props.join(',\n ') + '\n'}})`;
} else {
return JSON.stringify(obj);
}
}
return matcher;
}
@ -116,6 +111,26 @@ export function matchTI18n(expected?: Partial<TI18n>): jasmine.AsymmetricMatcher
}
/**
* Asymmetric matcher which matches a `T1cu` of a given shape.
*
* Expected usage:
* ```
* expect(tNode).toEqual(matchTIcu({type: TIcuType.select}));
* expect({
* type: TIcuType.select
* }).toEqual({
* node: matchT18n({type: TIcuType.select})
* });
* ```
*
* @param expected optional properties which the `TIcu` must contain.
*/
export function matchTIcu(expected?: Partial<TIcu>): jasmine.AsymmetricMatcher<TIcu> {
return matchObjectShape('TIcu', isTIcu, expected);
}
/**
* Asymmetric matcher which matches a DOM Element.
@ -214,5 +229,27 @@ export function matchDomText(expectedText: string|undefined = undefined):
return `[${actualStr} != ${expectedStr}]`;
};
return matcher;
}
export function matchI18nMutableOpCodes(expectedMutableOpCodes: string[]):
jasmine.AsymmetricMatcher<I18nMutateOpCodes> {
const matcher = function() {};
let _actual: any = null;
matcher.asymmetricMatch = function(actual: any) {
_actual = actual;
if (!Array.isArray(actual)) return false;
const debug = (actual as I18nDebug).debug as undefined | string[];
if (expectedMutableOpCodes && (!jasmine.matchersUtil.equals(debug, expectedMutableOpCodes))) {
return false;
}
return true;
};
matcher.jasmineToString = function() {
const debug = (_actual as I18nDebug).debug as undefined | string[];
return `[${JSON.stringify(debug)} != ${expectedMutableOpCodes}]`;
};
return matcher;
}

View File

@ -41,13 +41,8 @@ describe('render3 matchers', () => {
it('should produce human readable errors', () => {
const matcher = matchMyShape({propA: 'different'});
expect(matcher.asymmetricMatch(myShape, [])).toEqual(false);
expect(matcher.jasmineToString!()).toEqual(dedent`
MyShape({
propA: "value",
...
}) != MyShape({
propA: "different"
}))`);
expect(matcher.jasmineToString!())
.toEqual('\n property obj.propA to equal different but got value');
});
});

View File

@ -7,6 +7,7 @@
* found in the LICENSE file at https://angular.io/license
*/
/** Template string function that can be used to strip indentation from a given string literal. */
export function dedent(strings: TemplateStringsArray, ...values: any[]) {
let joinedString = '';
@ -59,15 +60,55 @@ function numOfWhiteSpaceLeadingChars(text: string): number {
*
* @param expected Expected value.
*/
// FIXME(misko): rename to `matchDebug` to be consistent with other API.
export function debugMatch<T>(expected: T): any {
const matcher = function() {};
let actual: any = null;
let actual: any = debugMatch;
matcher.asymmetricMatch = function(objectWithDebug: any) {
return jasmine.matchersUtil.equals(actual = objectWithDebug.debug, expected);
};
matcher.jasmineToString = function() {
return `<${JSON.stringify(actual)} != ${JSON.stringify(expected)}>`;
if (actual === debugMatch) {
// `asymmetricMatch` never got called hence no error to display
return '';
}
return buildFailureMessage(actual, expected);
};
return matcher;
}
export function buildFailureMessage(actual: any, expected: any): string {
const diffs: string[] = [];
listPropertyDifferences(diffs, '', actual, expected, 5);
return '\n ' + diffs.join('\n ');
}
function listPropertyDifferences(
diffs: string[], path: string, actual: any, expected: any, depth: number) {
if (actual === expected) return;
if (typeof actual !== typeof expected) {
diffs.push(`${path}: Expected ${jasmine.pp(actual)} to be ${jasmine.pp(expected)}`);
} else if (depth && Array.isArray(expected)) {
if (!Array.isArray(actual)) {
diffs.push(`${path}: Expected ${jasmine.pp(expected)} but was ${jasmine.pp(actual)}`);
} else {
const maxLength = Math.max(actual.length, expected.length);
listPropertyDifferences(diffs, path + '.length', expected.length, actual.length, depth - 1);
for (let i = 0; i < maxLength; i++) {
const actualItem = actual[i];
const expectedItem = expected[i];
listPropertyDifferences(diffs, path + '[' + i + ']', actualItem, expectedItem, depth - 1);
}
}
} else if (
depth && expected && typeof expected === 'object' && actual && typeof actual === 'object') {
new Set(Object.keys(expected).concat(Object.keys(actual))).forEach((key) => {
const actualItem = actual[key];
const expectedItem = expected[key];
listPropertyDifferences(diffs, path + '.' + key, actualItem, expectedItem, depth - 1);
});
} else {
diffs.push(`${path}: Expected ${jasmine.pp(actual)} to be ${jasmine.pp(expected)}`);
}
}

View File

@ -0,0 +1,84 @@
/**
* @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 {ComponentTemplate} from '@angular/core/src/render3';
import {createLView, createTNode, createTView} from '@angular/core/src/render3/instructions/shared';
import {TConstants, TElementNode, TNodeType} from '@angular/core/src/render3/interfaces/node';
import {domRendererFactory3} from '@angular/core/src/render3/interfaces/renderer';
import {LView, LViewFlags, T_HOST, TView, TViewType} from '@angular/core/src/render3/interfaces/view';
import {enterView, leaveView, specOnlyIsInstructionStateEmpty} from '@angular/core/src/render3/state';
import {noop} from '@angular/core/src/util/noop';
/**
* Fixture useful for testing operations which need `LView` / `TView`
*/
export class ViewFixture {
/**
* Clean up the `LFrame` stack between tests.
*/
static cleanUp() {
while (!specOnlyIsInstructionStateEmpty()) {
leaveView();
}
}
/**
* DOM element which acts as a host to the `LView`.
*/
host: HTMLElement;
tView: TView;
lView: LView;
constructor({template, decls, vars, consts, context}: {
decls?: number,
vars?: number,
template?: ComponentTemplate<any>,
consts?: TConstants,
context?: {}
} = {}) {
const hostRenderer = domRendererFactory3.createRenderer(null, null);
this.host = hostRenderer.createElement('host-element') as HTMLElement;
const hostTView = createTView(TViewType.Root, null, null, 1, 0, null, null, null, null, null);
const hostLView = createLView(
null, hostTView, {}, LViewFlags.CheckAlways | LViewFlags.IsRoot, null, null,
domRendererFactory3, hostRenderer, null, null);
this.tView = createTView(
TViewType.Component, null, template || noop, decls || 0, vars || 0, null, null, null, null,
consts || null);
const hostTNode =
createTNode(hostTView, null, TNodeType.Element, 0, 'host-element', null) as TElementNode;
this.lView = createLView(
hostLView, this.tView, context || {}, LViewFlags.CheckAlways, this.host, hostTNode,
domRendererFactory3, hostRenderer, null, null);
}
/**
* If you use `ViewFixture` and `enter()`, please add `afterEach(ViewFixture.cleanup);` to ensure
* that he global `LFrame` stack gets cleaned up between the tests.
*/
enterView() {
enterView(this.lView);
}
leaveView() {
leaveView();
}
apply(fn: () => void) {
this.enterView();
try {
fn();
} finally {
this.leaveView();
}
}
}

View File

@ -6,22 +6,21 @@
* found in the LICENSE file at https://angular.io/license
*/
import {createLContainer, createLView, createTNode, createTView} from '@angular/core/src/render3/instructions/shared';
import {createLContainer, createTNode} from '@angular/core/src/render3/instructions/shared';
import {isLContainer, isLView} from '@angular/core/src/render3/interfaces/type_checks';
import {TViewType} from '@angular/core/src/render3/interfaces/view';
import {ViewFixture} from './view_fixture';
describe('view_utils', () => {
it('should verify unwrap methods', () => {
const div = document.createElement('div');
const tView = createTView(TViewType.Root, null, null, 0, 0, null, null, null, null, null);
const lView = createLView(null, tView, {}, 0, div, null, {} as any, {} as any, null, null);
it('should verify unwrap methods (isLView and isLContainer)', () => {
const viewFixture = new ViewFixture();
const tNode = createTNode(null!, null, 3, 0, 'div', []);
const lContainer = createLContainer(lView, lView, div, tNode);
const lContainer =
createLContainer(viewFixture.lView, viewFixture.lView, viewFixture.host, tNode);
expect(isLView(lView)).toBe(true);
expect(isLView(viewFixture.lView)).toBe(true);
expect(isLView(lContainer)).toBe(false);
expect(isLContainer(lView)).toBe(false);
expect(isLContainer(viewFixture.lView)).toBe(false);
expect(isLContainer(lContainer)).toBe(true);
});
});

View File

@ -166,9 +166,10 @@ export class BaseAnimationRenderer implements Renderer2 {
this.engine.onInsert(this.namespaceId, newChild, parent, false);
}
insertBefore(parent: any, newChild: any, refChild: any): void {
insertBefore(parent: any, newChild: any, refChild: any, isMove: boolean = true): void {
this.delegate.insertBefore(parent, newChild, refChild);
this.engine.onInsert(this.namespaceId, newChild, parent, true);
// If `isMove` true than we should animate this insert.
this.engine.onInsert(this.namespaceId, newChild, parent, isMove);
}
removeChild(parent: any, oldChild: any, isHostElement: boolean): void {