diff --git a/packages/core/src/render3/i18n.ts b/packages/core/src/render3/i18n.ts index 00cb81fc6e..5a825519ca 100644 --- a/packages/core/src/render3/i18n.ts +++ b/packages/core/src/render3/i18n.ts @@ -10,13 +10,12 @@ import {SRCSET_ATTRS, URI_ATTRS, VALID_ATTRS, VALID_ELEMENTS, getTemplateContent import {InertBodyHelper} from '../sanitization/inert_body'; import {_sanitizeUrl, sanitizeSrcset} from '../sanitization/url_sanitizer'; import {assertDefined, assertEqual, assertGreaterThan} from '../util/assert'; - import {attachPatchData} from './context_discovery'; import {allocExpando, createNodeAtIndex, elementAttribute, load, textBinding} from './instructions'; 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, TNodeType} from './interfaces/node'; -import {RComment, RElement} from './interfaces/renderer'; +import {RComment, RElement, RText} from './interfaces/renderer'; import {SanitizerFn} from './interfaces/sanitization'; import {StylingContext} from './interfaces/styling'; import {BINDING_INDEX, HEADER_OFFSET, LView, RENDERER, TVIEW, TView, T_HOST} from './interfaces/view'; @@ -466,11 +465,13 @@ function i18nStartFirstPass( function appendI18nNode(tNode: TNode, parentTNode: TNode, previousTNode: TNode | null): TNode { ngDevMode && ngDevMode.rendererMoveNode++; + const nextNode = tNode.next; const viewData = getLView(); if (!previousTNode) { previousTNode = parentTNode; } - // re-organize node tree to put this node in the correct position. + + // Re-organize node tree to put this node in the correct position. if (previousTNode === parentTNode && tNode !== parentTNode.child) { tNode.next = parentTNode.child; parentTNode.child = tNode; @@ -485,6 +486,15 @@ function appendI18nNode(tNode: TNode, parentTNode: TNode, previousTNode: TNode | 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; + } + appendChild(getNativeByTNode(tNode, viewData), tNode, viewData); const slotValue = viewData[tNode.index]; @@ -655,6 +665,24 @@ function i18nEndFirstPass(tView: TView) { } } +/** + * Creates and stores the dynamic TNode, and unhooks it from the tree for now. + */ +function createDynamicNodeAtIndex( + index: number, type: TNodeType, native: RElement | RText | null, + name: string | null): TElementNode|TIcuContainerNode { + const previousOrParentTNode = getPreviousOrParentTNode(); + const tNode = createNodeAtIndex(index, type as any, native, 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 (previousOrParentTNode.next === tNode) { + previousOrParentTNode.next = null; + } + + return tNode; +} + function readCreateOpCodes( index: number, createOpCodes: I18nMutateOpCodes, icus: TIcu[] | null, viewData: LView): number[] { @@ -669,7 +697,7 @@ function readCreateOpCodes( const textNodeIndex = createOpCodes[++i] as number; ngDevMode && ngDevMode.rendererCreateTextNode++; previousTNode = currentTNode; - currentTNode = createNodeAtIndex(textNodeIndex, TNodeType.Element, textRNode, null, null); + currentTNode = createDynamicNodeAtIndex(textNodeIndex, TNodeType.Element, textRNode, null); visitedNodes.push(textNodeIndex); setIsParent(false); } else if (typeof opCode == 'number') { @@ -729,8 +757,8 @@ function readCreateOpCodes( const commentRNode = renderer.createComment(commentValue); ngDevMode && ngDevMode.rendererCreateComment++; previousTNode = currentTNode; - currentTNode = - createNodeAtIndex(commentNodeIndex, TNodeType.IcuContainer, commentRNode, null, null); + currentTNode = createDynamicNodeAtIndex( + commentNodeIndex, TNodeType.IcuContainer, commentRNode, null); visitedNodes.push(commentNodeIndex); attachPatchData(commentRNode, viewData); (currentTNode as TIcuContainerNode).activeCaseIndex = null; @@ -746,8 +774,8 @@ function readCreateOpCodes( const elementRNode = renderer.createElement(tagNameValue); ngDevMode && ngDevMode.rendererCreateElement++; previousTNode = currentTNode; - currentTNode = createNodeAtIndex( - elementNodeIndex, TNodeType.Element, elementRNode, tagNameValue, null); + currentTNode = createDynamicNodeAtIndex( + elementNodeIndex, TNodeType.Element, elementRNode, tagNameValue); visitedNodes.push(elementNodeIndex); break; default: diff --git a/packages/core/test/render3/i18n_spec.ts b/packages/core/test/render3/i18n_spec.ts index fc97c5aa04..20a04d53cb 100644 --- a/packages/core/test/render3/i18n_spec.ts +++ b/packages/core/test/render3/i18n_spec.ts @@ -12,7 +12,7 @@ import {defineComponent, defineDirective} from '../../src/render3/definition'; import {getTranslationForTemplate, i18n, i18nApply, i18nAttributes, i18nEnd, i18nExp, i18nPostprocess, i18nStart} from '../../src/render3/i18n'; import {RenderFlags} from '../../src/render3/interfaces/definition'; import {AttributeMarker} from '../../src/render3/interfaces/node'; -import {getNativeByIndex} from '../../src/render3/util'; +import {getNativeByIndex, getTNode} from '../../src/render3/util'; import {NgIf} from './common_with_def'; import {allocHostVars, element, elementEnd, elementStart, template, text, nextContext, bind, elementProperty, projectionDef, projection, elementContainerStart, elementContainerEnd} from '../../src/render3/instructions'; import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nUpdateOpCode, I18nUpdateOpCodes, TI18n} from '../../src/render3/interfaces/i18n'; @@ -1436,6 +1436,49 @@ describe('Runtime i18n', () => { `
`); }); + it('should fix the links when adding/moving/removing nodes', () => { + const MSG_DIV = `�#2��/#2��#8��/#8��#4��/#4��#5��/#5�Hello World�#3��/#3��#7��/#7�`; + let fixture = prepareFixture(() => { + elementStart(0, 'div'); + { + i18nStart(1, MSG_DIV); + { + element(2, 'div2'); + element(3, 'div3'); + element(4, 'div4'); + element(5, 'div5'); + element(6, 'div6'); + element(7, 'div7'); + element(8, 'div8'); + } + i18nEnd(); + } + elementEnd(); + }, null, 9); + + expect(fixture.html) + .toEqual( + '