diff --git a/packages/core/test/acceptance/common_integration_spec.ts b/packages/core/test/acceptance/common_integration_spec.ts new file mode 100644 index 0000000000..14615fb8f6 --- /dev/null +++ b/packages/core/test/acceptance/common_integration_spec.ts @@ -0,0 +1,541 @@ +/** + * @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 {Component, Directive} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; + +describe('@angular/common integration', () => { + + describe('NgForOf', () => { + @Directive({selector: '[dir]'}) + class MyDirective { + } + + @Component({selector: 'app-child', template: '
comp text
'}) + class ChildComponent { + } + + @Component({selector: 'app-root', template: ''}) + class AppComponent { + items: string[] = ['first', 'second']; + } + + beforeEach(() => { + TestBed.configureTestingModule({declarations: [AppComponent, ChildComponent, MyDirective]}); + }); + + it('should update a loop', () => { + TestBed.overrideTemplate( + AppComponent, ''); + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + + let listItems = + Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.map(li => li.textContent)).toEqual(['first', 'second']); + + // change detection cycle, no model changes + fixture.detectChanges(); + listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.map(li => li.textContent)).toEqual(['first', 'second']); + + // remove the last item + const items = fixture.componentInstance.items; + items.length = 1; + fixture.detectChanges(); + + listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.map(li => li.textContent)).toEqual(['first']); + + // change an item + items[0] = 'one'; + fixture.detectChanges(); + listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.map(li => li.textContent)).toEqual(['one']); + + // add an item + items.push('two'); + fixture.detectChanges(); + listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.map(li => li.textContent)).toEqual(['one', 'two']); + }); + + it('should support ngForOf context variables', () => { + TestBed.overrideTemplate( + AppComponent, + ''); + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + + let listItems = + Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.map(li => li.textContent)).toEqual(['0 of 2: first', '1 of 2: second']); + + // add an item in the middle + const items = fixture.componentInstance.items; + items.splice(1, 0, 'middle'); + fixture.detectChanges(); + listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.map(li => li.textContent)).toEqual([ + '0 of 3: first', '1 of 3: middle', '2 of 3: second' + ]); + }); + + it('should instantiate directives inside directives properly in an ngFor', () => { + TestBed.overrideTemplate(AppComponent, ''); + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + + const children = fixture.debugElement.queryAll(By.directive(ChildComponent)); + + // expect 2 children, each one with a directive + expect(children.length).toBe(2); + expect(children.map(child => child.nativeElement.innerHTML)).toEqual([ + '
comp text
', '
comp text
' + ]); + let directive = children[0].query(By.directive(MyDirective)); + expect(directive).not.toBeNull(); + directive = children[1].query(By.directive(MyDirective)); + expect(directive).not.toBeNull(); + + // add an item + const items = fixture.componentInstance.items; + items.push('third'); + fixture.detectChanges(); + + const childrenAfterAdd = fixture.debugElement.queryAll(By.directive(ChildComponent)); + + expect(childrenAfterAdd.length).toBe(3); + expect(childrenAfterAdd.map(child => child.nativeElement.innerHTML)).toEqual([ + '
comp text
', '
comp text
', '
comp text
' + ]); + directive = childrenAfterAdd[2].query(By.directive(MyDirective)); + expect(directive).not.toBeNull(); + }); + + it('should retain parent view listeners when the NgFor destroy views', () => { + @Component({ + selector: 'app-toggle', + template: ` + ` + }) + class ToggleComponent { + private _data: number[] = [1, 2, 3]; + items: number[] = []; + + toggle() { + if (this.items.length) { + this.items = []; + } else { + this.items = this._data; + } + } + } + + TestBed.configureTestingModule({declarations: [ToggleComponent]}); + const fixture = TestBed.createComponent(ToggleComponent); + fixture.detectChanges(); + + // no elements in the list + let listItems = + Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.length).toBe(0); + + // this will fill the list + fixture.componentInstance.toggle(); + fixture.detectChanges(); + listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.length).toBe(3); + expect(listItems.map(li => li.textContent)).toEqual(['1', '2', '3']); + + // now toggle via the button + const button: HTMLButtonElement = fixture.nativeElement.querySelector('button'); + button.click(); + fixture.detectChanges(); + listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.length).toBe(0); + + // toggle again + button.click(); + fixture.detectChanges(); + listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.length).toBe(3); + }); + + it('should support multiple levels of embedded templates', () => { + @Component({ + selector: 'app-multi', + template: `` + }) + class MultiLevelComponent { + items: any[] = [{data: ['1', '2'], value: 'first'}, {data: ['3', '4'], value: 'second'}]; + } + + TestBed.configureTestingModule({declarations: [MultiLevelComponent]}); + const fixture = TestBed.createComponent(MultiLevelComponent); + fixture.detectChanges(); + + // change detection cycle, no model changes + let listItems = + Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.length).toBe(2); + let spanItems = Array.from(listItems[0].querySelectorAll('span')); + expect(spanItems.map(span => span.textContent)).toEqual(['1 - first - 2', '2 - first - 2']); + spanItems = Array.from(listItems[1].querySelectorAll('span')); + expect(spanItems.map(span => span.textContent)).toEqual(['3 - second - 2', '4 - second - 2']); + + // remove the last item + const items = fixture.componentInstance.items; + items.length = 1; + fixture.detectChanges(); + + listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.length).toBe(1); + spanItems = Array.from(listItems[0].querySelectorAll('span')); + expect(spanItems.map(span => span.textContent)).toEqual(['1 - first - 1', '2 - first - 1']); + + // change an item + items[0].data[0] = 'one'; + fixture.detectChanges(); + + listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.length).toBe(1); + spanItems = Array.from(listItems[0].querySelectorAll('span')); + expect(spanItems.map(span => span.textContent)).toEqual(['one - first - 1', '2 - first - 1']); + + // add an item + items[1] = {data: ['three', '4'], value: 'third'}; + fixture.detectChanges(); + + listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.length).toBe(2); + spanItems = Array.from(listItems[0].querySelectorAll('span')); + expect(spanItems.map(span => span.textContent)).toEqual(['one - first - 2', '2 - first - 2']); + spanItems = Array.from(listItems[1].querySelectorAll('span')); + expect(spanItems.map(span => span.textContent)).toEqual([ + 'three - third - 2', '4 - third - 2' + ]); + }); + + it('should support multiple levels of embedded templates with listeners', () => { + @Component({ + selector: 'app-multi', + template: `
+

+ + {{ row.value }} - {{ name }} +

+
` + }) + class MultiLevelWithListenerComponent { + items: any[] = [{data: ['1'], value: 'first'}]; + name = 'app'; + events: string[] = []; + + onClick(value: string, name: string) { this.events.push(value, name); } + } + + TestBed.configureTestingModule({declarations: [MultiLevelWithListenerComponent]}); + const fixture = TestBed.createComponent(MultiLevelWithListenerComponent); + fixture.detectChanges(); + + const elements = fixture.nativeElement.querySelectorAll('p'); + expect(elements.length).toBe(1); + expect(elements[0].innerHTML).toBe(' first - app '); + + const span: HTMLSpanElement = fixture.nativeElement.querySelector('span'); + span.click(); + expect(fixture.componentInstance.events).toEqual(['first', 'app']); + + fixture.componentInstance.name = 'new name'; + fixture.detectChanges(); + expect(elements[0].innerHTML).toBe(' first - new name '); + + span.click(); + expect(fixture.componentInstance.events).toEqual(['first', 'app', 'first', 'new name']); + }); + + it('should support skipping contexts', () => { + @Component({ + selector: 'app-multi', + template: `
+
+ {{ cell.value }} - {{ name }} +
+
` + }) + class SkippingContextComponent { + name = 'app'; + items: any[] = [ + [ + // row + {value: 'one', data: ['1', '2']} // cell + ], + [{value: 'two', data: ['3', '4']}] + ]; + } + + TestBed.configureTestingModule({declarations: [SkippingContextComponent]}); + const fixture = TestBed.createComponent(SkippingContextComponent); + fixture.detectChanges(); + + const elements = fixture.nativeElement.querySelectorAll('span'); + expect(elements.length).toBe(4); + expect(elements[0].textContent).toBe('one - app'); + expect(elements[1].textContent).toBe('one - app'); + expect(elements[2].textContent).toBe('two - app'); + expect(elements[3].textContent).toBe('two - app'); + + fixture.componentInstance.name = 'other'; + fixture.detectChanges(); + expect(elements[0].textContent).toBe('one - other'); + expect(elements[1].textContent).toBe('one - other'); + expect(elements[2].textContent).toBe('two - other'); + expect(elements[3].textContent).toBe('two - other'); + }); + + it('should support context for 9+ levels of embedded templates', () => { + @Component({ + selector: 'app-multi', + template: `
+ + + + + + + + {{ item8 }}.{{ item7.value }}.{{ item6.value }}.{{ item5.value }}.{{ item4.value }}.{{ item3.value }}.{{ item2.value }}.{{ item1.value }}.{{ item0.value }}.{{ value }} + + + + + + + +
` + }) + class NineLevelsComponent { + value = 'App'; + items: any[] = [ + { + // item0 + data: [{ + // item1 + data: [{ + // item2 + data: [{ + // item3 + data: [{ + // item4 + data: [{ + // item5 + data: [{ + // item6 + data: [{ + // item7 + data: [ + '1', '2' // item8 + ], + value: 'h' + }], + value: 'g' + }], + value: 'f' + }], + value: 'e' + }], + value: 'd' + }], + value: 'c' + }], + value: 'b' + }], + value: 'a' + }, + { + // item0 + data: [{ + // item1 + data: [{ + // item2 + data: [{ + // item3 + data: [{ + // item4 + data: [{ + // item5 + data: [{ + // item6 + data: [{ + // item7 + data: [ + '3', '4' // item8 + ], + value: 'H' + }], + value: 'G' + }], + value: 'F' + }], + value: 'E' + }], + value: 'D' + }], + value: 'C' + }], + value: 'B' + }], + value: 'A' + } + ]; + } + + TestBed.configureTestingModule({declarations: [NineLevelsComponent]}); + const fixture = TestBed.createComponent(NineLevelsComponent); + fixture.detectChanges(); + + const divItems = (fixture.nativeElement as HTMLElement).querySelectorAll('div'); + expect(divItems.length).toBe(2); // 2 outer loops + let spanItems = + divItems[0].querySelectorAll('span > span > span > span > span > span > span > span'); + expect(spanItems.length).toBe(2); // 2 inner elements + expect(spanItems[0].textContent).toBe('1.h.g.f.e.d.c.b.a.App'); + expect(spanItems[1].textContent).toBe('2.h.g.f.e.d.c.b.a.App'); + spanItems = + divItems[1].querySelectorAll('span > span > span > span > span > span > span > span'); + expect(spanItems.length).toBe(2); // 2 inner elements + expect(spanItems[0].textContent).toBe('3.H.G.F.E.D.C.B.A.App'); + expect(spanItems[1].textContent).toBe('4.H.G.F.E.D.C.B.A.App'); + }); + }); + + describe('ngIf', () => { + it('should support sibling ngIfs', () => { + @Component({ + selector: 'app-multi', + template: ` +
{{ valueOne }}
+
{{ valueTwo }}
+ ` + }) + class SimpleConditionComponent { + showing = true; + valueOne = 'one'; + valueTwo = 'two'; + } + + TestBed.configureTestingModule({declarations: [SimpleConditionComponent]}); + const fixture = TestBed.createComponent(SimpleConditionComponent); + fixture.detectChanges(); + + const elements = fixture.nativeElement.querySelectorAll('div'); + expect(elements.length).toBe(2); + expect(elements[0].textContent).toBe('one'); + expect(elements[1].textContent).toBe('two'); + + fixture.componentInstance.valueOne = '$$one$$'; + fixture.componentInstance.valueTwo = '$$two$$'; + fixture.detectChanges(); + expect(elements[0].textContent).toBe('$$one$$'); + expect(elements[1].textContent).toBe('$$two$$'); + }); + + it('should handle nested ngIfs with no intermediate context vars', () => { + @Component({ + selector: 'app-multi', + template: `
+
+
{{ name }}
+
+
+ ` + }) + class NestedConditionsComponent { + showing = true; + outerShowing = true; + innerShowing = true; + name = 'App name'; + } + + TestBed.configureTestingModule({declarations: [NestedConditionsComponent]}); + const fixture = TestBed.createComponent(NestedConditionsComponent); + fixture.detectChanges(); + + const elements = fixture.nativeElement.querySelectorAll('div'); + expect(elements.length).toBe(3); + expect(elements[2].textContent).toBe('App name'); + + fixture.componentInstance.name = 'Other name'; + fixture.detectChanges(); + expect(elements[2].textContent).toBe('Other name'); + }); + }); + + describe('NgTemplateOutlet', () => { + + it('should create and remove embedded views', () => { + @Component({ + selector: 'app-multi', + template: `from tpl + + ` + }) + class EmbeddedViewsComponent { + showing = false; + } + + TestBed.configureTestingModule({declarations: [EmbeddedViewsComponent]}); + const fixture = TestBed.createComponent(EmbeddedViewsComponent); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).not.toBe('from tpl'); + + fixture.componentInstance.showing = true; + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toBe('from tpl'); + + fixture.componentInstance.showing = false; + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).not.toBe('from tpl'); + }); + + it('should create and remove embedded views', () => { + @Component({ + selector: 'app-multi', + template: `from tpl + + ` + }) + class NgContainerComponent { + showing = false; + } + + TestBed.configureTestingModule({declarations: [NgContainerComponent]}); + const fixture = TestBed.createComponent(NgContainerComponent); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).not.toBe('from tpl'); + + fixture.componentInstance.showing = true; + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toBe('from tpl'); + + fixture.componentInstance.showing = false; + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).not.toBe('from tpl'); + }); + }); +}); diff --git a/packages/core/test/render3/common_integration_spec.ts b/packages/core/test/render3/common_integration_spec.ts deleted file mode 100644 index 029c154027..0000000000 --- a/packages/core/test/render3/common_integration_spec.ts +++ /dev/null @@ -1,1094 +0,0 @@ -/** - * @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 {AttributeMarker, ɵɵdefineComponent, ɵɵelement, ɵɵgetCurrentView, ɵɵtemplateRefExtractor} from '../../src/render3/index'; -import {ɵɵbind, ɵɵelementContainerEnd, ɵɵelementContainerStart, ɵɵelementEnd, ɵɵelementProperty, ɵɵelementStart, ɵɵinterpolation1, ɵɵinterpolation2, ɵɵinterpolation3, ɵɵinterpolationV, ɵɵlistener, ɵɵload, ɵɵnextContext, ɵɵreference, ɵɵtemplate, ɵɵtext, ɵɵtextBinding} from '../../src/render3/instructions/all'; -import {RenderFlags} from '../../src/render3/interfaces/definition'; -import {ɵɵrestoreView} from '../../src/render3/state'; - -import {NgForOf, NgIf, NgTemplateOutlet} from './common_with_def'; -import {ComponentFixture, createDirective, getDirectiveOnNode} from './render_util'; - -describe('@angular/common integration', () => { - - describe('NgForOf', () => { - it('should update a loop', () => { - function liTemplate(rf: RenderFlags, ctx: NgForOfContext) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'li'); - { ɵɵtext(1); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const item = ctx.$implicit; - ɵɵtextBinding(1, ɵɵbind(item)); - } - } - - class MyApp { - items: string[] = ['first', 'second']; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 2, - vars: 1, - // - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'ul'); - { - ɵɵtemplate( - 1, liTemplate, 2, 1, 'li', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(ctx.items)); - } - }, - directives: () => [NgForOf] - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html).toEqual(''); - - // change detection cycle, no model changes - fixture.update(); - expect(fixture.html).toEqual(''); - - // remove the last item - fixture.component.items.length = 1; - fixture.update(); - expect(fixture.html).toEqual(''); - - // change an item - fixture.component.items[0] = 'one'; - fixture.update(); - expect(fixture.html).toEqual(''); - - // add an item - fixture.component.items.push('two'); - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - it('should support ngForOf context variables', () => { - function liTemplate(rf: RenderFlags, ctx: NgForOfContext) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'li'); - { ɵɵtext(1); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const item = ctx.$implicit; - ɵɵtextBinding(1, ɵɵinterpolation3('', ctx.index, ' of ', ctx.count, ': ', item, '')); - } - } - - class MyApp { - items: string[] = ['first', 'second']; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 2, - vars: 1, - // - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'ul'); - { - ɵɵtemplate( - 1, liTemplate, 2, 3, 'li', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(ctx.items)); - } - - }, - directives: () => [NgForOf] - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html).toEqual(''); - - fixture.component.items.splice(1, 0, 'middle'); - fixture.update(); - expect(fixture.html) - .toEqual(''); - }); - - it('should instantiate directives inside directives properly in an ngFor', () => { - let dirs: any[] = []; - - const Dir = createDirective('dir'); - - class Comp { - static ngComponentDef = ɵɵdefineComponent({ - type: Comp, - selectors: [['comp']], - factory: () => new Comp(), - consts: 2, - vars: 0, - template: (rf: RenderFlags, cmp: Comp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div', ['dir', '']); - { ɵɵtext(1, 'comp text'); } - ɵɵelementEnd(); - // testing only - dirs.push(getDirectiveOnNode(0)); - } - }, - directives: [Dir] - }); - } - - function ngForTemplate(rf: RenderFlags, ctx: NgForOfContext) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'comp'); - } - } - - /** */ - class MyApp { - rows: string[] = ['first', 'second']; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 1, - vars: 1, - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, ngForTemplate, 1, 0, 'comp', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'ngForOf', ɵɵbind(ctx.rows)); - } - }, - directives: () => [NgForOf, Comp, Dir] - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html) - .toEqual( - '
comp text
comp text
'); - expect(dirs.length).toBe(2); - expect(dirs[0] instanceof Dir).toBe(true); - expect(dirs[1] instanceof Dir).toBe(true); - - fixture.component.rows.push('third'); - fixture.update(); - expect(dirs.length).toBe(3); - expect(dirs[2] instanceof Dir).toBe(true); - expect(fixture.html) - .toEqual( - '
comp text
comp text
comp text
'); - }); - - it('should retain parent view listeners when the NgFor destroy views', () => { - - function liTemplate(rf: RenderFlags, ctx: NgForOfContext) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'li'); - { ɵɵtext(1); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const item = ctx.$implicit; - ɵɵtextBinding(1, ɵɵinterpolation1('', item, '')); - } - } - - class MyApp { - private _data: number[] = [1, 2, 3]; - items: number[] = []; - - toggle() { - if (this.items.length) { - this.items = []; - } else { - this.items = this._data; - } - } - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 4, - vars: 1, - // - //
    - //
  • {{index}}
  • - //
- template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'button'); - { - ɵɵlistener('click', function() { return ctx.toggle(); }); - ɵɵtext(1, 'Toggle List'); - } - ɵɵelementEnd(); - ɵɵelementStart(2, 'ul'); - { - ɵɵtemplate( - 3, liTemplate, 2, 1, 'li', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(3, 'ngForOf', ɵɵbind(ctx.items)); - } - - }, - directives: () => [NgForOf] - }); - } - - const fixture = new ComponentFixture(MyApp); - const button = fixture.hostElement.querySelector('button') !; - - expect(fixture.html).toEqual('
    '); - - // this will fill the list - fixture.component.toggle(); - fixture.update(); - expect(fixture.html) - .toEqual('
    • 1
    • 2
    • 3
    '); - - button.click(); - fixture.update(); - - expect(fixture.html).toEqual('
      '); - - button.click(); - fixture.update(); - expect(fixture.html) - .toEqual('
      • 1
      • 2
      • 3
      '); - }); - - it('should support multiple levels of embedded templates', () => { - /** - *
        - *
      • - * {{cell}} - {{ row.value }} - {{ items.length }} - * - *
      • - *
      - */ - class MyApp { - items: any[] = [{data: ['1', '2'], value: 'first'}, {data: ['3', '4'], value: 'second'}]; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 2, - vars: 1, - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'ul'); - { - ɵɵtemplate( - 1, liTemplate, 2, 1, 'li', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(ctx.items)); - } - - }, - directives: () => [NgForOf] - }); - } - - function liTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'li'); - { - ɵɵtemplate( - 1, spanTemplate, 2, 3, 'span', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const row = ctx.$implicit as any; - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(row.data)); - } - } - - function spanTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - { ɵɵtext(1); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const cell = ctx.$implicit; - const row = ɵɵnextContext().$implicit as any; - const app = ɵɵnextContext() as any; - ɵɵtextBinding( - 1, ɵɵinterpolation3('', cell, ' - ', row.value, ' - ', app.items.length, '')); - } - } - - const fixture = new ComponentFixture(MyApp); - - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html) - .toEqual( - '
      • 1 - first - 22 - first - 2
      • 3 - second - 24 - second - 2
      '); - - // Remove the last item - fixture.component.items.length = 1; - fixture.update(); - expect(fixture.html) - .toEqual('
      • 1 - first - 12 - first - 1
      '); - - // Change an item - fixture.component.items[0].data[0] = 'one'; - fixture.update(); - expect(fixture.html) - .toEqual('
      • one - first - 12 - first - 1
      '); - - // Add an item - fixture.component.items[1] = {data: ['three', '4'], value: 'third'}; - fixture.update(); - expect(fixture.html) - .toEqual( - '
      • one - first - 22 - first - 2
      • three - third - 24 - third - 2
      '); - }); - - it('should support multiple levels of embedded templates with listeners', () => { - /** - *
      - *

      - * - * {{ row.value }} - {{ name }} - *

      - *
      - */ - class MyApp { - items: any[] = [{data: ['1'], value: 'first'}]; - name = 'app'; - events: string[] = []; - - onClick(value: string, name: string) { this.events.push(value, name); } - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 1, - vars: 1, - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, divTemplate, 2, 1, 'div', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'ngForOf', ɵɵbind(ctx.items)); - } - - }, - directives: () => [NgForOf] - }); - } - - function divTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { ɵɵtemplate(1, pTemplate, 3, 2, 'p', [AttributeMarker.Template, 'ngFor', 'ngForOf']); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const row = ctx.$implicit as any; - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(row.data)); - } - } - - function pTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - const state = ɵɵgetCurrentView(); - ɵɵelementStart(0, 'p'); - { - ɵɵelementStart(1, 'span'); - { - ɵɵlistener('click', () => { - ɵɵrestoreView(state); - const row = ɵɵnextContext().$implicit as any; - const app = ɵɵnextContext(); - app.onClick(row.value, app.name); - }); - } - ɵɵelementEnd(); - ɵɵtext(2); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const row = ɵɵnextContext().$implicit as any; - const app = ɵɵnextContext() as any; - ɵɵtextBinding(2, ɵɵinterpolation2('', row.value, ' - ', app.name, '')); - } - } - - const fixture = new ComponentFixture(MyApp); - - fixture.update(); - expect(fixture.html).toEqual('

      first - app

      '); - - const span = fixture.hostElement.querySelector('span') as any; - span.click(); - expect(fixture.component.events).toEqual(['first', 'app']); - - fixture.component.name = 'new name'; - fixture.update(); - expect(fixture.html).toEqual('

      first - new name

      '); - - span.click(); - expect(fixture.component.events).toEqual(['first', 'app', 'first', 'new name']); - }); - - it('should support skipping contexts', () => { - /** - *
      - *
      - * - * {{ cell.value }} - {{ name }} - * - *
      - *
      - */ - class MyApp { - name = 'app'; - items: any[] = [ - [ - // row - {value: 'one', data: ['1', '2']} // cell - ], - [{value: 'two', data: ['3', '4']}] - ]; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 1, - vars: 1, - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, divTemplate, 2, 1, 'div', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'ngForOf', ɵɵbind(ctx.items)); - } - - }, - directives: () => [NgForOf] - }); - } - - function divTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { - ɵɵtemplate( - 1, innerDivTemplate, 2, 1, 'div', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const row = ctx.$implicit as any; - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(row)); - } - } - - function innerDivTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { - ɵɵtemplate( - 1, spanTemplate, 2, 2, 'span', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const cell = ctx.$implicit as any; - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(cell.data)); - } - } - - function spanTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - { ɵɵtext(1); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const cell = ɵɵnextContext().$implicit as any; - const app = ɵɵnextContext(2) as any; - ɵɵtextBinding(1, ɵɵinterpolation2('', cell.value, ' - ', app.name, '')); - } - } - - const fixture = new ComponentFixture(MyApp); - - fixture.update(); - expect(fixture.html) - .toEqual( - `
      one - appone - app
      two - apptwo - app
      `); - - fixture.component.name = 'other'; - fixture.update(); - expect(fixture.html) - .toEqual( - `
      one - otherone - other
      two - othertwo - other
      `); - }); - - it('should support context for 9+ levels of embedded templates', () => { - /** - * - * - * - * - * - * - * - * - * - * - * {{ item8 }} - {{ item7.value }} - {{ item6.value }}... - * - * - * - * - * - * - * - * - * - */ - class MyApp { - value = 'App'; - items: any[] = [ - { - // item0 - data: [{ - // item1 - data: [{ - // item2 - data: [{ - // item3 - data: [{ - // item4 - data: [{ - // item5 - data: [{ - // item6 - data: [{ - // item7 - data: [ - '1', '2' // item8 - ], - value: 'h' - }], - value: 'g' - }], - value: 'f' - }], - value: 'e' - }], - value: 'd' - }], - value: 'c' - }], - value: 'b' - }], - value: 'a' - }, - { - // item0 - data: [{ - // item1 - data: [{ - // item2 - data: [{ - // item3 - data: [{ - // item4 - data: [{ - // item5 - data: [{ - // item6 - data: [{ - // item7 - data: [ - '3', '4' // item8 - ], - value: 'H' - }], - value: 'G' - }], - value: 'F' - }], - value: 'E' - }], - value: 'D' - }], - value: 'C' - }], - value: 'B' - }], - value: 'A' - } - ]; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 1, - vars: 1, - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, itemTemplate0, 2, 1, 'span', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'ngForOf', ɵɵbind(ctx.items)); - } - - }, - directives: () => [NgForOf] - }); - } - - function itemTemplate0(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - { - ɵɵtemplate( - 1, itemTemplate1, 2, 1, 'span', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const item0 = ctx.$implicit as any; - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(item0.data)); - } - } - - function itemTemplate1(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - { - ɵɵtemplate( - 1, itemTemplate2, 2, 1, 'span', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const item1 = ctx.$implicit as any; - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(item1.data)); - } - } - - function itemTemplate2(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - { - ɵɵtemplate( - 1, itemTemplate3, 2, 1, 'span', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const item2 = ctx.$implicit as any; - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(item2.data)); - } - } - - function itemTemplate3(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - { - ɵɵtemplate( - 1, itemTemplate4, 2, 1, 'span', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const item3 = ctx.$implicit as any; - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(item3.data)); - } - } - - function itemTemplate4(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - { - ɵɵtemplate( - 1, itemTemplate5, 2, 1, 'span', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const item4 = ctx.$implicit as any; - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(item4.data)); - } - } - - function itemTemplate5(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - { - ɵɵtemplate( - 1, itemTemplate6, 2, 1, 'span', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const item5 = ctx.$implicit as any; - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(item5.data)); - } - } - - function itemTemplate6(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - { - ɵɵtemplate( - 1, itemTemplate7, 2, 1, 'span', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const item6 = ctx.$implicit as any; - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(item6.data)); - } - } - - function itemTemplate7(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - { - ɵɵtemplate( - 1, itemTemplate8, 2, 10, 'span', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const item7 = ctx.$implicit as any; - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(item7.data)); - } - } - - function itemTemplate8(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - { ɵɵtext(1); } - ɵɵelementEnd(); - } - - if (rf & RenderFlags.Update) { - const value = ctx.$implicit; - const item7 = ɵɵnextContext().$implicit; - const item6 = ɵɵnextContext().$implicit; - const item5 = ɵɵnextContext().$implicit; - const item4 = ɵɵnextContext().$implicit; - const item3 = ɵɵnextContext().$implicit; - const item2 = ɵɵnextContext().$implicit; - const item1 = ɵɵnextContext().$implicit; - const item0 = ɵɵnextContext().$implicit; - const myApp = ɵɵnextContext(); - ɵɵtextBinding(1, ɵɵinterpolationV([ - '', value, '.', item7.value, '.', item6.value, '.', item5.value, - '.', item4.value, '.', item3.value, '.', item2.value, '.', item1.value, - '.', item0.value, '.', myApp.value, '' - ])); - } - } - - const fixture = new ComponentFixture(MyApp); - - expect(fixture.html) - .toEqual( - '' + - '1.h.g.f.e.d.c.b.a.App' + - '2.h.g.f.e.d.c.b.a.App' + - '' + - '' + - '3.H.G.F.E.D.C.B.A.App' + - '4.H.G.F.E.D.C.B.A.App' + - ''); - }); - - }); - - describe('ngIf', () => { - it('should support sibling ngIfs', () => { - class MyApp { - showing = true; - valueOne = 'one'; - valueTwo = 'two'; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 2, - vars: 2, - /** - *
      {{ valueOne }}
      - *
      {{ valueTwo }}
      - */ - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵtemplate(0, templateOne, 2, 1, 'div', [AttributeMarker.Template, 'ngIf']); - ɵɵtemplate(1, templateTwo, 2, 1, 'div', [AttributeMarker.Template, 'ngIf']); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'ngIf', ɵɵbind(ctx.showing)); - ɵɵelementProperty(1, 'ngIf', ɵɵbind(ctx.showing)); - } - - }, - directives: () => [NgIf] - }); - } - - function templateOne(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { ɵɵtext(1); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const myApp = ɵɵnextContext(); - ɵɵtextBinding(1, ɵɵbind(myApp.valueOne)); - } - } - - function templateTwo(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { ɵɵtext(1); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const myApp = ɵɵnextContext(); - ɵɵtextBinding(1, ɵɵbind(myApp.valueTwo)); - } - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html).toEqual('
      one
      two
      '); - - fixture.component.valueOne = '$$one$$'; - fixture.component.valueTwo = '$$two$$'; - fixture.update(); - expect(fixture.html).toEqual('
      $$one$$
      $$two$$
      '); - }); - - it('should handle nested ngIfs with no intermediate context vars', () => { - /** - *
      - *
      - *
      - */ - template: (rf: RenderFlags, myApp: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵtemplate(0, (rf1: RenderFlags) => { - if (rf1 & RenderFlags.Create) { - ɵɵtext(0, 'from tpl'); - } - }, 1, 0, 'ng-template', undefined, ['tpl', ''], ɵɵtemplateRefExtractor); - ɵɵtemplate( - 2, null, 0, 0, 'ng-template', [AttributeMarker.Bindings, 'ngTemplateOutlet']); - } - if (rf & RenderFlags.Update) { - const tplRef = ɵɵload(1); - ɵɵelementProperty(2, 'ngTemplateOutlet', ɵɵbind(myApp.showing ? tplRef : null)); - } - }, - directives: () => [NgTemplateOutlet] - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html).toEqual(''); - - fixture.component.showing = true; - fixture.update(); - expect(fixture.html).toEqual('from tpl'); - - fixture.component.showing = false; - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - it('should allow usage on ng-container', () => { - class MyApp { - showing = false; - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 3, - vars: 1, - /** - * from tpl - * - */ - template: (rf: RenderFlags, myApp: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵtemplate(0, (rf1: RenderFlags) => { - if (rf1 & RenderFlags.Create) { - ɵɵtext(0, 'from tpl'); - } - }, 1, 0, 'ng-template', undefined, ['tpl', ''], ɵɵtemplateRefExtractor); - ɵɵelementContainerStart(2, [AttributeMarker.Bindings, 'ngTemplateOutlet']); - ɵɵelementContainerEnd(); - } - if (rf & RenderFlags.Update) { - const tplRef = ɵɵreference(1); - ɵɵelementProperty(2, 'ngTemplateOutlet', ɵɵbind(myApp.showing ? tplRef : null)); - } - }, - directives: () => [NgTemplateOutlet] - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html).toEqual(''); - - fixture.component.showing = true; - fixture.update(); - expect(fixture.html).toEqual('from tpl'); - - fixture.component.showing = false; - fixture.update(); - expect(fixture.html).toEqual(''); - - }); - - }); -});