fix(ivy): remove nested placeholders with i18n (#28595)

PR Close #28595
This commit is contained in:
Olivier Combe 2019-02-07 16:57:37 +01:00 committed by Miško Hevery
parent e0d2ca261b
commit 9d109929be
3 changed files with 97 additions and 12 deletions

View File

@ -614,6 +614,16 @@ export function i18nEnd(): void {
i18nEndFirstPass(tView); i18nEndFirstPass(tView);
} }
function findLastNode(node: TNode): TNode {
while (node.next) {
node = node.next;
}
if (node.child) {
return findLastNode(node.child);
}
return node;
}
/** /**
* See `i18nEnd` above. * See `i18nEnd` above.
*/ */
@ -629,12 +639,17 @@ function i18nEndFirstPass(tView: TView) {
// The last placeholder that was added before `i18nEnd` // The last placeholder that was added before `i18nEnd`
const previousOrParentTNode = getPreviousOrParentTNode(); const previousOrParentTNode = getPreviousOrParentTNode();
const visitedPlaceholders = readCreateOpCodes(rootIndex, tI18n.create, tI18n.icus, viewData); const visitedNodes = readCreateOpCodes(rootIndex, tI18n.create, tI18n.icus, viewData);
// Remove deleted placeholders // Find the last node that was added before `i18nEnd`
// The last placeholder that was added before `i18nEnd` is `previousOrParentTNode` let lastCreatedNode = previousOrParentTNode;
for (let i = rootIndex + 1; i <= previousOrParentTNode.index - HEADER_OFFSET; i++) { if (lastCreatedNode.child) {
if (visitedPlaceholders.indexOf(i) === -1) { lastCreatedNode = findLastNode(lastCreatedNode.child);
}
// Remove deleted nodes
for (let i = rootIndex + 1; i <= lastCreatedNode.index - HEADER_OFFSET; i++) {
if (visitedNodes.indexOf(i) === -1) {
removeNode(i, viewData); removeNode(i, viewData);
} }
} }
@ -646,7 +661,7 @@ function readCreateOpCodes(
const renderer = getLView()[RENDERER]; const renderer = getLView()[RENDERER];
let currentTNode: TNode|null = null; let currentTNode: TNode|null = null;
let previousTNode: TNode|null = null; let previousTNode: TNode|null = null;
const visitedPlaceholders: number[] = []; const visitedNodes: number[] = [];
for (let i = 0; i < createOpCodes.length; i++) { for (let i = 0; i < createOpCodes.length; i++) {
const opCode = createOpCodes[i]; const opCode = createOpCodes[i];
if (typeof opCode == 'string') { if (typeof opCode == 'string') {
@ -655,6 +670,7 @@ function readCreateOpCodes(
ngDevMode && ngDevMode.rendererCreateTextNode++; ngDevMode && ngDevMode.rendererCreateTextNode++;
previousTNode = currentTNode; previousTNode = currentTNode;
currentTNode = createNodeAtIndex(textNodeIndex, TNodeType.Element, textRNode, null, null); currentTNode = createNodeAtIndex(textNodeIndex, TNodeType.Element, textRNode, null, null);
visitedNodes.push(textNodeIndex);
setIsParent(false); setIsParent(false);
} else if (typeof opCode == 'number') { } else if (typeof opCode == 'number') {
switch (opCode & I18nMutateOpCode.MASK_OPCODE) { switch (opCode & I18nMutateOpCode.MASK_OPCODE) {
@ -677,7 +693,7 @@ function readCreateOpCodes(
break; break;
case I18nMutateOpCode.Select: case I18nMutateOpCode.Select:
const nodeIndex = opCode >>> I18nMutateOpCode.SHIFT_REF; const nodeIndex = opCode >>> I18nMutateOpCode.SHIFT_REF;
visitedPlaceholders.push(nodeIndex); visitedNodes.push(nodeIndex);
previousTNode = currentTNode; previousTNode = currentTNode;
currentTNode = getTNode(nodeIndex, viewData); currentTNode = getTNode(nodeIndex, viewData);
if (currentTNode) { if (currentTNode) {
@ -715,6 +731,7 @@ function readCreateOpCodes(
previousTNode = currentTNode; previousTNode = currentTNode;
currentTNode = currentTNode =
createNodeAtIndex(commentNodeIndex, TNodeType.IcuContainer, commentRNode, null, null); createNodeAtIndex(commentNodeIndex, TNodeType.IcuContainer, commentRNode, null, null);
visitedNodes.push(commentNodeIndex);
attachPatchData(commentRNode, viewData); attachPatchData(commentRNode, viewData);
(currentTNode as TIcuContainerNode).activeCaseIndex = null; (currentTNode as TIcuContainerNode).activeCaseIndex = null;
// We will add the case nodes later, during the update phase // We will add the case nodes later, during the update phase
@ -731,6 +748,7 @@ function readCreateOpCodes(
previousTNode = currentTNode; previousTNode = currentTNode;
currentTNode = createNodeAtIndex( currentTNode = createNodeAtIndex(
elementNodeIndex, TNodeType.Element, elementRNode, tagNameValue, null); elementNodeIndex, TNodeType.Element, elementRNode, tagNameValue, null);
visitedNodes.push(elementNodeIndex);
break; break;
default: default:
throw new Error(`Unable to determine the type of mutate operation for "${opCode}"`); throw new Error(`Unable to determine the type of mutate operation for "${opCode}"`);
@ -740,7 +758,7 @@ function readCreateOpCodes(
setIsParent(false); setIsParent(false);
return visitedPlaceholders; return visitedNodes;
} }
function readUpdateOpCodes( function readUpdateOpCodes(

View File

@ -6,10 +6,10 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Component, Directive, TemplateRef, ViewContainerRef} from '@angular/core'; import {Component, ContentChild, ContentChildren, Directive, QueryList, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core';
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/src/matchers'; import {expect} from '@angular/platform-browser/testing/src/matchers';
import {fixmeIvy, onlyInIvy, polyfillGoogGetMsg} from '@angular/private/testing'; import {onlyInIvy, polyfillGoogGetMsg} from '@angular/private/testing';
@Directive({ @Directive({
selector: '[tplRef]', selector: '[tplRef]',
@ -59,7 +59,9 @@ const TRANSLATIONS: any = {
'{VAR_SELECT, select, 10 {10 - {$startBoldText}ten{$closeBoldText}} 20 {20 - {$startItalicText}twenty{$closeItalicText}} other {{$startTagDiv}{$startUnderlinedText}other{$closeUnderlinedText}{$closeTagDiv}}}': '{VAR_SELECT, select, 10 {10 - {$startBoldText}ten{$closeBoldText}} 20 {20 - {$startItalicText}twenty{$closeItalicText}} other {{$startTagDiv}{$startUnderlinedText}other{$closeUnderlinedText}{$closeTagDiv}}}':
'{VAR_SELECT, select, 10 {10 - {$startBoldText}dix{$closeBoldText}} 20 {20 - {$startItalicText}vingt{$closeItalicText}} other {{$startTagDiv}{$startUnderlinedText}autres{$closeUnderlinedText}{$closeTagDiv}}}', '{VAR_SELECT, select, 10 {10 - {$startBoldText}dix{$closeBoldText}} 20 {20 - {$startItalicText}vingt{$closeItalicText}} other {{$startTagDiv}{$startUnderlinedText}autres{$closeUnderlinedText}{$closeTagDiv}}}',
'{VAR_SELECT_2, select, 10 {ten - {VAR_SELECT, select, 1 {one} 2 {two} other {more than two}}} 20 {twenty - {VAR_SELECT_1, select, 1 {one} 2 {two} other {more than two}}} other {other}}': '{VAR_SELECT_2, select, 10 {ten - {VAR_SELECT, select, 1 {one} 2 {two} other {more than two}}} 20 {twenty - {VAR_SELECT_1, select, 1 {one} 2 {two} other {more than two}}} other {other}}':
'{VAR_SELECT_2, select, 10 {dix - {VAR_SELECT, select, 1 {un} 2 {deux} other {plus que deux}}} 20 {vingt - {VAR_SELECT_1, select, 1 {un} 2 {deux} other {plus que deux}}} other {autres}}' '{VAR_SELECT_2, select, 10 {dix - {VAR_SELECT, select, 1 {un} 2 {deux} other {plus que deux}}} 20 {vingt - {VAR_SELECT_1, select, 1 {un} 2 {deux} other {plus que deux}}} other {autres}}',
'{$startTagNgTemplate}{$startTagDiv_1}{$startTagDiv}{$startTagSpan}Content{$closeTagSpan}{$closeTagDiv}{$closeTagDiv}{$closeTagNgTemplate}':
'{$startTagNgTemplate}Contenu{$closeTagNgTemplate}'
}; };
const getFixtureWithOverrides = (overrides = {}) => { const getFixtureWithOverrides = (overrides = {}) => {
@ -514,4 +516,69 @@ onlyInIvy('Ivy i18n logic').describe('i18n', function() {
expect(element).toHaveText('vingt'); expect(element).toHaveText('vingt');
}); });
}); });
describe('queries', () => {
function toHtml(element: Element): string {
return element.innerHTML.replace(/\sng-reflect-\S*="[^"]*"/g, '')
.replace(/<!--bindings=\{(\W.*\W\s*)?\}-->/g, '');
}
it('detached nodes should still be part of query', () => {
const template = `
<div-query #q i18n>
<ng-template>
<div>
<div *ngIf="visible">
<span text="1">Content</span>
</div>
</div>
</ng-template>
</div-query>
`;
@Directive({selector: '[text]', inputs: ['text'], exportAs: 'textDir'})
class TextDirective {
// TODO(issue/24571): remove '!'.
text !: string;
constructor() {}
}
@Component({selector: 'div-query', template: '<ng-container #vc></ng-container>'})
class DivQuery {
// TODO(issue/24571): remove '!'.
@ContentChild(TemplateRef) template !: TemplateRef<any>;
// TODO(issue/24571): remove '!'.
@ViewChild('vc', {read: ViewContainerRef})
vc !: ViewContainerRef;
// TODO(issue/24571): remove '!'.
@ContentChildren(TextDirective, {descendants: true})
query !: QueryList<TextDirective>;
create() { this.vc.createEmbeddedView(this.template); }
destroy() { this.vc.clear(); }
}
TestBed.configureTestingModule({declarations: [TextDirective, DivQuery]});
const fixture = getFixtureWithOverrides({template});
const q = fixture.debugElement.children[0].references.q;
expect(q.query.length).toEqual(0);
// Create embedded view
q.create();
fixture.detectChanges();
expect(q.query.length).toEqual(1);
expect(toHtml(fixture.nativeElement))
.toEqual(`<div-query><!--ng-container-->Contenu<!--container--></div-query>`);
// Disable ng-if
fixture.componentInstance.visible = false;
fixture.detectChanges();
expect(q.query.length).toEqual(0);
expect(toHtml(fixture.nativeElement))
.toEqual(`<div-query><!--ng-container-->Contenu<!--container--></div-query>`);
});
});
}); });

View File

@ -1771,7 +1771,7 @@ describe('Runtime i18n', () => {
selectors: [['parent']], selectors: [['parent']],
directives: [Child], directives: [Child],
factory: () => new Parent(), factory: () => new Parent(),
consts: 2, consts: 3,
vars: 0, vars: 0,
template: (rf: RenderFlags, cmp: Parent) => { template: (rf: RenderFlags, cmp: Parent) => {
if (rf & RenderFlags.Create) { if (rf & RenderFlags.Create) {