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:
parent
6790848f68
commit
ca11ef2376
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
"master": {
|
||||
"uncompressed": {
|
||||
"runtime-es2015": 3037,
|
||||
"main-es2015": 447742,
|
||||
"main-es2015": 448676,
|
||||
"polyfills-es2015": 52415
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 :
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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})`;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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++) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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 */
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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).
|
||||
*/
|
||||
|
|
|
@ -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++;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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`. */
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)}.`);
|
||||
}
|
|
@ -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, // "_"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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: []});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
});
|
|
@ -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,
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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]);',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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)}`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue