feat(ivy): runtime i18n (#24037)

PR Close #24037
This commit is contained in:
Olivier Combe 2018-06-18 16:55:43 +02:00 committed by Miško Hevery
parent cb31381734
commit 84272e2227
10 changed files with 2011 additions and 23 deletions

View File

@ -87,6 +87,13 @@ export {
DirectiveDefInternal as ɵDirectiveDef, DirectiveDefInternal as ɵDirectiveDef,
PipeDef as ɵPipeDef, PipeDef as ɵPipeDef,
whenRendered as ɵwhenRendered, whenRendered as ɵwhenRendered,
iA as ɵiA,
iEM as ɵiEM,
iI as ɵiI,
iIV as ɵIV,
iM as ɵiM,
I18nInstruction as ɵI18nInstruction,
I18nExpInstruction as ɵI18nExpInstruction,
} from './render3/index'; } from './render3/index';
export { export {
bypassSanitizationTrustHtml as ɵbypassSanitizationTrustHtml, bypassSanitizationTrustHtml as ɵbypassSanitizationTrustHtml,

View File

@ -0,0 +1,460 @@
/**
* @license
* Copyright Google Inc. 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, assertLessThan} from './assert';
import {NO_CHANGE, bindingUpdated, createLNode, getPreviousOrParentNode, getRenderer, getViewData, load} from './instructions';
import {LContainerNode, LElementNode, LNode, TNodeType} from './interfaces/node';
import {BINDING_INDEX} from './interfaces/view';
import {appendChild, createTextNode, getParentLNode, removeChild} from './node_manipulation';
import {stringify} from './util';
/**
* A list of flags to encode the i18n instructions used to translate the template.
* We shift the flags by 29 so that 30 & 31 & 32 bits contains the instructions.
*/
export const enum I18nInstructions {
Text = 1 << 29,
Element = 2 << 29,
Expression = 3 << 29,
CloseNode = 4 << 29,
RemoveNode = 5 << 29,
/** Used to decode the number encoded with the instruction. */
IndexMask = (1 << 29) - 1,
/** Used to test the type of instruction. */
InstructionMask = ~((1 << 29) - 1),
}
/**
* Represents the instructions used to translate the template.
* Instructions can be a placeholder index, a static text or a simple bit field (`I18nFlag`).
* When the instruction is the flag `Text`, it is always followed by its text value.
*/
export type I18nInstruction = number | string;
/**
* Represents the instructions used to translate attributes containing expressions.
* Even indexes contain static strings, while odd indexes contain the index of the expression whose
* value will be concatenated into the final translation.
*/
export type I18nExpInstruction = number | string;
/** Mapping of placeholder names to their absolute indexes in their templates. */
export type PlaceholderMap = {
[name: string]: number
};
const i18nTagRegex = /\{\$([^}]+)\}/g;
/**
* Takes a translation string, the initial list of placeholders (elements and expressions) and the
* indexes of their corresponding expression nodes to return a list of instructions for each
* template function.
*
* Because embedded templates have different indexes for each placeholder, each parameter (except
* the translation) is an array, where each value corresponds to a different template, by order of
* appearance.
*
* @param translation A translation string where placeholders are represented by `{$name}`
* @param elements An array containing, for each template, the maps of element placeholders and
* their indexes.
* @param expressions An array containing, for each template, the maps of expression placeholders
* and their indexes.
* @param tmplContainers An array of template container placeholders whose content should be ignored
* when generating the instructions for their parent template.
* @param lastChildIndex The index of the last child of the i18n node. Used when the i18n block is
* an ng-container.
*
* @returns A list of instructions used to translate each template.
*/
export function i18nMapping(
translation: string, elements: (PlaceholderMap | null)[] | null,
expressions?: (PlaceholderMap | null)[] | null, tmplContainers?: string[] | null,
lastChildIndex?: number | null): I18nInstruction[][] {
const translationParts = translation.split(i18nTagRegex);
const instructions: I18nInstruction[][] = [];
generateMappingInstructions(
0, translationParts, instructions, elements, expressions, tmplContainers, lastChildIndex);
return instructions;
}
/**
* Internal function that reads the translation parts and generates a set of instructions for each
* template.
*
* See `i18nMapping()` for more details.
*
* @param index The current index in `translationParts`.
* @param translationParts The translation string split into an array of placeholders and text
* elements.
* @param instructions The current list of instructions to update.
* @param elements An array containing, for each template, the maps of element placeholders and
* their indexes.
* @param expressions An array containing, for each template, the maps of expression placeholders
* and their indexes.
* @param tmplContainers An array of template container placeholders whose content should be ignored
* when generating the instructions for their parent template.
* @param lastChildIndex The index of the last child of the i18n node. Used when the i18n block is
* an ng-container.
* @returns the current index in `translationParts`
*/
function generateMappingInstructions(
index: number, translationParts: string[], instructions: I18nInstruction[][],
elements: (PlaceholderMap | null)[] | null, expressions?: (PlaceholderMap | null)[] | null,
tmplContainers?: string[] | null, lastChildIndex?: number | null): number {
const tmplIndex = instructions.length;
const tmplInstructions: I18nInstruction[] = [];
const phVisited = [];
let openedTagCount = 0;
let maxIndex = 0;
instructions.push(tmplInstructions);
for (; index < translationParts.length; index++) {
const value = translationParts[index];
// Odd indexes are placeholders
if (index & 1) {
let phIndex;
if (elements && elements[tmplIndex] &&
typeof(phIndex = elements[tmplIndex] ![value]) !== 'undefined') {
// The placeholder represents a DOM element
// Add an instruction to move the element
tmplInstructions.push(phIndex | I18nInstructions.Element);
phVisited.push(value);
openedTagCount++;
} else if (
expressions && expressions[tmplIndex] &&
typeof(phIndex = expressions[tmplIndex] ![value]) !== 'undefined') {
// The placeholder represents an expression
// Add an instruction to move the expression
tmplInstructions.push(phIndex | I18nInstructions.Expression);
phVisited.push(value);
} else { // It is a closing tag
tmplInstructions.push(I18nInstructions.CloseNode);
if (tmplIndex > 0) {
openedTagCount--;
// If we have reached the closing tag for this template, exit the loop
if (openedTagCount === 0) {
break;
}
}
}
if (typeof phIndex !== 'undefined' && phIndex > maxIndex) {
maxIndex = phIndex;
}
if (tmplContainers && tmplContainers.indexOf(value) !== -1 &&
tmplContainers.indexOf(value) >= tmplIndex) {
index = generateMappingInstructions(
index, translationParts, instructions, elements, expressions, tmplContainers,
lastChildIndex);
}
} else if (value) {
// It's a non-empty string, create a text node
tmplInstructions.push(I18nInstructions.Text, value);
}
}
// Check if some elements from the template are missing from the translation
if (elements) {
const tmplElements = elements[tmplIndex];
if (tmplElements) {
const phKeys = Object.keys(tmplElements);
for (let i = 0; i < phKeys.length; i++) {
const ph = phKeys[i];
if (phVisited.indexOf(ph) === -1) {
let index = tmplElements[ph];
// Add an instruction to remove the element
tmplInstructions.push(index | I18nInstructions.RemoveNode);
if (index > maxIndex) {
maxIndex = index;
}
}
}
}
}
// Check if some expressions from the template are missing from the translation
if (expressions) {
const tmplExpressions = expressions[tmplIndex];
if (tmplExpressions) {
const phKeys = Object.keys(tmplExpressions);
for (let i = 0; i < phKeys.length; i++) {
const ph = phKeys[i];
if (phVisited.indexOf(ph) === -1) {
let index = tmplExpressions[ph];
if (ngDevMode) {
assertLessThan(
index.toString(2).length, 28, `Index ${index} is too big and will overflow`);
}
// Add an instruction to remove the expression
tmplInstructions.push(index | I18nInstructions.RemoveNode);
if (index > maxIndex) {
maxIndex = index;
}
}
}
}
}
if (tmplIndex === 0 && typeof lastChildIndex === 'number') {
// The current parent is an ng-container and it has more children after the translation that we
// need to append to keep the order of the DOM nodes correct
for (let i = maxIndex + 1; i <= lastChildIndex; i++) {
if (ngDevMode) {
assertLessThan(i.toString(2).length, 28, `Index ${i} is too big and will overflow`);
}
// We consider those additional placeholders as expressions because we don't care about
// their children, all we need to do is to append them
tmplInstructions.push(i | I18nInstructions.Expression);
}
}
return index;
}
function appendI18nNode(node: LNode, parentNode: LNode, previousNode: LNode) {
if (ngDevMode) {
ngDevMode.rendererMoveNode++;
}
const viewData = getViewData();
appendChild(parentNode, node.native || null, viewData);
if (previousNode === parentNode && parentNode.pChild === null) {
parentNode.pChild = node;
} else {
previousNode.pNextOrParent = node;
}
// Template containers also have a comment node for the `ViewContainerRef` that should be moved
if (node.tNode.type === TNodeType.Container && node.dynamicLContainerNode) {
// (node.native as RComment).textContent = 'test';
// console.log(node.native);
appendChild(parentNode, node.dynamicLContainerNode.native || null, viewData);
node.pNextOrParent = node.dynamicLContainerNode;
return node.dynamicLContainerNode;
}
return node;
}
/**
* Takes a list of instructions generated by `i18nMapping()` to transform the template accordingly.
*
* @param startIndex Index of the first element to translate (for instance the first child of the
* element with the i18n attribute).
* @param instructions The list of instructions to apply on the current view.
*/
export function i18nApply(startIndex: number, instructions: I18nInstruction[]): void {
const viewData = getViewData();
if (ngDevMode) {
assertEqual(viewData[BINDING_INDEX], -1, 'i18nApply should be called before any binding');
}
if (!instructions) {
return;
}
const renderer = getRenderer();
let localParentNode: LNode = getParentLNode(load(startIndex)) || getPreviousOrParentNode();
let localPreviousNode: LNode = localParentNode;
for (let i = 0; i < instructions.length; i++) {
const instruction = instructions[i] as number;
switch (instruction & I18nInstructions.InstructionMask) {
case I18nInstructions.Element:
const element: LNode = load(instruction & I18nInstructions.IndexMask);
localPreviousNode = appendI18nNode(element, localParentNode, localPreviousNode);
localParentNode = element;
break;
case I18nInstructions.Expression:
const expr: LNode = load(instruction & I18nInstructions.IndexMask);
localPreviousNode = appendI18nNode(expr, localParentNode, localPreviousNode);
break;
case I18nInstructions.Text:
if (ngDevMode) {
ngDevMode.rendererCreateTextNode++;
}
const value = instructions[++i];
const textRNode = createTextNode(value, renderer);
// If we were to only create a `RNode` then projections won't move the text.
// But since this text doesn't have an index in `LViewData`, we need to create an
// `LElementNode` with the index -1 so that it isn't saved in `LViewData`
const textLNode = createLNode(-1, TNodeType.Element, textRNode, null, null);
textLNode.dynamicParent = localParentNode as LElementNode | LContainerNode;
localPreviousNode = appendI18nNode(textLNode, localParentNode, localPreviousNode);
break;
case I18nInstructions.CloseNode:
localPreviousNode = localParentNode;
localParentNode = getParentLNode(localParentNode) !;
break;
case I18nInstructions.RemoveNode:
if (ngDevMode) {
ngDevMode.rendererRemoveNode++;
}
const index = instruction & I18nInstructions.IndexMask;
const removedNode: LNode|LContainerNode = load(index);
const parentNode = getParentLNode(removedNode) !;
removeChild(parentNode, removedNode.native || null, viewData);
// For template containers we also need to remove their `ViewContainerRef` from the DOM
if (removedNode.tNode.type === TNodeType.Container && removedNode.dynamicLContainerNode) {
removeChild(parentNode, removedNode.dynamicLContainerNode.native || null, viewData);
removedNode.dynamicLContainerNode.tNode.detached = true;
}
break;
}
}
}
/**
* Takes a translation string and the initial list of expressions and returns a list of instructions
* that will be used to translate an attribute.
* Even indexes contain static strings, while odd indexes contain the index of the expression whose
* value will be concatenated into the final translation.
*/
export function i18nExpMapping(
translation: string, placeholders: PlaceholderMap): I18nExpInstruction[] {
const staticText: I18nExpInstruction[] = translation.split(i18nTagRegex);
// odd indexes are placeholders
for (let i = 1; i < staticText.length; i += 2) {
staticText[i] = placeholders[staticText[i]];
}
return staticText;
}
/**
* Checks if the value of up to 8 expressions have changed and replaces them by their values in a
* translation, or returns NO_CHANGE.
*
* @returns The concatenated string when any of the arguments changes, `NO_CHANGE` otherwise.
*/
export function i18nInterpolation(
instructions: I18nExpInstruction[], numberOfExp: number, v0: any, v1?: any, v2?: any, v3?: any,
v4?: any, v5?: any, v6?: any, v7?: any): string|NO_CHANGE {
let different = bindingUpdated(v0);
if (numberOfExp > 1) {
different = bindingUpdated(v1) || different;
if (numberOfExp > 2) {
different = bindingUpdated(v2) || different;
if (numberOfExp > 3) {
different = bindingUpdated(v3) || different;
if (numberOfExp > 4) {
different = bindingUpdated(v4) || different;
if (numberOfExp > 5) {
different = bindingUpdated(v5) || different;
if (numberOfExp > 6) {
different = bindingUpdated(v6) || different;
if (numberOfExp > 7) {
different = bindingUpdated(v7) || different;
}
}
}
}
}
}
}
if (!different) {
return NO_CHANGE;
}
let res = '';
for (let i = 0; i < instructions.length; i++) {
let value: any;
// Odd indexes are placeholders
if (i & 1) {
switch (instructions[i]) {
case 0:
value = v0;
break;
case 1:
value = v1;
break;
case 2:
value = v2;
break;
case 3:
value = v3;
break;
case 4:
value = v4;
break;
case 5:
value = v5;
break;
case 6:
value = v6;
break;
case 7:
value = v7;
break;
}
res += stringify(value);
} else {
res += instructions[i];
}
}
return res;
}
/**
* Create a translated interpolation binding with a variable number of expressions.
*
* If there are 1 to 8 expressions then `i18nInterpolation()` should be used instead. It is faster
* because there is no need to create an array of expressions and iterate over it.
*
* @returns The concatenated string when any of the arguments changes, `NO_CHANGE` otherwise.
*/
export function i18nInterpolationV(instructions: I18nExpInstruction[], values: any[]): string|
NO_CHANGE {
let different = false;
for (let i = 0; i < values.length; i++) {
// Check if bindings have changed
bindingUpdated(values[i]) && (different = true);
}
if (!different) {
return NO_CHANGE;
}
let res = '';
for (let i = 0; i < instructions.length; i++) {
// Odd indexes are placeholders
if (i & 1) {
res += stringify(values[instructions[i] as number]);
} else {
res += instructions[i];
}
}
return res;
}

View File

@ -8,8 +8,8 @@
import {LifecycleHooksFeature, getHostElement, getRenderedText, renderComponent, whenRendered} from './component'; import {LifecycleHooksFeature, getHostElement, getRenderedText, renderComponent, whenRendered} from './component';
import {NgOnChangesFeature, PublicFeature, defineComponent, defineDirective, defineNgModule, definePipe} from './definition'; import {NgOnChangesFeature, PublicFeature, defineComponent, defineDirective, defineNgModule, definePipe} from './definition';
import {I18nExpInstruction, I18nInstruction, i18nExpMapping, i18nInterpolation, i18nInterpolationV} from './i18n';
import {ComponentDefInternal, ComponentTemplate, ComponentType, DirectiveDefFlags, DirectiveDefInternal, DirectiveType, PipeDef} from './interfaces/definition'; import {ComponentDefInternal, ComponentTemplate, ComponentType, DirectiveDefFlags, DirectiveDefInternal, DirectiveType, PipeDef} from './interfaces/definition';
export {ComponentFactory, ComponentFactoryResolver, ComponentRef} from './component_ref'; export {ComponentFactory, ComponentFactoryResolver, ComponentRef} from './component_ref';
export {QUERY_READ_CONTAINER_REF, QUERY_READ_ELEMENT_REF, QUERY_READ_FROM_NODE, QUERY_READ_TEMPLATE_REF, directiveInject, injectAttribute, injectChangeDetectorRef, injectElementRef, injectTemplateRef, injectViewContainerRef} from './di'; export {QUERY_READ_CONTAINER_REF, QUERY_READ_ELEMENT_REF, QUERY_READ_FROM_NODE, QUERY_READ_TEMPLATE_REF, directiveInject, injectAttribute, injectChangeDetectorRef, injectElementRef, injectTemplateRef, injectViewContainerRef} from './di';
export {RenderFlags} from './interfaces/definition'; export {RenderFlags} from './interfaces/definition';
@ -79,6 +79,16 @@ export {
tick, tick,
} from './instructions'; } from './instructions';
export {
i18nApply as iA,
i18nMapping as iM,
i18nInterpolation as iI,
i18nInterpolationV as iIV,
i18nExpMapping as iEM,
I18nInstruction,
I18nExpInstruction
} from './i18n';
export {NgModuleDef, NgModuleFactory, NgModuleRef, NgModuleType} from './ng_module_ref'; export {NgModuleDef, NgModuleFactory, NgModuleRef, NgModuleType} from './ng_module_ref';
export { export {
@ -96,7 +106,6 @@ export {
export { export {
QueryList, QueryList,
query as Q, query as Q,
queryRefresh as qR, queryRefresh as qR,
} from './query'; } from './query';

View File

@ -6,22 +6,19 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import './ng_dev_mode';
import {Sanitizer} from '../sanitization/security'; import {Sanitizer} from '../sanitization/security';
import {assertDefined, assertEqual, assertLessThan, assertNotDefined, assertNotEqual} from './assert';
import {assertDefined, assertEqual, assertLessThan, assertNotDefined, assertNotEqual, assertSame} from './assert';
import {throwCyclicDependencyError, throwErrorIfNoChangesMode, throwMultipleComponentError} from './errors'; import {throwCyclicDependencyError, throwErrorIfNoChangesMode, throwMultipleComponentError} from './errors';
import {executeHooks, executeInitHooks, queueInitHooks, queueLifecycleHooks} from './hooks'; import {executeHooks, executeInitHooks, queueInitHooks, queueLifecycleHooks} from './hooks';
import {ACTIVE_INDEX, LContainer, RENDER_PARENT, VIEWS} from './interfaces/container'; import {ACTIVE_INDEX, LContainer, RENDER_PARENT, VIEWS} from './interfaces/container';
import {LInjector} from './interfaces/injector'; import {LInjector} from './interfaces/injector';
import {AttributeMarker, InitialInputData, InitialInputs, LContainerNode, LElementNode, LNode, LProjectionNode, LTextNode, LViewNode, PropertyAliases, PropertyAliasValue, TAttributes, TContainerNode, TElementNode, TNode, TNodeFlags, TNodeType,} from './interfaces/node';
import {CssSelectorList, LProjection, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection'; import {CssSelectorList, LProjection, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection';
import {LQueries} from './interfaces/query'; import {LQueries} from './interfaces/query';
import {BINDING_INDEX, CLEANUP, CONTEXT, CurrentMatchesList, DIRECTIVES, FLAGS, HEADER_OFFSET, HOST_NODE, INJECTOR, LViewData, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RootContext, SANITIZER, TAIL, TData, TVIEW, TView,} from './interfaces/view'; import {BINDING_INDEX, CLEANUP, CONTEXT, CurrentMatchesList, DIRECTIVES, FLAGS, HEADER_OFFSET, HOST_NODE, INJECTOR, LViewData, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RootContext, SANITIZER, TAIL, TVIEW, TView,} from './interfaces/view';
import './ng_dev_mode';
import {AttributeMarker, TAttributes, LContainerNode, LElementNode, LNode, TNodeType, TNodeFlags, LProjectionNode, LTextNode, LViewNode, TNode, TContainerNode, InitialInputData, InitialInputs, PropertyAliases, PropertyAliasValue, TElementNode,} from './interfaces/node';
import {assertNodeType} from './node_assert'; import {assertNodeType} from './node_assert';
import {appendChild, insertView, appendProjectedNode, removeView, canInsertNativeNode, createTextNode, getNextLNode, getChildLNode, getParentLNode, getLViewChild} from './node_manipulation'; import {appendChild, appendProjectedNode, canInsertNativeNode, createTextNode, getChildLNode, getLViewChild, getNextLNode, getParentLNode, insertView, removeChild, removeView} from './node_manipulation';
import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher'; import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher';
import {ComponentDefInternal, ComponentTemplate, ComponentQuery, DirectiveDefInternal, DirectiveDefListOrFactory, PipeDefListOrFactory, RenderFlags} from './interfaces/definition'; import {ComponentDefInternal, ComponentTemplate, ComponentQuery, DirectiveDefInternal, DirectiveDefListOrFactory, PipeDefListOrFactory, RenderFlags} from './interfaces/definition';
import {RComment, RElement, RText, Renderer3, RendererFactory3, ProceduralRenderer3, RendererStyleFlags3, isProceduralRenderer} from './interfaces/renderer'; import {RComment, RElement, RText, Renderer3, RendererFactory3, ProceduralRenderer3, RendererStyleFlags3, isProceduralRenderer} from './interfaces/renderer';
@ -103,6 +100,11 @@ export function getCurrentSanitizer(): Sanitizer|null {
return viewData && viewData[SANITIZER]; return viewData && viewData[SANITIZER];
} }
export function getViewData(): LViewData {
// top level variables should not be exported for performance reasons (PERF_NOTES.md)
return viewData;
}
/** Used to set the parent property when nodes are created. */ /** Used to set the parent property when nodes are created. */
let previousOrParentNode: LNode; let previousOrParentNode: LNode;
@ -319,7 +321,8 @@ export function createLNodeObject(
tNode: null !, tNode: null !,
pNextOrParent: null, pNextOrParent: null,
dynamicLContainerNode: null, dynamicLContainerNode: null,
dynamicParent: null dynamicParent: null,
pChild: null,
}; };
} }
@ -367,8 +370,8 @@ export function createLNode(
if (index === -1 || type === TNodeType.View) { if (index === -1 || type === TNodeType.View) {
// View nodes are not stored in data because they can be added / removed at runtime (which // View nodes are not stored in data because they can be added / removed at runtime (which
// would cause indices to change). Their TNodes are instead stored in TView.node. // would cause indices to change). Their TNodes are instead stored in TView.node.
node.tNode = node.tNode = (state ? (state as LViewData)[TVIEW].node : null) ||
(state as LViewData)[TVIEW].node || createTNode(type, index, null, null, tParent, null); createTNode(type, index, null, null, tParent, null);
} else { } else {
const adjustedIndex = index + HEADER_OFFSET; const adjustedIndex = index + HEADER_OFFSET;
@ -1157,7 +1160,8 @@ export function createTNode(
next: null, next: null,
child: null, child: null,
parent: parent, parent: parent,
dynamicContainerNode: null dynamicContainerNode: null,
detached: null
}; };
} }
@ -1334,8 +1338,6 @@ export function elementStyle<T>(
} }
} }
////////////////////////// //////////////////////////
//// Text //// Text
////////////////////////// //////////////////////////
@ -1893,7 +1895,16 @@ export function projectionDef(
} }
const componentNode: LElementNode = findComponentHost(viewData); const componentNode: LElementNode = findComponentHost(viewData);
let componentChild: LNode|null = getChildLNode(componentNode); let isProjectingI18nNodes = false;
let componentChild: LNode|null;
// for i18n translations we use pChild to point to the next child
// TODO(kara): Remove when removing LNodes
if (componentNode.pChild) {
isProjectingI18nNodes = true;
componentChild = componentNode.pChild;
} else {
componentChild = getChildLNode(componentNode);
}
while (componentChild !== null) { while (componentChild !== null) {
// execute selector matching logic if and only if: // execute selector matching logic if and only if:
@ -1906,7 +1917,11 @@ export function projectionDef(
distributedNodes[0].push(componentChild); distributedNodes[0].push(componentChild);
} }
componentChild = getNextLNode(componentChild); if (isProjectingI18nNodes) {
componentChild = componentChild.pNextOrParent;
} else {
componentChild = getNextLNode(componentChild);
}
} }
ngDevMode && assertDataNext(index + HEADER_OFFSET); ngDevMode && assertDataNext(index + HEADER_OFFSET);

View File

@ -100,6 +100,13 @@ export interface LNode {
*/ */
pNextOrParent: LNode|null; pNextOrParent: LNode|null;
/**
* If this node is part of an i18n block, pointer to the first child after translation
* If this node is not part of an i18n block, this field is null.
*/
// TODO(kara): Remove this when removing pNextOrParent
pChild: LNode|null;
/** /**
* Pointer to the corresponding TNode object, which stores static * Pointer to the corresponding TNode object, which stores static
* data about this node. * data about this node.
@ -342,6 +349,12 @@ export interface TNode {
* A pointer to a TContainerNode created by directives requesting ViewContainerRef * A pointer to a TContainerNode created by directives requesting ViewContainerRef
*/ */
dynamicContainerNode: TNode|null; dynamicContainerNode: TNode|null;
/**
* If this node is part of an i18n block, it indicates whether this container is part of the DOM
* If this node is not part of an i18n block, this field is null.
*/
detached: boolean|null;
} }
/** Static data for an LElementNode */ /** Static data for an LElementNode */

View File

@ -27,6 +27,8 @@ declare global {
rendererRemoveStyle: number; rendererRemoveStyle: number;
rendererDestroy: number; rendererDestroy: number;
rendererDestroyNode: number; rendererDestroyNode: number;
rendererMoveNode: number;
rendererRemoveNode: number;
} }
} }
@ -54,6 +56,8 @@ export const ngDevModeResetPerfCounters: () => void =
rendererRemoveStyle: 0, rendererRemoveStyle: 0,
rendererDestroy: 0, rendererDestroy: 0,
rendererDestroyNode: 0, rendererDestroyNode: 0,
rendererMoveNode: 0,
rendererRemoveNode: 0,
}; };
} }
ngDevModeResetPerfCounters(); ngDevModeResetPerfCounters();

