fix(view): remove dynamic components when the parent view is dehydrated
Also adds a bunch of unit tests for affected parts. Fixes #1201
This commit is contained in:
@ -1,22 +1,59 @@
|
||||
import {ddescribe, describe, it, iit, expect, beforeEach} from 'angular2/test_lib';
|
||||
import {
|
||||
AsyncTestCompleter,
|
||||
beforeEach,
|
||||
ddescribe,
|
||||
xdescribe,
|
||||
describe,
|
||||
el,
|
||||
dispatchEvent,
|
||||
expect,
|
||||
iit,
|
||||
inject,
|
||||
beforeEachBindings,
|
||||
it,
|
||||
xit,
|
||||
SpyObject, proxy
|
||||
} from 'angular2/test_lib';
|
||||
import {IMPLEMENTS} from 'angular2/src/facade/lang';
|
||||
import {MapWrapper, ListWrapper} from 'angular2/src/facade/collection';
|
||||
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
|
||||
import {DirectiveMetadataReader} from 'angular2/src/core/compiler/directive_metadata_reader';
|
||||
import {DynamicComponentLoader} from 'angular2/src/core/compiler/dynamic_component_loader';
|
||||
import {Decorator, Viewport} from 'angular2/src/core/annotations/annotations';
|
||||
|
||||
@Decorator({selector: 'someDecorator'})
|
||||
class SomeDecorator {}
|
||||
|
||||
@Viewport({selector: 'someViewport'})
|
||||
class SomeViewport {}
|
||||
import {Decorator, Viewport, Component} from 'angular2/src/core/annotations/annotations';
|
||||
import {ElementRef, ElementInjector, ProtoElementInjector, PreBuiltObjects} from 'angular2/src/core/compiler/element_injector';
|
||||
import {Compiler} from 'angular2/src/core/compiler/compiler';
|
||||
import {AppProtoView, AppView} from 'angular2/src/core/compiler/view';
|
||||
import {ViewFactory} from 'angular2/src/core/compiler/view_factory'
|
||||
import {Renderer} from 'angular2/src/render/api';
|
||||
|
||||
export function main() {
|
||||
describe("DynamicComponentLoader", () => {
|
||||
var compiler;
|
||||
var viewFactory;
|
||||
var directiveMetadataReader;
|
||||
var renderer;
|
||||
var loader;
|
||||
|
||||
beforeEach(() => {
|
||||
loader = new DynamicComponentLoader(null, new DirectiveMetadataReader(), null, null);
|
||||
beforeEach( () => {
|
||||
compiler = new SpyCompiler();
|
||||
viewFactory = new SpyViewFactory();
|
||||
renderer = new SpyRenderer();
|
||||
directiveMetadataReader = new DirectiveMetadataReader();
|
||||
loader = new DynamicComponentLoader(compiler, directiveMetadataReader, renderer, viewFactory);;
|
||||
});
|
||||
|
||||
function createProtoView() {
|
||||
return new AppProtoView(null, null, null);
|
||||
}
|
||||
|
||||
function createElementRef(view, boundElementIndex) {
|
||||
var peli = new ProtoElementInjector(null, boundElementIndex, []);
|
||||
var eli = new ElementInjector(peli, null);
|
||||
var preBuiltObjects = new PreBuiltObjects(view, null, null, null);
|
||||
eli.instantiateDirectives(null, null, null, preBuiltObjects);
|
||||
return new ElementRef(eli);
|
||||
}
|
||||
|
||||
describe("loadIntoExistingLocation", () => {
|
||||
describe('Load errors', () => {
|
||||
it('should throw when trying to load a decorator', () => {
|
||||
@ -29,7 +66,55 @@ export function main() {
|
||||
.toThrowError("Could not load 'SomeViewport' because it is not a component.");
|
||||
});
|
||||
});
|
||||
|
||||
it('should add the child view into the host view', inject([AsyncTestCompleter], (async) => {
|
||||
var log = [];
|
||||
var hostView = new SpyAppView();
|
||||
var childView = new SpyAppView();
|
||||
hostView.spy('setDynamicComponentChildView').andCallFake( (boundElementIndex, childView) => {
|
||||
ListWrapper.push(log, ['setDynamicComponentChildView', boundElementIndex, childView]);
|
||||
});
|
||||
childView.spy('hydrate').andCallFake( (appInjector, hostElementInjector, context, locals) => {
|
||||
ListWrapper.push(log, 'hydrate');
|
||||
});
|
||||
compiler.spy('compile').andCallFake( (_) => PromiseWrapper.resolve(createProtoView()));
|
||||
viewFactory.spy('getView').andCallFake( (_) => childView);
|
||||
|
||||
var elementRef = createElementRef(hostView, 23);
|
||||
loader.loadIntoExistingLocation(SomeComponent, elementRef).then( (componentRef) => {
|
||||
expect(log[0]).toEqual('hydrate');
|
||||
expect(log[1]).toEqual(['setDynamicComponentChildView', 23, childView]);
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@Decorator({selector: 'someDecorator'})
|
||||
class SomeDecorator {}
|
||||
|
||||
@Viewport({selector: 'someViewport'})
|
||||
class SomeViewport {}
|
||||
|
||||
@Component({selector: 'someComponent'})
|
||||
class SomeComponent {}
|
||||
|
||||
|
||||
@proxy
|
||||
@IMPLEMENTS(Compiler)
|
||||
class SpyCompiler extends SpyObject {noSuchMethod(m){return super.noSuchMethod(m)}}
|
||||
|
||||
@proxy
|
||||
@IMPLEMENTS(ViewFactory)
|
||||
class SpyViewFactory extends SpyObject {noSuchMethod(m){return super.noSuchMethod(m)}}
|
||||
|
||||
@proxy
|
||||
@IMPLEMENTS(Renderer)
|
||||
class SpyRenderer extends SpyObject {noSuchMethod(m){return super.noSuchMethod(m)}}
|
||||
|
||||
@proxy
|
||||
@IMPLEMENTS(AppView)
|
||||
class SpyAppView extends SpyObject {noSuchMethod(m){return super.noSuchMethod(m)}}
|
||||
|
@ -567,7 +567,7 @@ export function main() {
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
it('should support render global events from multiple directives', inject([TestBed, AsyncTestCompleter], (tb, async) => {
|
||||
tb.overrideView(MyComp, new View({
|
||||
template: '<div *if="ctxBoolProp" listener listenerother></div>',
|
||||
@ -636,6 +636,40 @@ export function main() {
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
it('should allow to destroy and create them via viewport directives',
|
||||
inject([TestBed, AsyncTestCompleter], (tb, async) => {
|
||||
tb.overrideView(MyComp, new View({
|
||||
template: '<div><dynamic-comp #dynamic template="if: ctxBoolProp"></dynamic-comp></div>',
|
||||
directives: [DynamicComp, If]
|
||||
}));
|
||||
|
||||
tb.createView(MyComp).then((view) => {
|
||||
view.context.ctxBoolProp = true;
|
||||
view.detectChanges();
|
||||
var dynamicComponent = view.rawView.viewContainers[0].get(0).locals.get("dynamic");
|
||||
dynamicComponent.done.then((_) => {
|
||||
view.detectChanges();
|
||||
expect(view.rootNodes).toHaveText('hello');
|
||||
|
||||
view.context.ctxBoolProp = false;
|
||||
view.detectChanges();
|
||||
|
||||
expect(view.rawView.viewContainers[0].length).toBe(0);
|
||||
expect(view.rootNodes).toHaveText('');
|
||||
|
||||
view.context.ctxBoolProp = true;
|
||||
view.detectChanges();
|
||||
|
||||
var dynamicComponent = view.rawView.viewContainers[0].get(0).locals.get("dynamic");
|
||||
return dynamicComponent.done;
|
||||
}).then((_) => {
|
||||
view.detectChanges();
|
||||
expect(view.rootNodes).toHaveText('hello');
|
||||
async.done();
|
||||
});
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
it('should support static attributes', inject([TestBed, AsyncTestCompleter], (tb, async) => {
|
||||
|
@ -1,23 +1,71 @@
|
||||
import {describe, ddescribe, it, iit, xit, xdescribe, expect, beforeEach, el} from 'angular2/test_lib';
|
||||
|
||||
import {
|
||||
AsyncTestCompleter,
|
||||
beforeEach,
|
||||
ddescribe,
|
||||
xdescribe,
|
||||
describe,
|
||||
el,
|
||||
dispatchEvent,
|
||||
expect,
|
||||
iit,
|
||||
inject,
|
||||
beforeEachBindings,
|
||||
it,
|
||||
xit,
|
||||
SpyObject, proxy
|
||||
} from 'angular2/test_lib';
|
||||
import {IMPLEMENTS, isBlank} from 'angular2/src/facade/lang';
|
||||
import {ViewFactory} from 'angular2/src/core/compiler/view_factory';
|
||||
import {AppProtoView, AppView} from 'angular2/src/core/compiler/view';
|
||||
import {dynamicChangeDetection} from 'angular2/change_detection';
|
||||
import {DirectiveBinding, ElementInjector} from 'angular2/src/core/compiler/element_injector';
|
||||
import {DirectiveMetadataReader} from 'angular2/src/core/compiler/directive_metadata_reader';
|
||||
import {Component} from 'angular2/src/core/annotations/annotations';
|
||||
import {ElementBinder} from 'angular2/src/core/compiler/element_binder';
|
||||
import {ChangeDetector, ProtoChangeDetector} from 'angular2/change_detection';
|
||||
|
||||
export function main() {
|
||||
function createViewFactory({capacity}):ViewFactory {
|
||||
return new ViewFactory(capacity);
|
||||
}
|
||||
describe('AppViewFactory', () => {
|
||||
var reader;
|
||||
|
||||
function createPv() {
|
||||
return new AppProtoView(null,
|
||||
null,
|
||||
dynamicChangeDetection.createProtoChangeDetector('dummy', null));
|
||||
}
|
||||
beforeEach( () => {
|
||||
reader = new DirectiveMetadataReader();
|
||||
});
|
||||
|
||||
function createViewFactory({capacity}):ViewFactory {
|
||||
return new ViewFactory(capacity);
|
||||
}
|
||||
|
||||
function createProtoChangeDetector() {
|
||||
var pcd = new SpyProtoChangeDetector();
|
||||
pcd.spy('instantiate').andCallFake( (dispatcher, bindingRecords, variableBindings, directiveRecords) => {
|
||||
return new SpyChangeDetector();
|
||||
});
|
||||
return pcd;
|
||||
}
|
||||
|
||||
function createProtoView(binders=null) {
|
||||
if (isBlank(binders)) {
|
||||
binders = [];
|
||||
}
|
||||
var pv = new AppProtoView(null, null, createProtoChangeDetector());
|
||||
pv.elementBinders = binders;
|
||||
return pv;
|
||||
}
|
||||
|
||||
function createDirectiveBinding(type) {
|
||||
var meta = reader.read(type);
|
||||
return DirectiveBinding.createFromType(meta.type, meta.annotation);
|
||||
}
|
||||
|
||||
function createComponentElBinder(binding, nestedProtoView = null) {
|
||||
var binder = new ElementBinder(0, null, 0, null, binding, null);
|
||||
binder.nestedProtoView = nestedProtoView;
|
||||
return binder;
|
||||
}
|
||||
|
||||
describe('RenderViewFactory', () => {
|
||||
it('should create views', () => {
|
||||
var pv = createPv();
|
||||
var pv = createProtoView();
|
||||
var vf = createViewFactory({
|
||||
capacity: 1
|
||||
});
|
||||
@ -27,8 +75,8 @@ export function main() {
|
||||
describe('caching', () => {
|
||||
|
||||
it('should support multiple AppProtoViews', () => {
|
||||
var pv1 = createPv();
|
||||
var pv2 = createPv();
|
||||
var pv1 = createProtoView();
|
||||
var pv2 = createProtoView();
|
||||
var vf = createViewFactory({ capacity: 2 });
|
||||
var view1 = vf.getView(pv1);
|
||||
var view2 = vf.getView(pv2);
|
||||
@ -40,7 +88,7 @@ export function main() {
|
||||
});
|
||||
|
||||
it('should reuse the newest view that has been returned', () => {
|
||||
var pv = createPv();
|
||||
var pv = createProtoView();
|
||||
var vf = createViewFactory({ capacity: 2 });
|
||||
var view1 = vf.getView(pv);
|
||||
var view2 = vf.getView(pv);
|
||||
@ -51,7 +99,7 @@ export function main() {
|
||||
});
|
||||
|
||||
it('should not add views when the capacity has been reached', () => {
|
||||
var pv = createPv();
|
||||
var pv = createProtoView();
|
||||
var vf = createViewFactory({ capacity: 2 });
|
||||
var view1 = vf.getView(pv);
|
||||
var view2 = vf.getView(pv);
|
||||
@ -66,5 +114,57 @@ export function main() {
|
||||
|
||||
});
|
||||
|
||||
describe('child components', () => {
|
||||
|
||||
var vf;
|
||||
|
||||
beforeEach(() => {
|
||||
vf = createViewFactory({capacity: 1});
|
||||
});
|
||||
|
||||
it('should create static child component views', () => {
|
||||
var hostPv = createProtoView([
|
||||
createComponentElBinder(
|
||||
createDirectiveBinding(SomeComponent),
|
||||
createProtoView()
|
||||
)
|
||||
]);
|
||||
var hostView = vf.getView(hostPv);
|
||||
var shadowView = hostView.componentChildViews[0];
|
||||
expect(shadowView).toBeTruthy();
|
||||
expect(hostView.changeDetector.spy('addShadowDomChild')).toHaveBeenCalledWith(shadowView.changeDetector);
|
||||
});
|
||||
|
||||
it('should not create dynamic child component views', () => {
|
||||
var hostPv = createProtoView([
|
||||
createComponentElBinder(
|
||||
createDirectiveBinding(SomeComponent),
|
||||
null
|
||||
)
|
||||
]);
|
||||
var hostView = vf.getView(hostPv);
|
||||
var shadowView = hostView.componentChildViews[0];
|
||||
expect(shadowView).toBeFalsy();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@Component({ selector: 'someComponent' })
|
||||
class SomeComponent {}
|
||||
|
||||
@proxy
|
||||
@IMPLEMENTS(ChangeDetector)
|
||||
class SpyChangeDetector extends SpyObject {
|
||||
constructor(){super(ChangeDetector);}
|
||||
noSuchMethod(m){return super.noSuchMethod(m)}
|
||||
}
|
||||
|
||||
@proxy
|
||||
@IMPLEMENTS(ProtoChangeDetector)
|
||||
class SpyProtoChangeDetector extends SpyObject {
|
||||
constructor(){super(ProtoChangeDetector);}
|
||||
noSuchMethod(m){return super.noSuchMethod(m)}
|
||||
}
|
||||
|
213
modules/angular2/test/core/compiler/view_spec.js
vendored
Normal file
213
modules/angular2/test/core/compiler/view_spec.js
vendored
Normal file
@ -0,0 +1,213 @@
|
||||
import {
|
||||
AsyncTestCompleter,
|
||||
beforeEach,
|
||||
ddescribe,
|
||||
xdescribe,
|
||||
describe,
|
||||
el,
|
||||
dispatchEvent,
|
||||
expect,
|
||||
iit,
|
||||
inject,
|
||||
beforeEachBindings,
|
||||
it,
|
||||
xit,
|
||||
SpyObject, proxy
|
||||
} from 'angular2/test_lib';
|
||||
import {IMPLEMENTS, isBlank} from 'angular2/src/facade/lang';
|
||||
import {MapWrapper, ListWrapper} from 'angular2/src/facade/collection';
|
||||
|
||||
import {AppProtoView, AppView} from 'angular2/src/core/compiler/view';
|
||||
import {Renderer, ViewRef} from 'angular2/src/render/api';
|
||||
import {ChangeDetector} from 'angular2/change_detection';
|
||||
import {ElementBinder} from 'angular2/src/core/compiler/element_binder';
|
||||
import {DirectiveBinding, ElementInjector} from 'angular2/src/core/compiler/element_injector';
|
||||
import {DirectiveMetadataReader} from 'angular2/src/core/compiler/directive_metadata_reader';
|
||||
import {Component} from 'angular2/src/core/annotations/annotations';
|
||||
|
||||
export function main() {
|
||||
describe('AppView', () => {
|
||||
var renderer;
|
||||
var reader;
|
||||
|
||||
beforeEach( () => {
|
||||
renderer = new SpyRenderer();
|
||||
reader = new DirectiveMetadataReader();
|
||||
});
|
||||
|
||||
function createDirectiveBinding(type) {
|
||||
var meta = reader.read(type);
|
||||
return DirectiveBinding.createFromType(meta.type, meta.annotation);
|
||||
}
|
||||
|
||||
function createElementInjector() {
|
||||
var res = new SpyElementInjector();
|
||||
res.spy('isExportingComponent').andCallFake( () => false );
|
||||
res.spy('isExportingElement').andCallFake( () => false );
|
||||
return res;
|
||||
}
|
||||
|
||||
function createEmptyElBinder() {
|
||||
return new ElementBinder(0, null, 0, null, null, null);
|
||||
}
|
||||
|
||||
function createComponentElBinder(binding, nestedProtoView = null) {
|
||||
var binder = new ElementBinder(0, null, 0, null, binding, null);
|
||||
binder.nestedProtoView = nestedProtoView;
|
||||
return binder;
|
||||
}
|
||||
|
||||
function createProtoView(binders = null) {
|
||||
if (isBlank(binders)) {
|
||||
binders = [];
|
||||
}
|
||||
var res = new AppProtoView(renderer, null, null);
|
||||
res.elementBinders = binders;
|
||||
return res;
|
||||
}
|
||||
|
||||
function createHostProtoView(nestedProtoView) {
|
||||
return createProtoView([
|
||||
createComponentElBinder(
|
||||
createDirectiveBinding(SomeComponent),
|
||||
nestedProtoView
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
function createHostView(pv, shadowView, componentInstance) {
|
||||
var view = new AppView(pv, MapWrapper.create());
|
||||
var changeDetector = new SpyChangeDetector();
|
||||
var eij = createElementInjector();
|
||||
eij.spy('getComponent').andCallFake( () => componentInstance );
|
||||
view.init(changeDetector, [eij], [eij],
|
||||
[null], [null], [shadowView]);
|
||||
return view;
|
||||
}
|
||||
|
||||
describe('setDynamicComponentChildView', () => {
|
||||
|
||||
it('should not allow to use non component indices', () => {
|
||||
var pv = createProtoView([createEmptyElBinder()]);
|
||||
var view = createHostView(pv, null, null);
|
||||
var shadowView = new FakeAppView();
|
||||
expect(
|
||||
() => view.setDynamicComponentChildView(0, shadowView)
|
||||
).toThrowError('There is no dynamic component directive at element 0');
|
||||
});
|
||||
|
||||
it('should not allow to use static component indices', () => {
|
||||
var pv = createHostProtoView(createProtoView());
|
||||
var view = createHostView(pv, null, null);
|
||||
var shadowView = new FakeAppView();
|
||||
expect(
|
||||
() => view.setDynamicComponentChildView(0, shadowView)
|
||||
).toThrowError('There is no dynamic component directive at element 0');
|
||||
});
|
||||
|
||||
it('should not allow to overwrite an existing component', () => {
|
||||
var pv = createHostProtoView(null);
|
||||
var shadowView = new FakeAppView();
|
||||
var view = createHostView(pv, null, null);
|
||||
view.setDynamicComponentChildView(0, shadowView);
|
||||
expect(
|
||||
() => view.setDynamicComponentChildView(0, shadowView)
|
||||
).toThrowError('There already is a bound component at element 0');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('hydrate', () => {
|
||||
|
||||
it('should hydrate existing child components', () => {
|
||||
var hostPv = createHostProtoView(createProtoView());
|
||||
var componentInstance = {};
|
||||
var shadowView = new FakeAppView();
|
||||
var hostView = createHostView(hostPv, shadowView, componentInstance);
|
||||
renderer.spy('createView').andCallFake( (_) => {
|
||||
return [new ViewRef(), new ViewRef()];
|
||||
});
|
||||
|
||||
hostView.hydrate(null, null, null, null);
|
||||
|
||||
expect(shadowView.spy('hydrate')).not.toHaveBeenCalled();
|
||||
expect(shadowView.spy('internalHydrateRecurse')).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('dehydrate', () => {
|
||||
var hostView;
|
||||
var shadowView;
|
||||
|
||||
function createAndHydrate(nestedProtoView) {
|
||||
var componentInstance = {};
|
||||
shadowView = new FakeAppView();
|
||||
var hostPv = createHostProtoView(nestedProtoView);
|
||||
hostView = createHostView(hostPv, shadowView, componentInstance);
|
||||
renderer.spy('createView').andCallFake( (_) => {
|
||||
return [new ViewRef(), new ViewRef()];
|
||||
});
|
||||
|
||||
hostView.hydrate(null, null, null, null);
|
||||
}
|
||||
|
||||
it('should dehydrate child components', () => {
|
||||
createAndHydrate(createProtoView());
|
||||
hostView.dehydrate();
|
||||
|
||||
expect(shadowView.spy('dehydrate')).not.toHaveBeenCalled();
|
||||
expect(shadowView.spy('internalDehydrateRecurse')).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not clear static child components', () => {
|
||||
createAndHydrate(createProtoView());
|
||||
hostView.dehydrate();
|
||||
|
||||
expect(hostView.componentChildViews[0]).toBe(shadowView);
|
||||
expect(hostView.changeDetector.spy('removeShadowDomChild')).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should clear dynamic child components', () => {
|
||||
createAndHydrate(null);
|
||||
hostView.dehydrate();
|
||||
|
||||
expect(hostView.componentChildViews[0]).toBe(null);
|
||||
expect(hostView.changeDetector.spy('removeShadowDomChild')).toHaveBeenCalledWith(shadowView.changeDetector);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@Component({ selector: 'someComponent' })
|
||||
class SomeComponent {}
|
||||
|
||||
@proxy
|
||||
@IMPLEMENTS(Renderer)
|
||||
class SpyRenderer extends SpyObject {
|
||||
constructor(){super(Renderer);}
|
||||
noSuchMethod(m){return super.noSuchMethod(m)}
|
||||
}
|
||||
|
||||
@proxy
|
||||
@IMPLEMENTS(ChangeDetector)
|
||||
class SpyChangeDetector extends SpyObject {
|
||||
constructor(){super(ChangeDetector);}
|
||||
noSuchMethod(m){return super.noSuchMethod(m)}
|
||||
}
|
||||
|
||||
@proxy
|
||||
@IMPLEMENTS(ElementInjector)
|
||||
class SpyElementInjector extends SpyObject {
|
||||
constructor(){super(ElementInjector);}
|
||||
noSuchMethod(m){return super.noSuchMethod(m)}
|
||||
}
|
||||
|
||||
@proxy
|
||||
@IMPLEMENTS(AppView)
|
||||
class FakeAppView extends SpyObject {
|
||||
constructor(){super(AppView);}
|
||||
noSuchMethod(m){return super.noSuchMethod(m)}
|
||||
}
|
Reference in New Issue
Block a user