From 8e2e9dcee6dd24cfb4c056a4eca58eda3479b654 Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Wed, 15 May 2019 19:31:25 +0200 Subject: [PATCH] test(ivy): move render3 view_container_ref tests to acceptance (#30488) Moves all manual render3 view_container_ref tests that use property bindings to acceptance tests with TestBed. Two issues surfaced and refer to a difference between Ivy and View engine: * Multi-slot projection is not working with Ivy: FW-1331 * ViewContainerRef throws if index is invalid while View Engine clamped index: FW-1330 PR Close #30488 --- .../acceptance/view_container_ref_spec.ts | 1427 +++++++++++++- .../test/render3/view_container_ref_spec.ts | 1642 +---------------- 2 files changed, 1429 insertions(+), 1640 deletions(-) diff --git a/packages/core/test/acceptance/view_container_ref_spec.ts b/packages/core/test/acceptance/view_container_ref_spec.ts index 82d08ed6e2..98048b7546 100644 --- a/packages/core/test/acceptance/view_container_ref_spec.ts +++ b/packages/core/test/acceptance/view_container_ref_spec.ts @@ -6,8 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ -import {Component, Directive, ElementRef, NO_ERRORS_SCHEMA, QueryList, TemplateRef, ViewChild, ViewChildren, ViewContainerRef, ɵi18nConfigureLocalize} from '@angular/core'; -import {TestBed} from '@angular/core/testing'; +import {CommonModule, DOCUMENT} from '@angular/common'; +import {Compiler, Component, ComponentFactoryResolver, Directive, DoCheck, ElementRef, EmbeddedViewRef, ErrorHandler, NO_ERRORS_SCHEMA, NgModule, OnInit, Pipe, PipeTransform, QueryList, RendererFactory2, Sanitizer, TemplateRef, ViewChild, ViewChildren, ViewContainerRef, ɵi18nConfigureLocalize} from '@angular/core'; +import {Input} from '@angular/core/src/metadata'; +import {ngDevModeResetPerfCounters} from '@angular/core/src/util/ng_dev_mode'; +import {TestBed, TestComponentRenderer} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; import {expect} from '@angular/platform-browser/testing/src/matchers'; import {ivyEnabled, onlyInIvy} from '@angular/private/testing'; @@ -21,6 +25,15 @@ describe('ViewContainerRef', () => { '{$startTagDiv}{$closeTagDiv}{$startTagBefore}{$closeTagBefore}' }; + /** + * Gets the inner HTML of the given element with all HTML comments and Angular internal + * reflect attributes omitted. This makes HTML comparisons easier and less verbose. + */ + function getElementHtml(element: Element) { + return element.innerHTML.replace(//g, '') + .replace(/\sng-reflect-\S*="[^"]*"/g, ''); + } + beforeEach(() => { ɵi18nConfigureLocalize({translations: TRANSLATIONS}); TestBed.configureTestingModule({ @@ -269,8 +282,1418 @@ describe('ViewContainerRef', () => { }); }); + describe('length', () => { + it('should return the number of embedded views', () => { + TestBed.configureTestingModule({declarations: [EmbeddedViewInsertionComp, VCRefDirective]}); + const fixture = TestBed.createComponent(EmbeddedViewInsertionComp); + const vcRefDir = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + fixture.detectChanges(); + + expect(vcRefDir.vcref.length).toEqual(0); + + vcRefDir.createView('A'); + vcRefDir.createView('B'); + vcRefDir.createView('C'); + fixture.detectChanges(); + expect(vcRefDir.vcref.length).toEqual(3); + + vcRefDir.vcref.detach(1); + fixture.detectChanges(); + expect(vcRefDir.vcref.length).toEqual(2); + + vcRefDir.vcref.clear(); + fixture.detectChanges(); + expect(vcRefDir.vcref.length).toEqual(0); + }); + }); + + describe('get and indexOf', () => { + + beforeEach(() => { + TestBed.configureTestingModule({declarations: [EmbeddedViewInsertionComp, VCRefDirective]}); + }); + + it('should retrieve a ViewRef from its index, and vice versa', () => { + const fixture = TestBed.createComponent(EmbeddedViewInsertionComp); + const vcRefDir = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + fixture.detectChanges(); + + vcRefDir.createView('A'); + vcRefDir.createView('B'); + vcRefDir.createView('C'); + fixture.detectChanges(); + + let viewRef = vcRefDir.vcref.get(0); + expect(vcRefDir.vcref.indexOf(viewRef !)).toEqual(0); + + viewRef = vcRefDir.vcref.get(1); + expect(vcRefDir.vcref.indexOf(viewRef !)).toEqual(1); + + viewRef = vcRefDir.vcref.get(2); + expect(vcRefDir.vcref.indexOf(viewRef !)).toEqual(2); + }); + + it('should handle out of bounds cases', () => { + const fixture = TestBed.createComponent(EmbeddedViewInsertionComp); + const vcRefDir = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + fixture.detectChanges(); + + vcRefDir.createView('A'); + fixture.detectChanges(); + + expect(vcRefDir.vcref.get(-1)).toBeNull(); + expect(vcRefDir.vcref.get(42)).toBeNull(); + + const viewRef = vcRefDir.vcref.get(0); + vcRefDir.vcref.remove(0); + expect(vcRefDir.vcref.indexOf(viewRef !)).toEqual(-1); + }); + }); + + describe('move', () => { + it('should move embedded views and associated DOM nodes without recreating them', () => { + TestBed.configureTestingModule({declarations: [EmbeddedViewInsertionComp, VCRefDirective]}); + const fixture = TestBed.createComponent(EmbeddedViewInsertionComp); + const vcRefDir = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + fixture.detectChanges(); + + vcRefDir.createView('A'); + vcRefDir.createView('B'); + vcRefDir.createView('C'); + + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toEqual('

ABC'); + + // The DOM is manually modified here to ensure that the text node is actually moved + fixture.nativeElement.childNodes[2].nodeValue = '**A**'; + expect(getElementHtml(fixture.nativeElement)).toEqual('

**A**BC'); + + let viewRef = vcRefDir.vcref.get(0); + vcRefDir.vcref.move(viewRef !, 2); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toEqual('

BC**A**'); + + vcRefDir.vcref.move(viewRef !, 0); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toEqual('

**A**BC'); + + vcRefDir.vcref.move(viewRef !, 1); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toEqual('

B**A**C'); + + // Invalid indices when detaching throws an exception in Ivy: FW-1330. + ivyEnabled && expect(() => vcRefDir.vcref.move(viewRef !, -1)).toThrow(); + ivyEnabled && expect(() => vcRefDir.vcref.move(viewRef !, 42)).toThrow(); + }); + }); + + describe('getters', () => { + it('should work on templates', () => { + @Component({ + template: ` + {{name}} + + ` + }) + class TestComponent { + @ViewChild(VCRefDirective, {static: true}) vcRefDir !: VCRefDirective; + } + + TestBed.configureTestingModule({declarations: [VCRefDirective, TestComponent]}); + const fixture = TestBed.createComponent(TestComponent); + const {vcRefDir} = fixture.componentInstance; + fixture.detectChanges(); + + expect(vcRefDir.vcref.element.nativeElement.nodeType).toBe(Node.COMMENT_NODE); + // In Ivy, the comment for the view container ref has text that implies + // that the comment is a placeholder for a container. + ivyEnabled && expect(vcRefDir.vcref.element.nativeElement.textContent).toEqual('container'); + + expect(vcRefDir.vcref.injector.get(ElementRef).nativeElement.textContent); + expect(getElementHtml(vcRefDir.vcref.parentInjector.get(ElementRef).nativeElement)) + .toBe(''); + }); + }); + + describe('detach', () => { + + beforeEach(() => { + TestBed.configureTestingModule({declarations: [EmbeddedViewInsertionComp, VCRefDirective]}); + + // Tests depend on perf counters when running with Ivy. In order to have + // clean perf counters at the beginning of a test, we reset those here. + ivyEnabled && ngDevModeResetPerfCounters(); + }); + + it('should detach the right embedded view when an index is specified', () => { + const fixture = TestBed.createComponent(EmbeddedViewInsertionComp); + const vcRefDir = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + fixture.detectChanges(); + + const viewA = vcRefDir.createView('A'); + vcRefDir.createView('B'); + vcRefDir.createView('C'); + const viewD = vcRefDir.createView('D'); + vcRefDir.createView('E'); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toEqual('

ABCDE'); + + vcRefDir.vcref.detach(3); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toEqual('

ABCE'); + expect(viewD.destroyed).toBeFalsy(); + + vcRefDir.vcref.detach(0); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toEqual('

BCE'); + expect(viewA.destroyed).toBeFalsy(); + + // Invalid indices when detaching throws an exception in Ivy: FW-1330. + ivyEnabled && expect(() => vcRefDir.vcref.detach(-1)).toThrow(); + ivyEnabled && expect(() => vcRefDir.vcref.detach(42)).toThrow(); + ivyEnabled && expect(ngDevMode !.rendererDestroyNode).toBe(0); + }); + + it('should detach the last embedded view when no index is specified', () => { + const fixture = TestBed.createComponent(EmbeddedViewInsertionComp); + const vcRefDir = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + fixture.detectChanges(); + + vcRefDir.createView('A'); + vcRefDir.createView('B'); + vcRefDir.createView('C'); + vcRefDir.createView('D'); + const viewE = vcRefDir.createView('E'); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toEqual('

ABCDE'); + + vcRefDir.vcref.detach(); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toEqual('

ABCD'); + expect(viewE.destroyed).toBeFalsy(); + ivyEnabled && expect(ngDevMode !.rendererDestroyNode).toBe(0); + }); + }); + + describe('remove', () => { + beforeEach(() => { + TestBed.configureTestingModule({declarations: [EmbeddedViewInsertionComp, VCRefDirective]}); + + const _origRendererFactory = TestBed.get(RendererFactory2) as RendererFactory2; + const _origCreateRenderer = _origRendererFactory.createRenderer; + + _origRendererFactory.createRenderer = function() { + const renderer = _origCreateRenderer.apply(_origRendererFactory, arguments); + renderer.destroyNode = () => {}; + return renderer; + }; + + // Tests depend on perf counters when running with Ivy. In order to have + // clean perf counters at the beginning of a test, we reset those here. + ivyEnabled && ngDevModeResetPerfCounters(); + }); + + it('should remove the right embedded view when an index is specified', () => { + const fixture = TestBed.createComponent(EmbeddedViewInsertionComp); + const vcRefDir = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + fixture.detectChanges(); + + const viewA = vcRefDir.createView('A'); + vcRefDir.createView('B'); + vcRefDir.createView('C'); + const viewD = vcRefDir.createView('D'); + vcRefDir.createView('E'); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toEqual('

ABCDE'); + + vcRefDir.vcref.remove(3); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toEqual('

ABCE'); + expect(viewD.destroyed).toBeTruthy(); + + vcRefDir.vcref.remove(0); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toEqual('

BCE'); + expect(viewA.destroyed).toBeTruthy(); + + // Invalid indices when detaching throws an exception in Ivy: FW-1330. + ivyEnabled && expect(() => vcRefDir.vcref.remove(-1)).toThrow(); + ivyEnabled && expect(() => vcRefDir.vcref.remove(42)).toThrow(); + ivyEnabled && expect(ngDevMode !.rendererDestroyNode).toBe(2); + }); + + it('should remove the last embedded view when no index is specified', () => { + const fixture = TestBed.createComponent(EmbeddedViewInsertionComp); + const vcRefDir = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + fixture.detectChanges(); + + vcRefDir.createView('A'); + vcRefDir.createView('B'); + vcRefDir.createView('C'); + vcRefDir.createView('D'); + const viewE = vcRefDir.createView('E'); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toEqual('

ABCDE'); + + vcRefDir.vcref.remove(); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toEqual('

ABCD'); + expect(viewE.destroyed).toBeTruthy(); + ivyEnabled && expect(ngDevMode !.rendererDestroyNode).toBe(1); + }); + + it('should throw when trying to insert a removed or destroyed view', () => { + const fixture = TestBed.createComponent(EmbeddedViewInsertionComp); + const vcRefDir = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + fixture.detectChanges(); + + const viewA = vcRefDir.createView('A'); + const viewB = vcRefDir.createView('B'); + fixture.detectChanges(); + + vcRefDir.vcref.remove(); + fixture.detectChanges(); + expect(() => vcRefDir.vcref.insert(viewB)).toThrow(); + + viewA.destroy(); + fixture.detectChanges(); + expect(() => vcRefDir.vcref.insert(viewA)).toThrow(); + }); + }); + + describe('createEmbeddedView (incl. insert)', () => { + it('should work on elements', () => { + @Component({ + template: ` + {{name}} +
+ + `, + }) + class TestComponent { + } + + TestBed.configureTestingModule({declarations: [TestComponent, VCRefDirective]}); + + const fixture = TestBed.createComponent(TestComponent); + const vcRef = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)) + .toEqual('
'); + + vcRef.createView('A'); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)) + .toEqual('
A'); + + vcRef.createView('B'); + vcRef.createView('C'); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)) + .toEqual('
ABC'); + + vcRef.createView('Y', 0); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)) + .toEqual('
YABC'); + + // Invalid indices when detaching throws an exception in Ivy: FW-1330. + ivyEnabled && expect(() => vcRef.createView('Z', -1)).toThrow(); + ivyEnabled && expect(() => vcRef.createView('Z', 5)).toThrow(); + }); + + it('should work on components', () => { + @Component({selector: 'header-cmp', template: ``}) + class HeaderComponent { + } + + @Component({ + template: ` + {{name}} + + + `, + }) + class TestComponent { + } + + TestBed.configureTestingModule( + {declarations: [TestComponent, HeaderComponent, VCRefDirective]}); + const fixture = TestBed.createComponent(TestComponent); + const vcRef = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + fixture.detectChanges(); + + expect(getElementHtml(fixture.nativeElement)) + .toEqual(''); + + vcRef.createView('A'); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)) + .toEqual('A'); + + vcRef.createView('B'); + vcRef.createView('C'); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)) + .toEqual('ABC'); + + vcRef.createView('Y', 0); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)) + .toEqual('YABC'); + + // Invalid indices when detaching throws an exception in Ivy: FW-1330. + ivyEnabled && expect(() => vcRef.createView('Z', -1)).toThrow(); + ivyEnabled && expect(() => vcRef.createView('Z', 5)).toThrow(); + }); + + it('should work with multiple instances of view container refs', () => { + @Component({ + template: ` + {{name}} +
+
+ `, + }) + class TestComponent { + } + + TestBed.configureTestingModule({declarations: [TestComponent, VCRefDirective]}); + const fixture = TestBed.createComponent(TestComponent); + const vcRefs = fixture.debugElement.queryAll(By.directive(VCRefDirective)) + .map(debugEl => debugEl.injector.get(VCRefDirective)); + fixture.detectChanges(); + + expect(getElementHtml(fixture.nativeElement)) + .toEqual('
'); + + vcRefs[0].createView('A'); + vcRefs[1].createView('B'); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)) + .toEqual('
A
B'); + }); + + it('should work on templates', () => { + @Component({ + template: ` + {{name}} + + ` + }) + class TestComponent { + @ViewChild(VCRefDirective, {static: true}) vcRef !: VCRefDirective; + } + + TestBed.configureTestingModule({declarations: [TestComponent, VCRefDirective]}); + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + const {vcRef} = fixture.componentInstance; + + expect(getElementHtml(fixture.nativeElement)).toEqual(''); + + vcRef.createView('A'); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toEqual('A'); + + vcRef.createView('B'); + vcRef.createView('C'); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toEqual('ABC'); + + vcRef.createView('Y', 0); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toEqual('YABC'); + + // Invalid indices when detaching throws an exception in Ivy: FW-1330. + ivyEnabled && expect(() => vcRef !.createView('Z', -1)).toThrow(); + ivyEnabled && expect(() => vcRef !.createView('Z', 5)).toThrow(); + }); + + it('should apply directives and pipes of the host view to the TemplateRef', () => { + @Component({selector: 'child', template: `{{name}}`}) + class Child { + @Input() name: string|undefined; + } + + @Pipe({name: 'starPipe'}) + class StarPipe implements PipeTransform { + transform(value: any) { return `**${value}**`; } + } + + @Component({ + template: ` + + + + + + ` + }) + class SomeComponent { + } + + TestBed.configureTestingModule( + {declarations: [Child, StarPipe, SomeComponent, VCRefDirective]}); + const fixture = TestBed.createComponent(SomeComponent); + const vcRefDir = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + fixture.detectChanges(); + + vcRefDir.vcref.createEmbeddedView(vcRefDir.tplRef !); + vcRefDir.vcref.createEmbeddedView(vcRefDir.tplRef !); + fixture.detectChanges(); + + expect(getElementHtml(fixture.nativeElement)) + .toEqual( + '**A****C****C****B**'); + }); + + }); + + describe('createComponent', () => { + let templateExecutionCounter = 0; + + beforeEach(() => templateExecutionCounter = 0); + + it('should work without Injector and NgModuleRef', () => { + @Component({selector: 'embedded-cmp', template: `foo`}) + class EmbeddedComponent implements DoCheck, OnInit { + ngOnInit() { templateExecutionCounter++; } + + ngDoCheck() { templateExecutionCounter++; } + } + + @NgModule({entryComponents: [EmbeddedComponent], declarations: [EmbeddedComponent]}) + class EmbeddedComponentModule { + } + + TestBed.configureTestingModule({ + declarations: [EmbeddedViewInsertionComp, VCRefDirective], + imports: [EmbeddedComponentModule] + }); + const fixture = TestBed.createComponent(EmbeddedViewInsertionComp); + const vcRefDir = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + fixture.detectChanges(); + + expect(getElementHtml(fixture.nativeElement)).toEqual('

'); + expect(templateExecutionCounter).toEqual(0); + + const componentRef = + vcRefDir.vcref.createComponent(vcRefDir.cfr.resolveComponentFactory(EmbeddedComponent)); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)) + .toEqual('

foo'); + expect(templateExecutionCounter).toEqual(2); + + vcRefDir.vcref.detach(0); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toEqual('

'); + expect(templateExecutionCounter).toEqual(2); + + vcRefDir.vcref.insert(componentRef.hostView); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)) + .toEqual('

foo'); + expect(templateExecutionCounter).toEqual(3); + }); + + it('should work with NgModuleRef and Injector', () => { + @Component({ + selector: 'embedded-cmp', + template: `foo`, + }) + class EmbeddedComponent implements DoCheck, + OnInit { + constructor(public s: String) {} + + ngOnInit() { templateExecutionCounter++; } + + ngDoCheck() { templateExecutionCounter++; } + } + + @NgModule({entryComponents: [EmbeddedComponent], declarations: [EmbeddedComponent]}) + class EmbeddedComponentModule { + } + + TestBed.configureTestingModule({ + declarations: [EmbeddedViewInsertionComp, VCRefDirective], + imports: [EmbeddedComponentModule] + }); + + @NgModule({ + providers: [ + {provide: String, useValue: 'root_module'}, + // We need to provide the following tokens because otherwise view engine + // will throw when creating a component factory in debug mode. + {provide: Sanitizer, useValue: TestBed.get(Sanitizer)}, + {provide: ErrorHandler, useValue: TestBed.get(ErrorHandler)}, + {provide: RendererFactory2, useValue: TestBed.get(RendererFactory2)}, + ] + }) + class MyAppModule { + } + + @NgModule({providers: [{provide: String, useValue: 'some_module'}]}) + class SomeModule { + } + + // Compile test modules in order to be able to pass the NgModuleRef or the + // module injector to the ViewContainerRef create component method. + const compiler = TestBed.get(Compiler) as Compiler; + const appModuleFactory = compiler.compileModuleSync(MyAppModule); + const someModuleFactory = compiler.compileModuleSync(SomeModule); + const appModuleRef = appModuleFactory.create(null); + const someModuleRef = someModuleFactory.create(null); + + const fixture = TestBed.createComponent(EmbeddedViewInsertionComp); + const vcRefDir = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + fixture.detectChanges(); + + expect(getElementHtml(fixture.nativeElement)).toEqual('

'); + expect(templateExecutionCounter).toEqual(0); + + let componentRef = vcRefDir.vcref.createComponent( + vcRefDir.cfr.resolveComponentFactory(EmbeddedComponent), 0, someModuleRef.injector); + fixture.detectChanges(); + + expect(getElementHtml(fixture.nativeElement)) + .toEqual('

foo'); + expect(templateExecutionCounter).toEqual(2); + expect(componentRef.instance.s).toEqual('some_module'); + + componentRef = vcRefDir.vcref.createComponent( + vcRefDir.cfr.resolveComponentFactory(EmbeddedComponent), 0, undefined, undefined, + appModuleRef); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)) + .toEqual( + '

foofoo'); + expect(componentRef.instance.s).toEqual('root_module'); + expect(templateExecutionCounter).toEqual(5); + }); + + it('should support projectable nodes', () => { + TestBed.configureTestingModule({ + declarations: [EmbeddedViewInsertionComp, VCRefDirective], + imports: [EmbeddedComponentWithNgZoneModule] + }); + const fixture = TestBed.createComponent(EmbeddedViewInsertionComp); + const vcRefDir = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + fixture.detectChanges(); + + expect(getElementHtml(fixture.nativeElement)).toEqual('

'); + + const myNode = document.createElement('div'); + const myText = document.createTextNode('bar'); + const myText2 = document.createTextNode('baz'); + myNode.appendChild(myText); + myNode.appendChild(myText2); + + vcRefDir.vcref.createComponent( + vcRefDir.cfr.resolveComponentFactory(EmbeddedComponentWithNgContent), 0, undefined, + [[myNode]]); + fixture.detectChanges(); + + // With Ivy the projected content is inserted into the last ng-content container, + // while with View Engine the content is projected into the first ng-content slot. + // View Engine correctly respects the passed index of "projectedNodes". See: FW-1331. + if (ivyEnabled) { + expect(getElementHtml(fixture.nativeElement)) + .toEqual( + '


barbaz
'); + } else { + expect(getElementHtml(fixture.nativeElement)) + .toEqual( + '

barbaz

'); + } + }); + + it('should support reprojection of projectable nodes', () => { + @Component({ + selector: 'reprojector', + template: + ``, + }) + class Reprojector { + } + + @NgModule({ + exports: [Reprojector, EmbeddedComponentWithNgContent], + declarations: [Reprojector, EmbeddedComponentWithNgContent], + entryComponents: [Reprojector] + }) + class ReprojectorModule { + } + + TestBed.configureTestingModule({ + declarations: [EmbeddedViewInsertionComp, VCRefDirective], + imports: [ReprojectorModule] + }); + const fixture = TestBed.createComponent(EmbeddedViewInsertionComp); + const vcRefDir = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + fixture.detectChanges(); + + expect(getElementHtml(fixture.nativeElement)).toEqual('

'); + + const myNode = document.createElement('div'); + const myText = document.createTextNode('bar'); + const myText2 = document.createTextNode('baz'); + myNode.appendChild(myText); + myNode.appendChild(myText2); + + vcRefDir.vcref.createComponent( + vcRefDir.cfr.resolveComponentFactory(Reprojector), 0, undefined, [[myNode]]); + fixture.detectChanges(); + + expect(getElementHtml(fixture.nativeElement)) + .toEqual( + '


barbaz
'); + }); + + it('should support many projectable nodes with many slots', () => { + TestBed.configureTestingModule({ + declarations: [EmbeddedViewInsertionComp, VCRefDirective], + imports: [EmbeddedComponentWithNgZoneModule] + }); + const fixture = TestBed.createComponent(EmbeddedViewInsertionComp); + const vcRefDir = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + fixture.detectChanges(); + + expect(getElementHtml(fixture.nativeElement)).toEqual('

'); + + vcRefDir.vcref.createComponent( + vcRefDir.cfr.resolveComponentFactory(EmbeddedComponentWithNgContent), 0, undefined, [ + [document.createTextNode('1'), document.createTextNode('2')], + [document.createTextNode('3'), document.createTextNode('4')] + ]); + fixture.detectChanges(); + + // With Ivy multi-slot projection is currently not working. This is a bug that + // is tracked with FW-1333. + if (ivyEnabled) { + expect(getElementHtml(fixture.nativeElement)) + .toEqual( + '


12
'); + } else { + expect(getElementHtml(fixture.nativeElement)) + .toEqual( + '

12
34
'); + } + }); + }); + + describe('insertion points and declaration points', () => { + @Directive({selector: '[tplDir]'}) + class InsertionDir { + @Input() + set tplDir(tpl: TemplateRef|null) { + tpl ? this.vcr.createEmbeddedView(tpl) : this.vcr.clear(); + } + + constructor(public vcr: ViewContainerRef) {} + } + + // see running stackblitz example: https://stackblitz.com/edit/angular-w3myy6 + it('should work with a template declared in a different component view from insertion', () => { + @Component({selector: 'child', template: `
{{name}}
`}) + class Child { + @Input() tpl: TemplateRef|null = null; + name = 'Child'; + } + + @Component({ + template: ` + +
{{name}}
+
+ + + ` + }) + class Parent { + name = 'Parent'; + } + + TestBed.configureTestingModule({declarations: [Child, Parent, InsertionDir]}); + const fixture = TestBed.createComponent(Parent); + const child = fixture.debugElement.query(By.directive(Child)).componentInstance; + fixture.detectChanges(); + + // Context should be inherited from the declaration point, not the + // insertion point, so the template should read 'Parent'. + expect(getElementHtml(fixture.nativeElement)) + .toEqual(`
Child
Parent
`); + + child.tpl = null; + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toEqual(`
Child
`); + }); + + // see running stackblitz example: https://stackblitz.com/edit/angular-3vplec + it('should work with nested for loops with different declaration / insertion points', () => { + @Component({ + selector: 'loop-comp', + template: ` + + + `, + }) + class LoopComp { + @Input() tpl !: TemplateRef; + @Input() rows !: any[]; + name = 'Loop'; + } + + @Component({ + template: ` + + +
{{cell}} - {{row.value}} - {{name}}
+
+ + +
+ + + `, + }) + class Parent { + name = 'Parent'; + rows = [{data: ['1', '2'], value: 'one'}, {data: ['3', '4'], value: 'two'}]; + } + + TestBed.configureTestingModule({declarations: [LoopComp, Parent], imports: [CommonModule]}); + const fixture = TestBed.createComponent(Parent); + fixture.detectChanges(); + + expect(getElementHtml(fixture.nativeElement)) + .toEqual( + '' + + '
1 - one - Parent
2 - one - Parent
' + + '
3 - two - Parent
4 - two - Parent
' + + '
'); + + fixture.componentInstance.rows = + [{data: ['5', '6'], value: 'three'}, {data: ['7'], value: 'four'}]; + fixture.componentInstance.name = 'New name!'; + fixture.detectChanges(); + + expect(getElementHtml(fixture.nativeElement)) + .toEqual( + '' + + '
5 - three - New name!
6 - three - New name!
' + + '
7 - four - New name!
' + + '
'); + }); + + }); + + describe('lifecycle hooks', () => { + + // Angular 5 reference: https://stackblitz.com/edit/lifecycle-hooks-vcref + const log: string[] = []; + + @Component({selector: 'hooks', template: `{{name}}`}) + class ComponentWithHooks { + @Input() name: string|undefined; + + private log(msg: string) { log.push(msg); } + + ngOnChanges() { this.log('onChanges-' + this.name); } + ngOnInit() { this.log('onInit-' + this.name); } + ngDoCheck() { this.log('doCheck-' + this.name); } + + ngAfterContentInit() { this.log('afterContentInit-' + this.name); } + ngAfterContentChecked() { this.log('afterContentChecked-' + this.name); } + + ngAfterViewInit() { this.log('afterViewInit-' + this.name); } + ngAfterViewChecked() { this.log('afterViewChecked-' + this.name); } + + ngOnDestroy() { this.log('onDestroy-' + this.name); } + } + + @NgModule({ + declarations: [ComponentWithHooks], + exports: [ComponentWithHooks], + entryComponents: [ComponentWithHooks] + }) + class ComponentWithHooksModule { + } + + it('should call all hooks in correct order when creating with createEmbeddedView', () => { + @Component({ + template: ` + + + + + + ` + }) + class SomeComponent { + } + + log.length = 0; + + TestBed.configureTestingModule({ + declarations: [SomeComponent, ComponentWithHooks, VCRefDirective], + }); + const fixture = TestBed.createComponent(SomeComponent); + const vcRefDir = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + + fixture.detectChanges(); + expect(log).toEqual([ + 'onChanges-A', 'onInit-A', 'doCheck-A', 'onChanges-B', 'onInit-B', 'doCheck-B', + 'afterContentInit-A', 'afterContentChecked-A', 'afterContentInit-B', + 'afterContentChecked-B', 'afterViewInit-A', 'afterViewChecked-A', 'afterViewInit-B', + 'afterViewChecked-B' + ]); + + log.length = 0; + fixture.detectChanges(); + expect(log).toEqual([ + 'doCheck-A', 'doCheck-B', 'afterContentChecked-A', 'afterContentChecked-B', + 'afterViewChecked-A', 'afterViewChecked-B' + ]); + + log.length = 0; + vcRefDir.vcref.createEmbeddedView(vcRefDir.tplRef !); + expect(getElementHtml(fixture.nativeElement)) + .toEqual('AB'); + expect(log).toEqual([]); + + log.length = 0; + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)) + .toEqual('ACB'); + expect(log).toEqual([ + 'doCheck-A', 'doCheck-B', 'onChanges-C', 'onInit-C', 'doCheck-C', 'afterContentInit-C', + 'afterContentChecked-C', 'afterViewInit-C', 'afterViewChecked-C', 'afterContentChecked-A', + 'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B' + ]); + + log.length = 0; + fixture.detectChanges(); + expect(log).toEqual([ + 'doCheck-A', 'doCheck-B', 'doCheck-C', 'afterContentChecked-C', 'afterViewChecked-C', + 'afterContentChecked-A', 'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B' + ]); + + log.length = 0; + const viewRef = vcRefDir.vcref.detach(0); + fixture.detectChanges(); + expect(log).toEqual([ + 'doCheck-A', 'doCheck-B', 'afterContentChecked-A', 'afterContentChecked-B', + 'afterViewChecked-A', 'afterViewChecked-B' + ]); + + log.length = 0; + vcRefDir.vcref.insert(viewRef !); + fixture.detectChanges(); + expect(log).toEqual([ + 'doCheck-A', 'doCheck-B', 'doCheck-C', 'afterContentChecked-C', 'afterViewChecked-C', + 'afterContentChecked-A', 'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B' + ]); + + log.length = 0; + vcRefDir.vcref.remove(0); + fixture.detectChanges(); + expect(log).toEqual([ + 'onDestroy-C', 'doCheck-A', 'doCheck-B', 'afterContentChecked-A', 'afterContentChecked-B', + 'afterViewChecked-A', 'afterViewChecked-B' + ]); + }); + + it('should call all hooks in correct order when creating with createComponent', () => { + @Component({ + template: ` + + + ` + }) + class SomeComponent { + } + + log.length = 0; + + TestBed.configureTestingModule( + {declarations: [SomeComponent, VCRefDirective], imports: [ComponentWithHooksModule]}); + const fixture = TestBed.createComponent(SomeComponent); + const vcRefDir = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + + fixture.detectChanges(); + expect(log).toEqual([ + 'onChanges-A', 'onInit-A', 'doCheck-A', 'onChanges-B', 'onInit-B', 'doCheck-B', + 'afterContentInit-A', 'afterContentChecked-A', 'afterContentInit-B', + 'afterContentChecked-B', 'afterViewInit-A', 'afterViewChecked-A', 'afterViewInit-B', + 'afterViewChecked-B' + ]); + + log.length = 0; + fixture.detectChanges(); + expect(log).toEqual([ + 'doCheck-A', 'doCheck-B', 'afterContentChecked-A', 'afterContentChecked-B', + 'afterViewChecked-A', 'afterViewChecked-B' + ]); + + log.length = 0; + const componentRef = + vcRefDir.vcref.createComponent(vcRefDir.cfr.resolveComponentFactory(ComponentWithHooks)); + expect(getElementHtml(fixture.nativeElement)) + .toEqual('AB'); + expect(log).toEqual([]); + + componentRef.instance.name = 'D'; + log.length = 0; + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)) + .toEqual('ADB'); + expect(log).toEqual([ + 'doCheck-A', 'doCheck-B', 'onInit-D', 'doCheck-D', 'afterContentInit-D', + 'afterContentChecked-D', 'afterViewInit-D', 'afterViewChecked-D', 'afterContentChecked-A', + 'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B' + ]); + + log.length = 0; + fixture.detectChanges(); + expect(log).toEqual([ + 'doCheck-A', 'doCheck-B', 'doCheck-D', 'afterContentChecked-D', 'afterViewChecked-D', + 'afterContentChecked-A', 'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B' + ]); + + log.length = 0; + const viewRef = vcRefDir.vcref.detach(0); + fixture.detectChanges(); + expect(log).toEqual([ + 'doCheck-A', 'doCheck-B', 'afterContentChecked-A', 'afterContentChecked-B', + 'afterViewChecked-A', 'afterViewChecked-B' + ]); + + log.length = 0; + vcRefDir.vcref.insert(viewRef !); + fixture.detectChanges(); + expect(log).toEqual([ + 'doCheck-A', 'doCheck-B', 'doCheck-D', 'afterContentChecked-D', 'afterViewChecked-D', + 'afterContentChecked-A', 'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B' + ]); + + log.length = 0; + vcRefDir.vcref.remove(0); + fixture.detectChanges(); + expect(log).toEqual([ + 'onDestroy-D', 'doCheck-A', 'doCheck-B', 'afterContentChecked-A', 'afterContentChecked-B', + 'afterViewChecked-A', 'afterViewChecked-B' + ]); + }); + }); + + describe('host bindings', () => { + + it('should support host bindings on dynamically created components', () => { + @Component( + {selector: 'host-bindings', host: {'id': 'attribute', '[title]': 'title'}, template: ``}) + class HostBindingCmpt { + title = 'initial'; + } + + @Component({template: ``}) + class TestComponent { + @ViewChild(VCRefDirective, {static: true}) vcRefDir !: VCRefDirective; + } + + @NgModule({declarations: [HostBindingCmpt], entryComponents: [HostBindingCmpt]}) + class TestModule { + } + + TestBed.configureTestingModule( + {declarations: [TestComponent, VCRefDirective], imports: [TestModule]}); + const fixture = TestBed.createComponent(TestComponent); + const {vcRefDir} = fixture.componentInstance; + + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toBe(''); + + const componentRef = + vcRefDir.vcref.createComponent(vcRefDir.cfr.resolveComponentFactory(HostBindingCmpt)); + fixture.detectChanges(); + + expect(fixture.nativeElement.children[0].tagName).toBe('HOST-BINDINGS'); + expect(fixture.nativeElement.children[0].getAttribute('id')).toBe('attribute'); + expect(fixture.nativeElement.children[0].getAttribute('title')).toBe('initial'); + + componentRef.instance.title = 'changed'; + fixture.detectChanges(); + + expect(fixture.nativeElement.children[0].tagName).toBe('HOST-BINDINGS'); + expect(fixture.nativeElement.children[0].getAttribute('id')).toBe('attribute'); + expect(fixture.nativeElement.children[0].getAttribute('title')).toBe('changed'); + }); + + }); + + describe('projection', () => { + + it('should project the ViewContainerRef content along its host, in an element', () => { + @Component({selector: 'child', template: '
'}) + class Child { + } + + @Component({ + selector: 'parent', + template: ` + + {{name}} + + + +
blah
+
` + }) + class Parent { + name: string = 'bar'; + } + + TestBed.configureTestingModule({declarations: [Child, Parent, VCRefDirective]}); + const fixture = TestBed.createComponent(Parent); + const vcRefDir = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + fixture.detectChanges(); + + expect(getElementHtml(fixture.nativeElement)) + .toEqual('
blah
'); + + vcRefDir.vcref.createEmbeddedView(vcRefDir.tplRef !); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)) + .toEqual('
blah
bar
'); + }); + + it('should project the ViewContainerRef content along its host, in a view', () => { + @Component({ + selector: 'child-with-view', + template: `Before (inside)--After (inside)` + }) + class ChildWithView { + show: boolean = true; + } + + @Component({ + selector: 'parent', + template: ` + + {{name}} + + + Before projected +
blah
+ After projected +
` + }) + class Parent { + name: string = 'bar'; + } + + TestBed.configureTestingModule({declarations: [ChildWithView, Parent, VCRefDirective]}); + const fixture = TestBed.createComponent(Parent); + fixture.detectChanges(); + const vcRefDir = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + + expect(getElementHtml(fixture.nativeElement)) + .toEqual( + 'Before (inside)- Before projected
blah
After projected -After (inside)
'); + + vcRefDir.vcref.createEmbeddedView(vcRefDir.tplRef !); + fixture.detectChanges(); + + expect(getElementHtml(fixture.nativeElement)) + .toEqual( + 'Before (inside)- Before projected
blah
bar After projected -After (inside)
'); + }); + + describe('with select', () => { + + @Component({ + selector: 'child-with-selector', + template: ` +

+

`, + }) + class ChildWithSelector { + } + + it('should project the ViewContainerRef content along its host, when the host matches a selector', + () => { + @Component({ + selector: 'parent', + template: ` + + {{name}} + + +
blah
+
+ ` + }) + class Parent { + name: string = 'bar'; + } + + TestBed.configureTestingModule( + {declarations: [Parent, ChildWithSelector, VCRefDirective]}); + const fixture = TestBed.createComponent(Parent); + const vcRefDir = fixture.debugElement.query(By.directive(VCRefDirective)) + .injector.get(VCRefDirective); + fixture.detectChanges(); + + expect(getElementHtml(fixture.nativeElement)) + .toEqual( + '

blah

'); + + vcRefDir.vcref.createEmbeddedView(vcRefDir.tplRef !); + fixture.detectChanges(); + + expect(getElementHtml(fixture.nativeElement)) + .toEqual( + '

blah
bar

'); + }); + + it('should not project the ViewContainerRef content, when the host does not match a selector', + () => { + @Component({ + selector: 'parent', + template: ` + + {{name}} + + +
blah
+
+ ` + }) + class Parent { + name: string = 'bar'; + } + + TestBed.configureTestingModule( + {declarations: [Parent, ChildWithSelector, VCRefDirective]}); + const fixture = TestBed.createComponent(Parent); + const vcRefDir = fixture.debugElement.query(By.directive(VCRefDirective)) + .injector.get(VCRefDirective); + fixture.detectChanges(); + + expect(getElementHtml(fixture.nativeElement)) + .toEqual( + '

blah

'); + + vcRefDir.vcref.createEmbeddedView(vcRefDir.tplRef !); + fixture.detectChanges(); + + expect(getElementHtml(fixture.nativeElement)) + .toEqual( + '

blah
bar

'); + }); + }); + + }); + + describe('root view container ref', () => { + let containerEl: HTMLElement|null = null; + + beforeEach(() => containerEl = null); + + /** + * Creates a new test component renderer instance that wraps the root element + * in another element. This allows us to test if elements have been inserted into + * the parent element of the root component. + */ + function createTestComponentRenderer(document: any): TestComponentRenderer { + return { + insertRootElement(rootElementId: string) { + const rootEl = document.createElement('div'); + rootEl.id = rootElementId; + + containerEl = document.createElement('div'); + document.body.appendChild(containerEl); + containerEl !.appendChild(rootEl); + } + }; + } + + const TEST_COMPONENT_RENDERER = { + provide: TestComponentRenderer, + useFactory: createTestComponentRenderer, + deps: [DOCUMENT] + }; + + it('should check bindings for components dynamically created by root component', () => { + @Component({ + selector: 'dynamic-cmpt-with-bindings', + template: `check count: {{checkCount}}`, + }) + class DynamicCompWithBindings implements DoCheck { + checkCount = 0; + + ngDoCheck() { this.checkCount++; } + } + + @Component({template: ``}) + class TestComp { + constructor(public vcRef: ViewContainerRef, public cfResolver: ComponentFactoryResolver) {} + } + + @NgModule( + {entryComponents: [DynamicCompWithBindings], declarations: [DynamicCompWithBindings]}) + class DynamicCompWithBindingsModule { + } + + + TestBed.configureTestingModule({ + declarations: [TestComp], + imports: [DynamicCompWithBindingsModule], + providers: [TEST_COMPONENT_RENDERER] + }); + const fixture = TestBed.createComponent(TestComp); + const {vcRef, cfResolver} = fixture.componentInstance; + fixture.detectChanges(); + + // Ivy inserts a comment for the root view container ref instance. This is not + // the case for view engine and we need to adjust the assertions. + expect(containerEl !.childNodes.length).toBe(ivyEnabled ? 2 : 1); + ivyEnabled && expect(containerEl !.childNodes[1].nodeType).toBe(Node.COMMENT_NODE); + + expect((containerEl !.childNodes[0] as Element).tagName).toBe('DIV'); + + vcRef.createComponent(cfResolver.resolveComponentFactory(DynamicCompWithBindings)); + fixture.detectChanges(); + + expect(containerEl !.childNodes.length).toBe(ivyEnabled ? 3 : 2); + expect(containerEl !.childNodes[1].textContent).toBe('check count: 1'); + + fixture.detectChanges(); + + expect(containerEl !.childNodes.length).toBe(ivyEnabled ? 3 : 2); + expect(containerEl !.childNodes[1].textContent).toBe('check count: 2'); + }); + + it('should create deep DOM tree immediately for dynamically created components', () => { + @Component({template: ``}) + class TestComp { + constructor(public vcRef: ViewContainerRef, public cfResolver: ComponentFactoryResolver) {} + } + + @Component({selector: 'child', template: `
{{name}}
`}) + class Child { + name = 'text'; + } + + @Component({selector: 'dynamic-cmpt-with-children', template: ``}) + class DynamicCompWithChildren { + } + + @NgModule({ + entryComponents: [DynamicCompWithChildren], + declarations: [DynamicCompWithChildren, Child] + }) + class DynamicCompWithChildrenModule { + } + + TestBed.configureTestingModule({ + declarations: [TestComp], + imports: [DynamicCompWithChildrenModule], + providers: [TEST_COMPONENT_RENDERER] + }); + + const fixture = TestBed.createComponent(TestComp); + const {vcRef, cfResolver} = fixture.componentInstance; + fixture.detectChanges(); + + // Ivy inserts a comment for the root view container ref instance. This is not + // the case for view engine and we need to adjust the assertions. + expect(containerEl !.childNodes.length).toBe(ivyEnabled ? 2 : 1); + ivyEnabled && expect(containerEl !.childNodes[1].nodeType).toBe(Node.COMMENT_NODE); + + expect((containerEl !.childNodes[0] as Element).tagName).toBe('DIV'); + + vcRef.createComponent(cfResolver.resolveComponentFactory(DynamicCompWithChildren)); + + expect(containerEl !.childNodes.length).toBe(ivyEnabled ? 3 : 2); + expect(getElementHtml(containerEl !.childNodes[1] as Element)) + .toBe('
'); + + fixture.detectChanges(); + + expect(containerEl !.childNodes.length).toBe(ivyEnabled ? 3 : 2); + expect(getElementHtml(containerEl !.childNodes[1] as Element)) + .toBe(`
text
`); + }); + }); }); +@Component({ + template: ` + {{name}} +

+ `, +}) +class EmbeddedViewInsertionComp { +} + +@Directive({ + selector: '[vcref]', +}) +class VCRefDirective { + @Input() tplRef: TemplateRef|undefined; + @Input() name: string = ''; + + // Injecting the ViewContainerRef to create a dynamic container in which + // embedded views will be created + constructor(public vcref: ViewContainerRef, public cfr: ComponentFactoryResolver) {} + + createView(s: string, index?: number): EmbeddedViewRef { + if (!this.tplRef) { + throw new Error('No template reference passed to directive.'); + } + + return this.vcref.createEmbeddedView(this.tplRef, {$implicit: s}, index); + } +} + +@Component({ + selector: `embedded-cmp-with-ngcontent`, + template: `
` +}) +class EmbeddedComponentWithNgContent { +} + +@NgModule({ + exports: [EmbeddedComponentWithNgContent], + entryComponents: [EmbeddedComponentWithNgContent], + declarations: [EmbeddedComponentWithNgContent], +}) +class EmbeddedComponentWithNgZoneModule { +} + @Component({ selector: 'view-container-ref-comp', template: ` diff --git a/packages/core/test/render3/view_container_ref_spec.ts b/packages/core/test/render3/view_container_ref_spec.ts index d0c1462cd7..bfdadb8472 100644 --- a/packages/core/test/render3/view_container_ref_spec.ts +++ b/packages/core/test/render3/view_container_ref_spec.ts @@ -6,23 +6,16 @@ * found in the LICENSE file at https://angular.io/license */ -import {ChangeDetectorRef, Component as _Component, ComponentFactoryResolver, ComponentRef, ΔdefineInjector, ElementRef, EmbeddedViewRef, NgModuleRef, Pipe, PipeTransform, QueryList, RendererFactory2, TemplateRef, ViewContainerRef, ViewRef, ɵAPP_ROOT as APP_ROOT, ɵNgModuleDef as NgModuleDef,} from '../../src/core'; -import {createInjector} from '../../src/di/r3_injector'; +import {ChangeDetectorRef, Component as _Component, ComponentFactoryResolver, ComponentRef, ElementRef, QueryList, TemplateRef, ViewContainerRef, ViewRef,} from '../../src/core'; import {ViewEncapsulation} from '../../src/metadata'; -import {AttributeMarker, ΔdefineComponent, ΔdefineDirective, ΔdefinePipe, injectComponentFactoryResolver, Δlistener, ΔloadViewQuery, ΔNgOnChangesFeature, ΔqueryRefresh, ΔviewQuery,} from '../../src/render3/index'; +import {injectComponentFactoryResolver, ΔdefineComponent, ΔdefineDirective, Δlistener, ΔloadViewQuery, ΔqueryRefresh, ΔviewQuery,} from '../../src/render3/index'; -import {ΔallocHostVars, Δbind, Δcontainer, ΔcontainerRefreshEnd, ΔcontainerRefreshStart, ΔdirectiveInject, Δelement, ΔelementEnd, ΔelementHostAttrs, ΔelementProperty, ΔelementStart, ΔembeddedViewEnd, ΔembeddedViewStart, Δinterpolation1, Δinterpolation3, ΔnextContext, Δprojection, ΔprojectionDef, Δreference, Δtemplate, Δtext, ΔtextBinding,} from '../../src/render3/instructions/all'; +import {Δcontainer, ΔcontainerRefreshEnd, ΔcontainerRefreshStart, ΔdirectiveInject, Δelement, ΔelementEnd, ΔelementStart, ΔembeddedViewEnd, ΔembeddedViewStart, Δtemplate, Δtext,} from '../../src/render3/instructions/all'; import {RenderFlags} from '../../src/render3/interfaces/definition'; import {RElement} from '../../src/render3/interfaces/renderer'; -import {NgModuleFactory} from '../../src/render3/ng_module_ref'; -import {Δpipe, ΔpipeBind1} from '../../src/render3/pipe'; import {getLView} from '../../src/render3/state'; import {getNativeByIndex} from '../../src/render3/util/view_utils'; -import {ΔtemplateRefExtractor} from '../../src/render3/view_engine_compatibility_prebound'; -import {NgForOf} from '../../test/render3/common_with_def'; - -import {getRendererFactory2} from './imported_renderer2'; -import {ComponentFixture, createComponent, getDirectiveOnNode, TemplateFixture,} from './render_util'; +import {ComponentFixture, createComponent, TemplateFixture,} from './render_util'; const Component: typeof _Component = function(...args: any[]): any { // In test we use @Component for documentation only so it's safe to mock out the implementation. @@ -56,184 +49,8 @@ describe('ViewContainerRef', () => { } describe('API', () => { - /** - * {{name}} - */ - function embeddedTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - Δtext(0); - } - if (rf & RenderFlags.Update) { - ΔtextBinding(0, Δbind(ctx.name)); - } - } - - function createView(s: string, index?: number): EmbeddedViewRef { - return directiveInstance !.vcref.createEmbeddedView( - directiveInstance !.tplRef, {name: s}, index); - } - - /** - * {{name}} - *

- */ - function createTemplate() { - Δtemplate( - 0, embeddedTemplate, 1, 1, 'ng-template', null, ['tplRef', ''], ΔtemplateRefExtractor); - Δelement(2, 'p', ['vcref', '']); - } - - function updateTemplate() { - const tplRef = Δreference(1); - ΔelementProperty(2, 'tplRef', Δbind(tplRef)); - } describe('createEmbeddedView (incl. insert)', () => { - it('should work on elements', () => { - /** - * {{name}} - *
- *
- */ - function createTemplate() { - Δtemplate( - 0, embeddedTemplate, 1, 1, 'ng-template', null, ['tplRef', ''], - ΔtemplateRefExtractor); - Δelement(2, 'header', ['vcref', '']); - Δelement(3, 'footer'); - } - - const fixture = - new TemplateFixture(createTemplate, updateTemplate, 4, 1, [DirectiveWithVCRef]); - expect(fixture.html).toEqual('
'); - - createView('A'); - fixture.update(); - expect(fixture.html).toEqual('
A
'); - - createView('B'); - createView('C'); - fixture.update(); - expect(fixture.html).toEqual('
ABC
'); - - createView('Y', 0); - fixture.update(); - expect(fixture.html).toEqual('
YABC
'); - - expect(() => { createView('Z', -1); }).toThrow(); - expect(() => { createView('Z', 5); }).toThrow(); - }); - - it('should work on components', () => { - const HeaderComponent = - createComponent('header-cmp', function(rf: RenderFlags, ctx: any) {}); - - /** - * {{name}} - * - *
- */ - function createTemplate() { - Δtemplate( - 0, embeddedTemplate, 1, 1, 'ng-template', [], ['tplRef', ''], ΔtemplateRefExtractor); - Δelement(2, 'header-cmp', ['vcref', '']); - Δelement(3, 'footer'); - } - - const fixture = new TemplateFixture( - createTemplate, updateTemplate, 4, 1, [HeaderComponent, DirectiveWithVCRef]); - expect(fixture.html).toEqual('
'); - - createView('A'); - fixture.update(); - expect(fixture.html).toEqual('A
'); - - createView('B'); - createView('C'); - fixture.update(); - expect(fixture.html).toEqual('ABC
'); - - createView('Y', 0); - fixture.update(); - expect(fixture.html).toEqual('YABC
'); - - expect(() => { createView('Z', -1); }).toThrow(); - expect(() => { createView('Z', 5); }).toThrow(); - }); - - it('should work with multiple instances with vcrefs', () => { - let firstDir: DirectiveWithVCRef; - let secondDir: DirectiveWithVCRef; - - /** - * {{name}} - *
- *
- */ - function createTemplate() { - Δtemplate( - 0, embeddedTemplate, 1, 1, 'ng-template', null, ['tplRef', ''], - ΔtemplateRefExtractor); - Δelement(2, 'div', ['vcref', '']); - Δelement(3, 'div', ['vcref', '']); - - // for testing only: - firstDir = getDirectiveOnNode(2); - secondDir = getDirectiveOnNode(3); - } - - function update() { - const tplRef = Δreference(1); - ΔelementProperty(2, 'tplRef', Δbind(tplRef)); - ΔelementProperty(3, 'tplRef', Δbind(tplRef)); - } - - const fixture = new TemplateFixture(createTemplate, update, 4, 2, [DirectiveWithVCRef]); - expect(fixture.html).toEqual('
'); - - firstDir !.vcref.createEmbeddedView(firstDir !.tplRef, {name: 'A'}); - secondDir !.vcref.createEmbeddedView(secondDir !.tplRef, {name: 'B'}); - fixture.update(); - expect(fixture.html).toEqual('
A
B'); - }); - - it('should work on templates', () => { - /** - * {{name}} - *
- */ - function createTemplate() { - Δtemplate( - 0, embeddedTemplate, 1, 1, 'ng-template', ['vcref', ''], ['tplRef', ''], - ΔtemplateRefExtractor); - Δelement(2, 'footer'); - } - - function updateTemplate() { - const tplRef = Δreference(1); - ΔelementProperty(0, 'tplRef', Δbind(tplRef)); - } - - const fixture = - new TemplateFixture(createTemplate, updateTemplate, 3, 1, [DirectiveWithVCRef]); - expect(fixture.html).toEqual('
'); - - createView('A'); - fixture.update(); - expect(fixture.html).toEqual('A
'); - - createView('B'); - createView('C'); - fixture.update(); - expect(fixture.html).toEqual('ABC
'); - - createView('Y', 0); - fixture.update(); - expect(fixture.html).toEqual('YABC
'); - - expect(() => { createView('Z', -1); }).toThrow(); - expect(() => { createView('Z', 5); }).toThrow(); - }); it('should add embedded views at the right position in the DOM tree (ng-template next to other ng-template)', () => { @@ -411,646 +228,11 @@ describe('ViewContainerRef', () => { expect(fixture.html).toEqual('before|AAB|after'); }); - it('should apply directives and pipes of the host view to the TemplateRef', () => { - @Component({selector: 'child', template: `{{name}}`}) - class Child { - // TODO(issue/24571): remove '!'. - name !: string; - - static ngComponentDef = ΔdefineComponent({ - type: Child, - encapsulation: ViewEncapsulation.None, - selectors: [['child']], - factory: () => new Child(), - consts: 1, - vars: 1, - template: (rf: RenderFlags, cmp: Child) => { - if (rf & RenderFlags.Create) { - Δtext(0); - } - if (rf & RenderFlags.Update) { - ΔtextBinding(0, Δinterpolation1('', cmp.name, '')); - } - }, - inputs: {name: 'name'} - }); - } - - @Pipe({name: 'starPipe'}) - class StarPipe implements PipeTransform { - transform(value: any) { return `**${value}**`; } - - static ngPipeDef = ΔdefinePipe({ - name: 'starPipe', - type: StarPipe, - factory: function StarPipe_Factory() { return new StarPipe(); }, - }); - } - - function SomeComponent_Template_0(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - Δelement(0, 'child'); - Δpipe(1, 'starPipe'); - } - if (rf & RenderFlags.Update) { - ΔelementProperty(0, 'name', Δbind(ΔpipeBind1(1, 1, 'C'))); - } - } - - @Component({ - template: ` - - - - - - ` - }) - class SomeComponent { - static ngComponentDef = ΔdefineComponent({ - type: SomeComponent, - encapsulation: ViewEncapsulation.None, - selectors: [['some-comp']], - factory: () => new SomeComponent(), - consts: 6, - vars: 7, - template: (rf: RenderFlags, cmp: SomeComponent) => { - if (rf & RenderFlags.Create) { - Δtemplate( - 0, SomeComponent_Template_0, 2, 3, 'ng-template', [], ['foo', ''], - ΔtemplateRefExtractor); - Δpipe(2, 'starPipe'); - Δelement(3, 'child', ['vcref', '']); - Δpipe(4, 'starPipe'); - Δelement(5, 'child'); - } - if (rf & RenderFlags.Update) { - const tplRef = Δreference(1); - ΔelementProperty(3, 'tplRef', Δbind(tplRef)); - ΔelementProperty(3, 'name', Δbind(ΔpipeBind1(2, 3, 'A'))); - ΔelementProperty(5, 'name', Δbind(ΔpipeBind1(4, 5, 'B'))); - } - }, - directives: [Child, DirectiveWithVCRef], - pipes: [StarPipe] - }); - } - - const fixture = new ComponentFixture(SomeComponent); - directiveInstance !.vcref.createEmbeddedView(directiveInstance !.tplRef, fixture.component); - directiveInstance !.vcref.createEmbeddedView(directiveInstance !.tplRef, fixture.component); - fixture.update(); - expect(fixture.html) - .toEqual( - '**A****C****C****B**'); - }); - }); - - describe('insertion points and declaration points', () => { - class InsertionDir { - // @Input() - set tplDir(tpl: TemplateRef|null) { - tpl ? this.vcr.createEmbeddedView(tpl) : this.vcr.clear(); - } - - constructor(public vcr: ViewContainerRef) {} - - static ngDirectiveDef = ΔdefineDirective({ - type: InsertionDir, - selectors: [['', 'tplDir', '']], - factory: () => new InsertionDir(ΔdirectiveInject(ViewContainerRef as any)), - inputs: {tplDir: 'tplDir'} - }); - } - - // see running stackblitz example: https://stackblitz.com/edit/angular-w3myy6 - it('should work with a template declared in a different component view from insertion', - () => { - let child: Child|null = null; - - /** - *
{{ name }}
- * // template insertion point - */ - class Child { - name = 'Child'; - tpl: TemplateRef|null = null; - - static ngComponentDef = ΔdefineComponent({ - type: Child, - encapsulation: ViewEncapsulation.None, - selectors: [['child']], - factory: () => child = new Child(), - consts: 2, - vars: 2, - template: function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ΔelementStart(0, 'div', [AttributeMarker.Bindings, 'tplDir']); - { Δtext(1); } - ΔelementEnd(); - } - if (rf & RenderFlags.Update) { - ΔelementProperty(0, 'tplDir', Δbind(ctx.tpl)); - ΔtextBinding(1, Δbind(ctx.name)); - } - }, - inputs: {tpl: 'tpl'}, - directives: () => [InsertionDir] - }); - } - - /** - * // template declaration point - * - *
{{ name }}
- *
- * - * <-- template insertion inside - */ - const Parent = createComponent('parent', function(rf: RenderFlags, parent: any) { - if (rf & RenderFlags.Create) { - Δtemplate( - 0, fooTemplate, 2, 1, 'ng-template', null, ['foo', ''], ΔtemplateRefExtractor); - Δelement(2, 'child'); - } - - if (rf & RenderFlags.Update) { - const tplRef = Δreference(1); - ΔelementProperty(2, 'tpl', Δbind(tplRef)); - } - - }, 3, 1, [Child]); - - function fooTemplate(rf1: RenderFlags, ctx: any) { - if (rf1 & RenderFlags.Create) { - ΔelementStart(0, 'div'); - { Δtext(1); } - ΔelementEnd(); - } - if (rf1 & RenderFlags.Update) { - const parent = ΔnextContext(); - ΔtextBinding(1, Δbind(parent.name)); - } - } - - const fixture = new ComponentFixture(Parent); - fixture.component.name = 'Parent'; - fixture.update(); - - // Context should be inherited from the declaration point, not the insertion point, - // so the template should read 'Parent'. - expect(fixture.html).toEqual(`
Child
Parent
`); - - child !.tpl = null; - fixture.update(); - expect(fixture.html).toEqual(`
Child
`); - }); - - // see running stackblitz example: https://stackblitz.com/edit/angular-3vplec - it('should work with nested for loops with different declaration / insertion points', () => { - /** - * - * // insertion point for templates (both row and cell) - * - */ - class LoopComp { - name = 'Loop'; - - // @Input() - tpl !: TemplateRef; - - // @Input() - rows !: any[]; - - static ngComponentDef = ΔdefineComponent({ - type: LoopComp, - encapsulation: ViewEncapsulation.None, - selectors: [['loop-comp']], - factory: () => new LoopComp(), - consts: 1, - vars: 2, - template: function(rf: RenderFlags, loop: any) { - if (rf & RenderFlags.Create) { - Δtemplate(0, null, 0, 0, 'ng-template', [AttributeMarker.Bindings, 'ngForOf']); - } - - if (rf & RenderFlags.Update) { - ΔelementProperty(0, 'ngForOf', Δbind(loop.rows)); - ΔelementProperty(0, 'ngForTemplate', Δbind(loop.tpl)); - } - }, - inputs: {tpl: 'tpl', rows: 'rows'}, - directives: () => [NgForOf] - }); - } - - /** - * // row declaration point - * - * - * // cell declaration point - * - *
{{ cell }} - {{ row.value }} - {{ name }}
- *
- * - * <-- cell insertion - *
- * - * <-- row insertion - * - */ - const Parent = createComponent('parent', function(rf: RenderFlags, parent: any) { - if (rf & RenderFlags.Create) { - Δtemplate( - 0, rowTemplate, 3, 2, 'ng-template', null, ['rowTemplate', ''], - ΔtemplateRefExtractor); - Δelement(2, 'loop-comp'); - } - - if (rf & RenderFlags.Update) { - const rowTemplateRef = Δreference(1); - ΔelementProperty(2, 'tpl', Δbind(rowTemplateRef)); - ΔelementProperty(2, 'rows', Δbind(parent.rows)); - } - - }, 3, 2, [LoopComp]); - - function rowTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - Δtemplate( - 0, cellTemplate, 2, 3, 'ng-template', null, ['cellTemplate', ''], - ΔtemplateRefExtractor); - Δelement(2, 'loop-comp'); - } - - if (rf & RenderFlags.Update) { - const row = ctx.$implicit as any; - const cellTemplateRef = Δreference(1); - ΔelementProperty(2, 'tpl', Δbind(cellTemplateRef)); - ΔelementProperty(2, 'rows', Δbind(row.data)); - } - } - - function cellTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ΔelementStart(0, 'div'); - { Δtext(1); } - ΔelementEnd(); - } - - if (rf & RenderFlags.Update) { - const cell = ctx.$implicit as any; - const row = ΔnextContext().$implicit as any; - const parent = ΔnextContext(); - ΔtextBinding(1, Δinterpolation3('', cell, ' - ', row.value, ' - ', parent.name, '')); - } - } - - const fixture = new ComponentFixture(Parent); - fixture.component.name = 'Parent'; - fixture.component.rows = - [{data: ['1', '2'], value: 'one'}, {data: ['3', '4'], value: 'two'}]; - fixture.update(); - - expect(fixture.html) - .toEqual( - '' + - '
1 - one - Parent
2 - one - Parent
' + - '
3 - two - Parent
4 - two - Parent
' + - '
'); - - fixture.component.rows = [{data: ['5', '6'], value: 'three'}, {data: ['7'], value: 'four'}]; - fixture.component.name = 'New name!'; - fixture.update(); - - expect(fixture.html) - .toEqual( - '' + - '
5 - three - New name!
6 - three - New name!
' + - '
7 - four - New name!
' + - '
'); - }); - }); - - const rendererFactory = getRendererFactory2(document); - - describe('detach', () => { - it('should detach the right embedded view when an index is specified', () => { - const fixture = new TemplateFixture( - createTemplate, updateTemplate, 3, 1, [DirectiveWithVCRef], null, null, - rendererFactory); - const viewA = createView('A'); - createView('B'); - createView('C'); - const viewD = createView('D'); - createView('E'); - fixture.update(); - expect(fixture.html).toEqual('

ABCDE'); - - directiveInstance !.vcref.detach(3); - fixture.update(); - expect(fixture.html).toEqual('

ABCE'); - expect(viewD.destroyed).toBeFalsy(); - - directiveInstance !.vcref.detach(0); - fixture.update(); - expect(fixture.html).toEqual('

BCE'); - expect(viewA.destroyed).toBeFalsy(); - - expect(() => { directiveInstance !.vcref.detach(-1); }).toThrow(); - expect(() => { directiveInstance !.vcref.detach(42); }).toThrow(); - expect(ngDevMode).toHaveProperties({rendererDestroyNode: 0}); - }); - - - it('should detach the last embedded view when no index is specified', () => { - const fixture = new TemplateFixture( - createTemplate, updateTemplate, 3, 1, [DirectiveWithVCRef], null, null, - rendererFactory); - createView('A'); - createView('B'); - createView('C'); - createView('D'); - const viewE = createView('E'); - fixture.update(); - expect(fixture.html).toEqual('

ABCDE'); - - directiveInstance !.vcref.detach(); - fixture.update(); - expect(fixture.html).toEqual('

ABCD'); - expect(viewE.destroyed).toBeFalsy(); - expect(ngDevMode).toHaveProperties({rendererDestroyNode: 0}); - }); - }); - - describe('remove', () => { - it('should remove the right embedded view when an index is specified', () => { - const fixture = new TemplateFixture( - createTemplate, updateTemplate, 3, 1, [DirectiveWithVCRef], null, null, - rendererFactory); - const viewA = createView('A'); - createView('B'); - createView('C'); - const viewD = createView('D'); - createView('E'); - fixture.update(); - expect(fixture.html).toEqual('

ABCDE'); - - directiveInstance !.vcref.remove(3); - fixture.update(); - expect(fixture.html).toEqual('

ABCE'); - expect(viewD.destroyed).toBeTruthy(); - - directiveInstance !.vcref.remove(0); - fixture.update(); - expect(fixture.html).toEqual('

BCE'); - expect(viewA.destroyed).toBeTruthy(); - - expect(() => { directiveInstance !.vcref.remove(-1); }).toThrow(); - expect(() => { directiveInstance !.vcref.remove(42); }).toThrow(); - expect(ngDevMode).toHaveProperties({rendererDestroyNode: 2}); - }); - - it('should remove the last embedded view when no index is specified', () => { - const fixture = new TemplateFixture( - createTemplate, updateTemplate, 3, 1, [DirectiveWithVCRef], null, null, - rendererFactory); - createView('A'); - createView('B'); - createView('C'); - createView('D'); - const viewE = createView('E'); - fixture.update(); - expect(fixture.html).toEqual('

ABCDE'); - - directiveInstance !.vcref.remove(); - fixture.update(); - expect(fixture.html).toEqual('

ABCD'); - expect(viewE.destroyed).toBeTruthy(); - expect(ngDevMode).toHaveProperties({rendererDestroyNode: 1}); - }); - - it('should throw when trying to insert a removed or destroyed view', () => { - const fixture = new TemplateFixture( - createTemplate, updateTemplate, 3, 1, [DirectiveWithVCRef], null, null, - rendererFactory); - const viewA = createView('A'); - const viewB = createView('B'); - fixture.update(); - - directiveInstance !.vcref.remove(); - fixture.update(); - expect(() => directiveInstance !.vcref.insert(viewB)).toThrow(); - - viewA.destroy(); - fixture.update(); - expect(() => directiveInstance !.vcref.insert(viewA)).toThrow(); - }); - }); - - describe('length', () => { - it('should return the number of embedded views', () => { - const fixture = - new TemplateFixture(createTemplate, updateTemplate, 3, 1, [DirectiveWithVCRef]); - expect(directiveInstance !.vcref.length).toEqual(0); - - createView('A'); - createView('B'); - createView('C'); - fixture.update(); - expect(directiveInstance !.vcref.length).toEqual(3); - - directiveInstance !.vcref.detach(1); - fixture.update(); - expect(directiveInstance !.vcref.length).toEqual(2); - - directiveInstance !.vcref.clear(); - fixture.update(); - expect(directiveInstance !.vcref.length).toEqual(0); - }); - }); - - describe('get and indexOf', () => { - it('should retrieve a ViewRef from its index, and vice versa', () => { - const fixture = - new TemplateFixture(createTemplate, updateTemplate, 3, 1, [DirectiveWithVCRef]); - createView('A'); - createView('B'); - createView('C'); - fixture.update(); - - let viewRef = directiveInstance !.vcref.get(0); - expect(directiveInstance !.vcref.indexOf(viewRef !)).toEqual(0); - - viewRef = directiveInstance !.vcref.get(1); - expect(directiveInstance !.vcref.indexOf(viewRef !)).toEqual(1); - - viewRef = directiveInstance !.vcref.get(2); - expect(directiveInstance !.vcref.indexOf(viewRef !)).toEqual(2); - }); - - it('should handle out of bounds cases', () => { - const fixture = - new TemplateFixture(createTemplate, updateTemplate, 3, 1, [DirectiveWithVCRef]); - createView('A'); - fixture.update(); - - expect(directiveInstance !.vcref.get(-1)).toBeNull(); - expect(directiveInstance !.vcref.get(42)).toBeNull(); - - const viewRef = directiveInstance !.vcref.get(0); - directiveInstance !.vcref.remove(0); - expect(directiveInstance !.vcref.indexOf(viewRef !)).toEqual(-1); - }); - }); - - describe('move', () => { - it('should move embedded views and associated DOM nodes without recreating them', () => { - const fixture = - new TemplateFixture(createTemplate, updateTemplate, 3, 1, [DirectiveWithVCRef]); - createView('A'); - createView('B'); - createView('C'); - fixture.update(); - expect(fixture.html).toEqual('

ABC'); - - // The DOM is manually modified here to ensure that the text node is actually moved - fixture.hostElement.childNodes[2].nodeValue = '**A**'; - expect(fixture.html).toEqual('

**A**BC'); - - let viewRef = directiveInstance !.vcref.get(0); - directiveInstance !.vcref.move(viewRef !, 2); - fixture.update(); - expect(fixture.html).toEqual('

BC**A**'); - - directiveInstance !.vcref.move(viewRef !, 0); - fixture.update(); - expect(fixture.html).toEqual('

**A**BC'); - - directiveInstance !.vcref.move(viewRef !, 1); - fixture.update(); - expect(fixture.html).toEqual('

B**A**C'); - - expect(() => { directiveInstance !.vcref.move(viewRef !, -1); }).toThrow(); - expect(() => { directiveInstance !.vcref.move(viewRef !, 42); }).toThrow(); - }); }); describe('createComponent', () => { let templateExecutionCounter = 0; - it('should work without Injector and NgModuleRef', () => { - class EmbeddedComponent { - constructor() {} - - static ngComponentDef = ΔdefineComponent({ - type: EmbeddedComponent, - encapsulation: ViewEncapsulation.None, - selectors: [['embedded-cmp']], - factory: () => new EmbeddedComponent(), - consts: 1, - vars: 0, - template: (rf: RenderFlags, cmp: EmbeddedComponent) => { - templateExecutionCounter++; - if (rf & RenderFlags.Create) { - Δtext(0, 'foo'); - } - } - }); - } - - templateExecutionCounter = 0; - const fixture = - new TemplateFixture(createTemplate, updateTemplate, 3, 1, [DirectiveWithVCRef]); - expect(fixture.html).toEqual('

'); - expect(templateExecutionCounter).toEqual(0); - - const componentRef = directiveInstance !.vcref.createComponent( - directiveInstance !.cfr.resolveComponentFactory(EmbeddedComponent)); - fixture.update(); - expect(fixture.html).toEqual('

foo'); - expect(templateExecutionCounter).toEqual(2); - - directiveInstance !.vcref.detach(0); - fixture.update(); - expect(fixture.html).toEqual('

'); - expect(templateExecutionCounter).toEqual(2); - - directiveInstance !.vcref.insert(componentRef.hostView); - fixture.update(); - expect(fixture.html).toEqual('

foo'); - expect(templateExecutionCounter).toEqual(3); - }); - - it('should work with NgModuleRef and Injector', () => { - class EmbeddedComponent { - constructor(public s: String) {} - - static ngComponentDef = ΔdefineComponent({ - type: EmbeddedComponent, - encapsulation: ViewEncapsulation.None, - selectors: [['embedded-cmp']], - factory: () => new EmbeddedComponent(ΔdirectiveInject(String)), - consts: 1, - vars: 0, - template: (rf: RenderFlags, cmp: EmbeddedComponent) => { - templateExecutionCounter++; - if (rf & RenderFlags.Create) { - Δtext(0, 'foo'); - } - } - }); - } - - class MyAppModule { - static ngInjectorDef = ΔdefineInjector({ - factory: () => new MyAppModule(), - imports: [], - providers: [ - {provide: APP_ROOT, useValue: true}, - {provide: RendererFactory2, useValue: getRendererFactory2(document)}, - {provide: String, useValue: 'module'} - ] - }); - static ngModuleDef: NgModuleDef = { bootstrap: [] } as any; - } - const myAppModuleFactory = new NgModuleFactory(MyAppModule); - const ngModuleRef = myAppModuleFactory.create(null); - - class SomeModule { - static ngInjectorDef = ΔdefineInjector({ - factory: () => new SomeModule(), - providers: [ - {provide: NgModuleRef, useValue: ngModuleRef}, - {provide: String, useValue: 'injector'} - ] - }); - } - const injector = createInjector(SomeModule); - - templateExecutionCounter = 0; - const fixture = - new TemplateFixture(createTemplate, updateTemplate, 3, 1, [DirectiveWithVCRef]); - expect(fixture.html).toEqual('

'); - expect(templateExecutionCounter).toEqual(0); - - const componentRef = directiveInstance !.vcref.createComponent( - directiveInstance !.cfr.resolveComponentFactory(EmbeddedComponent), 0, injector); - fixture.update(); - expect(fixture.html).toEqual('

foo'); - expect(templateExecutionCounter).toEqual(2); - expect(componentRef.instance.s).toEqual('injector'); - - directiveInstance !.vcref.createComponent( - directiveInstance !.cfr.resolveComponentFactory(EmbeddedComponent), 0, undefined, - undefined, ngModuleRef); - fixture.update(); - expect(fixture.html) - .toEqual( - '

foofoo'); - expect(templateExecutionCounter).toEqual(5); - }); - describe('ComponentRef', () => { let dynamicComp !: DynamicComp; @@ -1129,101 +311,6 @@ describe('ViewContainerRef', () => { }); }); - - class EmbeddedComponentWithNgContent { - static ngComponentDef = ΔdefineComponent({ - type: EmbeddedComponentWithNgContent, - encapsulation: ViewEncapsulation.None, - selectors: [['embedded-cmp-with-ngcontent']], - factory: () => new EmbeddedComponentWithNgContent(), - consts: 3, - vars: 0, - template: (rf: RenderFlags, cmp: EmbeddedComponentWithNgContent) => { - if (rf & RenderFlags.Create) { - ΔprojectionDef(); - Δprojection(0, 0); - Δelement(1, 'hr'); - Δprojection(2, 1); - } - } - }); - } - - it('should support projectable nodes', () => { - const fixture = - new TemplateFixture(createTemplate, updateTemplate, 3, 1, [DirectiveWithVCRef]); - expect(fixture.html).toEqual('

'); - - const myNode = document.createElement('div'); - const myText = document.createTextNode('bar'); - const myText2 = document.createTextNode('baz'); - myNode.appendChild(myText); - myNode.appendChild(myText2); - - directiveInstance !.vcref.createComponent( - directiveInstance !.cfr.resolveComponentFactory(EmbeddedComponentWithNgContent), 0, - undefined, [[myNode]]); - fixture.update(); - expect(fixture.html) - .toEqual( - '

barbaz

'); - }); - - it('should support reprojection of projectable nodes', () => { - class Reprojector { - static ngComponentDef = ΔdefineComponent({ - type: Reprojector, - encapsulation: ViewEncapsulation.None, - selectors: [['reprojector']], - factory: () => new Reprojector(), - consts: 2, - vars: 0, - template: (rf: RenderFlags, cmp: Reprojector) => { - if (rf & RenderFlags.Create) { - ΔprojectionDef(); - ΔelementStart(0, 'embedded-cmp-with-ngcontent'); - { Δprojection(1, 0); } - ΔelementEnd(); - } - }, - directives: [EmbeddedComponentWithNgContent] - }); - } - - const fixture = - new TemplateFixture(createTemplate, updateTemplate, 3, 1, [DirectiveWithVCRef]); - expect(fixture.html).toEqual('

'); - - const myNode = document.createElement('div'); - const myText = document.createTextNode('bar'); - const myText2 = document.createTextNode('baz'); - myNode.appendChild(myText); - myNode.appendChild(myText2); - - directiveInstance !.vcref.createComponent( - directiveInstance !.cfr.resolveComponentFactory(Reprojector), 0, undefined, [[myNode]]); - fixture.update(); - expect(fixture.html) - .toEqual( - '

barbaz

'); - }); - - it('should support many projectable nodes with many slots', () => { - const fixture = - new TemplateFixture(createTemplate, updateTemplate, 3, 1, [DirectiveWithVCRef]); - expect(fixture.html).toEqual('

'); - - directiveInstance !.vcref.createComponent( - directiveInstance !.cfr.resolveComponentFactory(EmbeddedComponentWithNgContent), 0, - undefined, [ - [document.createTextNode('1'), document.createTextNode('2')], - [document.createTextNode('3'), document.createTextNode('4')] - ]); - fixture.update(); - expect(fixture.html) - .toEqual( - '

12
34
'); - }); }); describe('getters', () => { @@ -1261,657 +348,9 @@ describe('ViewContainerRef', () => { .toEqual('header-cmp'); expect(() => directiveInstance !.vcref.parentInjector.get(ElementRef)).toThrow(); }); - - it('should work on templates', () => { - function createTemplate() { - Δtemplate(0, embeddedTemplate, 1, 1, 'ng-template', ['vcref', '']); - Δelement(1, 'footer'); - } - - new TemplateFixture(createTemplate, () => {}, 2, 0, [DirectiveWithVCRef]); - expect(directiveInstance !.vcref.element.nativeElement.textContent).toEqual('container'); - expect(directiveInstance !.vcref.injector.get(ElementRef).nativeElement.textContent) - .toEqual('container'); - expect(() => directiveInstance !.vcref.parentInjector.get(ElementRef)).toThrow(); - }); }); }); - describe('projection', () => { - function embeddedTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ΔelementStart(0, 'span'); - Δtext(1); - ΔelementEnd(); - } - if (rf & RenderFlags.Update) { - ΔtextBinding(1, ctx.name); - } - } - - it('should project the ViewContainerRef content along its host, in an element', () => { - @Component({selector: 'child', template: '
'}) - class Child { - static ngComponentDef = ΔdefineComponent({ - type: Child, - encapsulation: ViewEncapsulation.None, - selectors: [['child']], - factory: () => new Child(), - consts: 2, - vars: 0, - template: (rf: RenderFlags, cmp: Child) => { - if (rf & RenderFlags.Create) { - ΔprojectionDef(); - ΔelementStart(0, 'div'); - { Δprojection(1); } - ΔelementEnd(); - } - } - }); - } - - @Component({ - selector: 'parent', - template: ` - - {{name}} - -
blah
` - }) - class Parent { - name: string = 'bar'; - static ngComponentDef = ΔdefineComponent({ - type: Parent, - encapsulation: ViewEncapsulation.None, - selectors: [['parent']], - factory: () => new Parent(), - consts: 5, - vars: 2, - template: (rf: RenderFlags, cmp: Parent) => { - if (rf & RenderFlags.Create) { - Δtemplate( - 0, embeddedTemplate, 2, 1, 'ng-template', null, ['foo', ''], - ΔtemplateRefExtractor); - ΔelementStart(2, 'child'); - { - ΔelementStart(3, 'header', ['vcref', '']); - { Δtext(4, 'blah'); } - ΔelementEnd(); - } - ΔelementEnd(); - } - let tplRef: any; - if (rf & RenderFlags.Update) { - tplRef = Δreference(1); - ΔelementProperty(3, 'tplRef', Δbind(tplRef)); - ΔelementProperty(3, 'name', Δbind(cmp.name)); - } - }, - directives: [Child, DirectiveWithVCRef] - }); - } - - const fixture = new ComponentFixture(Parent); - expect(fixture.html).toEqual('
blah
'); - - directiveInstance !.vcref.createEmbeddedView(directiveInstance !.tplRef, fixture.component); - fixture.update(); - expect(fixture.html) - .toEqual('
blah
bar
'); - }); - - it('should project the ViewContainerRef content along its host, in a view', () => { - @Component({ - selector: 'child-with-view', - template: ` - Before (inside)- - % if (show) { - - % } - After (inside) - ` - }) - class ChildWithView { - show: boolean = true; - static ngComponentDef = ΔdefineComponent({ - type: ChildWithView, - encapsulation: ViewEncapsulation.None, - selectors: [['child-with-view']], - factory: () => new ChildWithView(), - consts: 3, - vars: 0, - template: (rf: RenderFlags, cmp: ChildWithView) => { - if (rf & RenderFlags.Create) { - ΔprojectionDef(); - Δtext(0, 'Before (inside)-'); - Δcontainer(1); - Δtext(2, 'After (inside)'); - } - if (rf & RenderFlags.Update) { - ΔcontainerRefreshStart(1); - if (cmp.show) { - let rf0 = ΔembeddedViewStart(0, 1, 0); - if (rf0 & RenderFlags.Create) { - Δprojection(0); - } - ΔembeddedViewEnd(); - } - ΔcontainerRefreshEnd(); - } - } - }); - } - - @Component({ - selector: 'parent', - template: ` - - {{name}} - - - Before projected -
blah
- After projected -
` - }) - class Parent { - name: string = 'bar'; - static ngComponentDef = ΔdefineComponent({ - type: Parent, - encapsulation: ViewEncapsulation.None, - selectors: [['parent']], - factory: () => new Parent(), - consts: 7, - vars: 2, - template: (rf: RenderFlags, cmp: Parent) => { - if (rf & RenderFlags.Create) { - Δtemplate( - 0, embeddedTemplate, 2, 1, 'ng-template', undefined, ['foo', ''], - ΔtemplateRefExtractor); - ΔelementStart(2, 'child-with-view'); - Δtext(3, 'Before projected'); - ΔelementStart(4, 'header', ['vcref', '']); - Δtext(5, 'blah'); - ΔelementEnd(); - Δtext(6, 'After projected-'); - ΔelementEnd(); - } - if (rf & RenderFlags.Update) { - const tplRef = Δreference(1); - ΔelementProperty(4, 'tplRef', Δbind(tplRef)); - ΔelementProperty(4, 'name', Δbind(cmp.name)); - } - }, - directives: [ChildWithView, DirectiveWithVCRef] - }); - } - - const fixture = new ComponentFixture(Parent); - expect(fixture.html) - .toEqual( - 'Before (inside)-Before projected
blah
After projected-After (inside)
'); - - directiveInstance !.vcref.createEmbeddedView(directiveInstance !.tplRef, fixture.component); - fixture.update(); - expect(fixture.html) - .toEqual( - 'Before (inside)-Before projected
blah
barAfter projected-After (inside)
'); - }); - - describe('with select', () => { - @Component({ - selector: 'child-with-selector', - template: ` - - ` - }) - class ChildWithSelector { - static ngComponentDef = ΔdefineComponent({ - type: ChildWithSelector, - encapsulation: ViewEncapsulation.None, - selectors: [['child-with-selector']], - factory: () => new ChildWithSelector(), - consts: 4, - vars: 0, - template: (rf: RenderFlags, cmp: ChildWithSelector) => { - if (rf & RenderFlags.Create) { - ΔprojectionDef([[['header']]]); - ΔelementStart(0, 'first'); - { Δprojection(1, 1); } - ΔelementEnd(); - ΔelementStart(2, 'second'); - { Δprojection(3); } - ΔelementEnd(); - } - }, - directives: [ChildWithSelector, DirectiveWithVCRef] - }); - } - - it('should project the ViewContainerRef content along its host, when the host matches a selector', - () => { - @Component({ - selector: 'parent', - template: ` - - {{name}} - -
blah
` - }) - class Parent { - name: string = 'bar'; - static ngComponentDef = ΔdefineComponent({ - type: Parent, - encapsulation: ViewEncapsulation.None, - selectors: [['parent']], - factory: () => new Parent(), - consts: 5, - vars: 2, - template: (rf: RenderFlags, cmp: Parent) => { - let tplRef: any; - if (rf & RenderFlags.Create) { - Δtemplate( - 0, embeddedTemplate, 2, 1, 'ng-template', null, ['foo', ''], - ΔtemplateRefExtractor); - ΔelementStart(2, 'child-with-selector'); - ΔelementStart(3, 'header', ['vcref', '']); - Δtext(4, 'blah'); - ΔelementEnd(); - ΔelementEnd(); - } - if (rf & RenderFlags.Update) { - tplRef = Δreference(1); - ΔelementProperty(3, 'tplRef', Δbind(tplRef)); - ΔelementProperty(3, 'name', Δbind(cmp.name)); - } - }, - directives: [ChildWithSelector, DirectiveWithVCRef] - }); - } - - const fixture = new ComponentFixture(Parent); - expect(fixture.html) - .toEqual( - '
blah
'); - - directiveInstance !.vcref.createEmbeddedView( - directiveInstance !.tplRef, fixture.component); - fixture.update(); - expect(fixture.html) - .toEqual( - '
blah
bar
'); - }); - - it('should not project the ViewContainerRef content, when the host does not match a selector', - () => { - @Component({ - selector: 'parent', - template: ` - - {{name}} - -
blah
` - }) - class Parent { - name: string = 'bar'; - static ngComponentDef = ΔdefineComponent({ - type: Parent, - encapsulation: ViewEncapsulation.None, - selectors: [['parent']], - factory: () => new Parent(), - consts: 5, - vars: 2, - template: (rf: RenderFlags, cmp: Parent) => { - let tplRef: any; - if (rf & RenderFlags.Create) { - Δtemplate( - 0, embeddedTemplate, 2, 1, 'ng-template', null, ['foo', ''], - ΔtemplateRefExtractor); - ΔelementStart(2, 'child-with-selector'); - ΔelementStart(3, 'footer', ['vcref', '']); - Δtext(4, 'blah'); - ΔelementEnd(); - ΔelementEnd(); - } - if (rf & RenderFlags.Update) { - tplRef = Δreference(1); - ΔelementProperty(3, 'tplRef', Δbind(tplRef)); - ΔelementProperty(3, 'name', Δbind(cmp.name)); - } - }, - directives: [ChildWithSelector, DirectiveWithVCRef] - }); - } - - const fixture = new ComponentFixture(Parent); - expect(fixture.html) - .toEqual( - '
blah
'); - - directiveInstance !.vcref.createEmbeddedView( - directiveInstance !.tplRef, fixture.component); - fixture.update(); - expect(fixture.html) - .toEqual( - '
blah
bar
'); - }); - }); - }); - - describe('life cycle hooks', () => { - - // Angular 5 reference: https://stackblitz.com/edit/lifecycle-hooks-vcref - const log: string[] = []; - - @Component({selector: 'hooks', template: `{{name}}`}) - class ComponentWithHooks { - // TODO(issue/24571): remove '!'. - name !: string; - - private log(msg: string) { log.push(msg); } - - ngOnChanges() { this.log('onChanges-' + this.name); } - ngOnInit() { this.log('onInit-' + this.name); } - ngDoCheck() { this.log('doCheck-' + this.name); } - - ngAfterContentInit() { this.log('afterContentInit-' + this.name); } - ngAfterContentChecked() { this.log('afterContentChecked-' + this.name); } - - ngAfterViewInit() { this.log('afterViewInit-' + this.name); } - ngAfterViewChecked() { this.log('afterViewChecked-' + this.name); } - - ngOnDestroy() { this.log('onDestroy-' + this.name); } - - static ngComponentDef = ΔdefineComponent({ - type: ComponentWithHooks, - encapsulation: ViewEncapsulation.None, - selectors: [['hooks']], - factory: () => new ComponentWithHooks(), - consts: 1, - vars: 1, - template: (rf: RenderFlags, cmp: ComponentWithHooks) => { - if (rf & RenderFlags.Create) { - Δtext(0); - } - if (rf & RenderFlags.Update) { - ΔtextBinding(0, Δinterpolation1('', cmp.name, '')); - } - }, - features: [ΔNgOnChangesFeature()], - inputs: {name: 'name'} - }); - } - - it('should call all hooks in correct order when creating with createEmbeddedView', () => { - function SomeComponent_Template_0(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - Δelement(0, 'hooks'); - } - if (rf & RenderFlags.Update) { - ΔelementProperty(0, 'name', Δbind('C')); - } - } - - @Component({ - template: ` - - - - - - ` - }) - class SomeComponent { - static ngComponentDef = ΔdefineComponent({ - type: SomeComponent, - selectors: [['some-comp']], - factory: () => new SomeComponent(), - consts: 4, - vars: 3, - template: (rf: RenderFlags, cmp: SomeComponent) => { - if (rf & RenderFlags.Create) { - Δtemplate( - 0, SomeComponent_Template_0, 1, 1, 'ng-template', [], ['foo', ''], - ΔtemplateRefExtractor); - Δelement(2, 'hooks', ['vcref', '']); - Δelement(3, 'hooks'); - } - if (rf & RenderFlags.Update) { - const tplRef = Δreference(1); - ΔelementProperty(2, 'tplRef', Δbind(tplRef)); - ΔelementProperty(2, 'name', Δbind('A')); - ΔelementProperty(3, 'name', Δbind('B')); - } - }, - directives: [ComponentWithHooks, DirectiveWithVCRef], - features: [ΔNgOnChangesFeature()], - }); - } - - log.length = 0; - - const fixture = new ComponentFixture(SomeComponent); - expect(log).toEqual([ - 'onChanges-A', 'onInit-A', 'doCheck-A', 'onChanges-B', 'onInit-B', 'doCheck-B', - 'afterContentInit-A', 'afterContentChecked-A', 'afterContentInit-B', - 'afterContentChecked-B', 'afterViewInit-A', 'afterViewChecked-A', 'afterViewInit-B', - 'afterViewChecked-B' - ]); - - log.length = 0; - fixture.update(); - expect(log).toEqual([ - 'doCheck-A', 'doCheck-B', 'afterContentChecked-A', 'afterContentChecked-B', - 'afterViewChecked-A', 'afterViewChecked-B' - ]); - - log.length = 0; - directiveInstance !.vcref.createEmbeddedView(directiveInstance !.tplRef, fixture.component); - expect(fixture.html).toEqual('AB'); - expect(log).toEqual([]); - - log.length = 0; - fixture.update(); - expect(fixture.html).toEqual('ACB'); - expect(log).toEqual([ - 'doCheck-A', 'doCheck-B', 'onChanges-C', 'onInit-C', 'doCheck-C', 'afterContentInit-C', - 'afterContentChecked-C', 'afterViewInit-C', 'afterViewChecked-C', 'afterContentChecked-A', - 'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B' - ]); - - log.length = 0; - fixture.update(); - expect(log).toEqual([ - 'doCheck-A', 'doCheck-B', 'doCheck-C', 'afterContentChecked-C', 'afterViewChecked-C', - 'afterContentChecked-A', 'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B' - ]); - - log.length = 0; - const viewRef = directiveInstance !.vcref.detach(0); - fixture.update(); - expect(log).toEqual([ - 'doCheck-A', 'doCheck-B', 'afterContentChecked-A', 'afterContentChecked-B', - 'afterViewChecked-A', 'afterViewChecked-B' - ]); - - log.length = 0; - directiveInstance !.vcref.insert(viewRef !); - fixture.update(); - expect(log).toEqual([ - 'doCheck-A', 'doCheck-B', 'doCheck-C', 'afterContentChecked-C', 'afterViewChecked-C', - 'afterContentChecked-A', 'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B' - ]); - - log.length = 0; - directiveInstance !.vcref.remove(0); - fixture.update(); - expect(log).toEqual([ - 'onDestroy-C', 'doCheck-A', 'doCheck-B', 'afterContentChecked-A', 'afterContentChecked-B', - 'afterViewChecked-A', 'afterViewChecked-B' - ]); - }); - - it('should call all hooks in correct order when creating with createComponent', () => { - @Component({ - template: ` - - - ` - }) - class SomeComponent { - static ngComponentDef = ΔdefineComponent({ - type: SomeComponent, - encapsulation: ViewEncapsulation.None, - selectors: [['some-comp']], - factory: () => new SomeComponent(), - consts: 2, - vars: 2, - template: (rf: RenderFlags, cmp: SomeComponent) => { - if (rf & RenderFlags.Create) { - Δelement(0, 'hooks', ['vcref', '']); - Δelement(1, 'hooks'); - } - if (rf & RenderFlags.Update) { - ΔelementProperty(0, 'name', Δbind('A')); - ΔelementProperty(1, 'name', Δbind('B')); - } - }, - directives: [ComponentWithHooks, DirectiveWithVCRef], - features: [ΔNgOnChangesFeature()], - }); - } - - log.length = 0; - - const fixture = new ComponentFixture(SomeComponent); - expect(log).toEqual([ - 'onChanges-A', 'onInit-A', 'doCheck-A', 'onChanges-B', 'onInit-B', 'doCheck-B', - 'afterContentInit-A', 'afterContentChecked-A', 'afterContentInit-B', - 'afterContentChecked-B', 'afterViewInit-A', 'afterViewChecked-A', 'afterViewInit-B', - 'afterViewChecked-B' - ]); - - log.length = 0; - fixture.update(); - expect(log).toEqual([ - 'doCheck-A', 'doCheck-B', 'afterContentChecked-A', 'afterContentChecked-B', - 'afterViewChecked-A', 'afterViewChecked-B' - ]); - - log.length = 0; - const componentRef = directiveInstance !.vcref.createComponent( - directiveInstance !.cfr.resolveComponentFactory(ComponentWithHooks)); - expect(fixture.html).toEqual('AB'); - expect(log).toEqual([]); - - componentRef.instance.name = 'D'; - log.length = 0; - fixture.update(); - expect(fixture.html).toEqual('ADB'); - expect(log).toEqual([ - 'doCheck-A', 'doCheck-B', 'onInit-D', 'doCheck-D', 'afterContentInit-D', - 'afterContentChecked-D', 'afterViewInit-D', 'afterViewChecked-D', 'afterContentChecked-A', - 'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B' - ]); - - log.length = 0; - fixture.update(); - expect(log).toEqual([ - 'doCheck-A', 'doCheck-B', 'doCheck-D', 'afterContentChecked-D', 'afterViewChecked-D', - 'afterContentChecked-A', 'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B' - ]); - - log.length = 0; - const viewRef = directiveInstance !.vcref.detach(0); - fixture.update(); - expect(log).toEqual([ - 'doCheck-A', 'doCheck-B', 'afterContentChecked-A', 'afterContentChecked-B', - 'afterViewChecked-A', 'afterViewChecked-B' - ]); - - log.length = 0; - directiveInstance !.vcref.insert(viewRef !); - fixture.update(); - expect(log).toEqual([ - 'doCheck-A', 'doCheck-B', 'doCheck-D', 'afterContentChecked-D', 'afterViewChecked-D', - 'afterContentChecked-A', 'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B' - ]); - - log.length = 0; - directiveInstance !.vcref.remove(0); - fixture.update(); - expect(log).toEqual([ - 'onDestroy-D', 'doCheck-A', 'doCheck-B', 'afterContentChecked-A', 'afterContentChecked-B', - 'afterViewChecked-A', 'afterViewChecked-B' - ]); - }); - }); - - describe('host bindings', () => { - - it('should support host bindings on dynamically created components', () => { - - @Component( - {selector: 'host-bindings', host: {'id': 'attribute', '[title]': 'title'}, template: ``}) - class HostBindingCmpt { - title = 'initial'; - - static ngComponentDef = ΔdefineComponent({ - type: HostBindingCmpt, - selectors: [['host-bindings']], - factory: () => new HostBindingCmpt(), - consts: 0, - vars: 0, - template: (rf: RenderFlags, cmp: HostBindingCmpt) => {}, - hostBindings: function(rf: RenderFlags, ctx: HostBindingCmpt, elIndex: number) { - if (rf & RenderFlags.Create) { - ΔelementHostAttrs(['id', 'attribute']); - ΔallocHostVars(1); - } - if (rf & RenderFlags.Update) { - ΔelementProperty(elIndex, 'title', Δbind(ctx.title)); - } - }, - }); - } - - @Component({ - template: ` - - ` - }) - class AppCmpt { - static ngComponentDef = ΔdefineComponent({ - type: AppCmpt, - selectors: [['app']], - factory: () => new AppCmpt(), - consts: 1, - vars: 0, - template: (rf: RenderFlags, cmp: AppCmpt) => { - if (rf & RenderFlags.Create) { - Δtemplate(0, null, 0, 0, 'ng-template', ['vcref', '']); - } - }, - directives: [HostBindingCmpt, DirectiveWithVCRef] - }); - } - - const fixture = new ComponentFixture(AppCmpt); - expect(fixture.html).toBe(''); - - const componentRef = directiveInstance !.vcref.createComponent( - directiveInstance !.cfr.resolveComponentFactory(HostBindingCmpt)); - fixture.update(); - expect(fixture.html).toBe(''); - - - componentRef.instance.title = 'changed'; - fixture.update(); - expect(fixture.html).toBe(''); - }); - - }); - describe('view engine compatibility', () => { @Component({selector: 'app', template: ''}) @@ -1977,79 +416,6 @@ describe('ViewContainerRef', () => { expect(parentInjector.get('foo')).toEqual('bar'); }); - it('should check bindings for components dynamically created by root component', () => { - class DynamicCompWithBindings { - checkCount = 0; - - ngDoCheck() { this.checkCount++; } - - /** check count: {{ checkCount }} */ - static ngComponentDef = ΔdefineComponent({ - type: DynamicCompWithBindings, - selectors: [['dynamic-cmpt-with-bindings']], - factory: () => new DynamicCompWithBindings(), - consts: 1, - vars: 1, - template: (rf: RenderFlags, ctx: DynamicCompWithBindings) => { - if (rf & RenderFlags.Create) { - Δtext(0); - } - if (rf & RenderFlags.Update) { - ΔtextBinding(0, Δinterpolation1('check count: ', ctx.checkCount, '')); - } - } - }); - } - - const fixture = new ComponentFixture(AppCmpt); - expect(fixture.outerHtml).toBe('
'); - - fixture.component.insert(DynamicCompWithBindings); - fixture.update(); - expect(fixture.outerHtml) - .toBe( - '
check count: 1'); - - fixture.update(); - expect(fixture.outerHtml) - .toBe( - '
check count: 2'); - }); - - it('should create deep DOM tree immediately for dynamically created components', () => { - let name = 'text'; - const Child = createComponent('child', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ΔelementStart(0, 'div'); - { Δtext(1); } - ΔelementEnd(); - } - if (rf & RenderFlags.Update) { - ΔtextBinding(1, Δbind(name)); - } - }, 2, 1); - - const DynamicCompWithChildren = - createComponent('dynamic-cmpt-with-children', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - Δelement(0, 'child'); - } - }, 1, 0, [Child]); - - const fixture = new ComponentFixture(AppCmpt); - expect(fixture.outerHtml).toBe('
'); - - fixture.component.insert(DynamicCompWithChildren); - expect(fixture.outerHtml) - .toBe( - '
'); - - fixture.update(); - expect(fixture.outerHtml) - .toBe( - '
text
'); - }); - it('should support view queries for dynamically created components', () => { let dynamicComp !: DynamicCompWithViewQueries; let fooEl !: RElement;