View File

@ -29,6 +29,9 @@ export function getNextLNode(node: LNode): LNode|null {
/** Retrieves the first child of a given node */ /** Retrieves the first child of a given node */
export function getChildLNode(node: LNode): LNode|null { export function getChildLNode(node: LNode): LNode|null {
if (node.pChild) {
return node.pChild;
}
if (node.tNode.child) { if (node.tNode.child) {
const viewData = node.tNode.type === TNodeType.View ? node.data as LViewData : node.view; const viewData = node.tNode.type === TNodeType.View ? node.data as LViewData : node.view;
return viewData[node.tNode.child.index]; return viewData[node.tNode.child.index];
@ -313,7 +316,7 @@ export function insertView(
// If the container's renderParent is null, we know that it is a root node of its own parent view // If the container's renderParent is null, we know that it is a root node of its own parent view
// and we should wait until that parent processes its nodes (otherwise, we will insert this view's // and we should wait until that parent processes its nodes (otherwise, we will insert this view's
// nodes twice - once now and once when its parent inserts its views). // nodes twice - once now and once when its parent inserts its views).
if (container.data[RENDER_PARENT] !== null) { if (container.data[RENDER_PARENT] !== null && !container.tNode.detached) {
// Find the node to insert in front of // Find the node to insert in front of
const beforeNode = const beforeNode =
index + 1 < views.length ? (getChildLNode(views[index + 1]) !).native : container.native; index + 1 < views.length ? (getChildLNode(views[index + 1]) !).native : container.native;
@ -343,7 +346,9 @@ export function detachView(container: LContainerNode, removeIndex: number): LVie
views[removeIndex - 1].data[NEXT] = viewNode.data[NEXT] as LViewData; views[removeIndex - 1].data[NEXT] = viewNode.data[NEXT] as LViewData;
} }
views.splice(removeIndex, 1); views.splice(removeIndex, 1);
addRemoveViewFromContainer(container, viewNode, false); if (!container.tNode.detached) {
addRemoveViewFromContainer(container, viewNode, false);
}
// Notify query that view has been removed // Notify query that view has been removed
const removedLview = viewNode.data; const removedLview = viewNode.data;
if (removedLview[QUERIES]) { if (removedLview[QUERIES]) {
@ -506,7 +511,7 @@ export function canInsertNativeNode(parent: LNode, currentView: LViewData): bool
/** /**
* Appends the `child` element to the `parent`. * Appends the `child` element to the `parent`.
* *
* The element insertion might be delayed {@link canInsertNativeNode} * The element insertion might be delayed {@link canInsertNativeNode}.
* *
* @param parent The parent to which to append the child * @param parent The parent to which to append the child
* @param child The child that should be appended * @param child The child that should be appended
@ -515,7 +520,7 @@ export function canInsertNativeNode(parent: LNode, currentView: LViewData): bool
*/ */
export function appendChild(parent: LNode, child: RNode | null, currentView: LViewData): boolean { export function appendChild(parent: LNode, child: RNode | null, currentView: LViewData): boolean {
if (child !== null && canInsertNativeNode(parent, currentView)) { if (child !== null && canInsertNativeNode(parent, currentView)) {
// We only add element if not in View or not projected. // We only add the element if not in View or not projected.
const renderer = currentView[RENDERER]; const renderer = currentView[RENDERER];
isProceduralRenderer(renderer) ? renderer.appendChild(parent.native !as RElement, child) : isProceduralRenderer(renderer) ? renderer.appendChild(parent.native !as RElement, child) :
parent.native !.appendChild(child); parent.native !.appendChild(child);
@ -524,6 +529,25 @@ export function appendChild(parent: LNode, child: RNode | null, currentView: LVi
return false; return false;
} }
/**
* Removes the `child` element of the `parent` from the DOM.
*
* @param parent The parent from which to remove the child
* @param child The child that should be removed
* @param currentView The current LView
* @returns Whether or not the child was removed
*/
export function removeChild(parent: LNode, child: RNode | null, currentView: LViewData): boolean {
if (child !== null && canInsertNativeNode(parent, currentView)) {
// We only remove the element if not in View or not projected.
const renderer = currentView[RENDERER];
isProceduralRenderer(renderer) ? renderer.removeChild(parent.native as RElement, child) :
parent.native !.removeChild(child);
return true;
}
return false;
}
/** /**
* Appends a projected node to the DOM, or in the case of a projected container, * Appends a projected node to the DOM, or in the case of a projected container,
* appends the nodes from all of the container's active views to the DOM. * appends the nodes from all of the container's active views to the DOM.

View File

@ -0,0 +1,139 @@
/**
* @license
* Copyright Google Inc. 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 {NgForOfContext} from '@angular/common';
import {Component} from '../../../src/core';
import * as $r3$ from '../../../src/core_render3_private_export';
import {NgForOf} from '../common_with_def';
/// See: `normative.md`
describe('i18n', () => {
type $RenderFlags$ = $r3$.ɵRenderFlags;
it('should support html', () => {
type $MyApp$ = MyApp;
const $msg_1$ = `{$START_P}contenu{$END_P}`;
const $i18n_1$ = $r3$.ɵiM($msg_1$, [{START_P: 1}]);
@Component({selector: 'my-app', template: `<div i18n><p>content</p></div>`})
class MyApp {
static ngComponentDef = $r3$.ɵdefineComponent({
type: MyApp,
selectors: [['my-app']],
factory: () => new MyApp(),
template: function(rf: $RenderFlags$, ctx: $MyApp$) {
if (rf & 1) {
$r3$.ɵE(0, 'div');
$r3$.ɵE(1, 'p');
$r3$.ɵe();
$r3$.ɵe();
$r3$.ɵiA(1, $i18n_1$[0]);
}
}
});
}
});
it('should support expressions', () => {
type $MyApp$ = MyApp;
const $msg_1$ = `contenu: {$EXP_1}`;
const $i18n_1$ = $r3$.ɵiM($msg_1$, null, [{EXP_1: 1}]);
@Component({selector: 'my-app', template: `<div i18n>content: {{exp1}}</div>`})
class MyApp {
exp1 = '1';
static ngComponentDef = $r3$.ɵdefineComponent({
type: MyApp,
selectors: [['my-app']],
factory: () => new MyApp(),
template: function(rf: $RenderFlags$, ctx: $MyApp$) {
if (rf & 1) {
$r3$.ɵE(0, 'div');
$r3$.ɵT(1);
$r3$.ɵe();
$r3$.ɵiA(1, $i18n_1$[0]);
}
if (rf & 2) {
$r3$.ɵt(3, $r3$.ɵb(ctx.exp1));
}
}
});
}
});
it('should support expressions in attributes', () => {
type $MyApp$ = MyApp;
const $msg_1$ = `titre: {$EXP_1}`;
const $i18n_1$ = $r3$.ɵiEM($msg_1$, {EXP_1: 1});
@Component({selector: 'my-app', template: `<div i18n><p title="title: {{exp1}}"></p></div>`})
class MyApp {
exp1 = '1';
static ngComponentDef = $r3$.ɵdefineComponent({
type: MyApp,
selectors: [['my-app']],
factory: () => new MyApp(),
template: function(rf: $RenderFlags$, ctx: $MyApp$) {
if (rf & 1) {
$r3$.ɵE(0, 'div');
$r3$.ɵE(1, 'p');
$r3$.ɵe();
$r3$.ɵe();
}
if (rf & 2) {
$r3$.ɵp(0, 'title', $r3$.ɵiI($i18n_1$, 2, ctx.exp1));
}
}
});
}
});
it('should support embedded templates', () => {
type $MyApp$ = MyApp;
const $msg_1$ = `{$START_LI}valeur: {$EXP_1}!{$END_LI}`;
const $i18n_1$ =
$r3$.ɵiM($msg_1$, [{START_LI: 1}, {START_LI: 0}], [null, {EXP_1: 1}], ['START_LI']);
@Component({
selector: 'my-app',
template: `<ul i18n><li *ngFor="let item of items">value: {{item}}</li></ul>`
})
class MyApp {
items: string[] = ['1', '2'];
static ngComponentDef = $r3$.ɵdefineComponent({
type: MyApp,
factory: () => new MyApp(),
selectors: [['my-app']],
template: (rf: $RenderFlags$, myApp: $MyApp$) => {
if (rf & 1) {
$r3$.ɵE(0, 'ul');
$r3$.ɵC(1, liTemplate, null, ['ngForOf', '']);
$r3$.ɵe();
$r3$.ɵiA(1, $i18n_1$[0]);
}
if (rf & 2) {
$r3$.ɵp(1, 'ngForOf', $r3$.ɵb(myApp.items));
}
function liTemplate(rf1: $RenderFlags$, row: NgForOfContext<string>) {
if (rf1 & 1) {
$r3$.ɵE(0, 'li');
$r3$.ɵT(1);
$r3$.ɵe();
$r3$.ɵiA(0, $i18n_1$[1]);
}
if (rf1 & 2) {
$r3$.ɵt(1, $r3$.ɵb(row.$implicit));
}
}
},
directives: () => [NgForOf]
});
}
});
});

File diff suppressed because it is too large Load Diff

View File

@ -24,7 +24,8 @@ function testLStaticData(tagName: string, attrs: TAttributes | null): TNode {
next: null, next: null,
child: null, child: null,
parent: null, parent: null,
dynamicContainerNode: null dynamicContainerNode: null,
detached: null
}; };
} }