Skip to content

Commit 213dabd

Browse files
committed
fix(view): remove dynamic components when the parent view is dehydrated
Also adds a bunch of unit tests for affected parts. Fixes angular#1201
1 parent 6ecaa9a commit 213dabd

File tree

16 files changed

+812
-116
lines changed

16 files changed

+812
-116
lines changed

modules/angular2/src/change_detection/interfaces.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export class ChangeDetector {
4949
addChild(cd:ChangeDetector) {}
5050
addShadowDomChild(cd:ChangeDetector) {}
5151
removeChild(cd:ChangeDetector) {}
52+
removeShadowDomChild(cd:ChangeDetector) {}
5253
remove() {}
5354
hydrate(context:any, locals:Locals, directives:any) {}
5455
dehydrate() {}

modules/angular2/src/core/compiler/dynamic_component_loader.js

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,12 @@ export class ComponentRef {
4343
export class DynamicComponentLoader {
4444
_compiler:Compiler;
4545
_viewFactory:ViewFactory;
46-
_renderer:Renderer;
4746
_directiveMetadataReader:DirectiveMetadataReader;
4847

4948
constructor(compiler:Compiler, directiveMetadataReader:DirectiveMetadataReader,
5049
renderer:Renderer, viewFactory:ViewFactory) {
5150
this._compiler = compiler;
5251
this._directiveMetadataReader = directiveMetadataReader;
53-
this._renderer = renderer;
5452
this._viewFactory = viewFactory
5553
}
5654

@@ -67,16 +65,13 @@ export class DynamicComponentLoader {
6765

6866
var hostEi = location.elementInjector;
6967
var hostView = location.hostView;
70-
7168
return this._compiler.compile(type).then(componentProtoView => {
7269
var component = hostEi.dynamicallyCreateComponent(type, directiveMetadata.annotation, inj);
7370
var componentView = this._instantiateAndHydrateView(componentProtoView, injector, hostEi, component);
7471

7572
//TODO(vsavkin): do not use component child views as we need to clear the dynamically created views
7673
//same problem exists on the render side
77-
hostView.addComponentChildView(componentView);
78-
79-
this._renderer.setDynamicComponentView(hostView.render, location.boundElementIndex, componentView.render);
74+
hostView.setDynamicComponentChildView(location.boundElementIndex, componentView);
8075

8176
// TODO(vsavkin): return a component ref that dehydrates the component view and removes it
8277
// from the component child views

modules/angular2/src/core/compiler/element_binder.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {int, isBlank, BaseException} from 'angular2/src/facade/lang';
1+
import {int, isBlank, isPresent, BaseException} from 'angular2/src/facade/lang';
22
import * as eiModule from './element_injector';
33
import {DirectiveBinding} from './element_injector';
44
import {List, StringMap} from 'angular2/src/facade/collection';
@@ -32,4 +32,12 @@ export class ElementBinder {
3232
// updated later, so we are able to resolve cycles
3333
this.nestedProtoView = null;
3434
}
35+
36+
hasStaticComponent() {
37+
return isPresent(this.componentDirective) && isPresent(this.nestedProtoView);
38+
}
39+
40+
hasDynamicComponent() {
41+
return isPresent(this.componentDirective) && isBlank(this.nestedProtoView);
42+
}
3543
}

modules/angular2/src/core/compiler/view.js

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,6 @@ export class AppView {
143143
}
144144

145145
var binders = this.proto.elementBinders;
146-
var componentChildViewIndex = 0;
147146
for (var i = 0; i < binders.length; ++i) {
148147
var componentDirective = binders[i].componentDirective;
149148
var shadowDomAppInjector = null;
@@ -176,16 +175,15 @@ export class AppView {
176175
}
177176
}
178177

179-
if (isPresent(binders[i].nestedProtoView) && isPresent(componentDirective)) {
180-
renderComponentIndex = this.componentChildViews[componentChildViewIndex].internalHydrateRecurse(
178+
if (binders[i].hasStaticComponent()) {
179+
renderComponentIndex = this.componentChildViews[i].internalHydrateRecurse(
181180
renderComponentViewRefs,
182181
renderComponentIndex,
183182
shadowDomAppInjector,
184183
elementInjector,
185184
elementInjector.getComponent(),
186185
null
187186
);
188-
componentChildViewIndex++;
189187
}
190188
}
191189
this._hydrateChangeDetector();
@@ -198,7 +196,15 @@ export class AppView {
198196

199197
// componentChildViews
200198
for (var i = 0; i < this.componentChildViews.length; i++) {
201-
this.componentChildViews[i].internalDehydrateRecurse();
199+
var componentView = this.componentChildViews[i];
200+
if (isPresent(componentView)) {
201+
componentView.internalDehydrateRecurse();
202+
var binder = this.proto.elementBinders[i];
203+
if (binder.hasDynamicComponent()) {
204+
this.componentChildViews[i] = null;
205+
this.changeDetector.removeShadowDomChild(componentView.changeDetector);
206+
}
207+
}
202208
}
203209

204210
// elementInjectors
@@ -255,9 +261,16 @@ export class AppView {
255261
return elementInjector.getDirectiveAtIndex(directive.directiveIndex);
256262
}
257263

258-
addComponentChildView(view:AppView) {
259-
ListWrapper.push(this.componentChildViews, view);
264+
setDynamicComponentChildView(boundElementIndex, view:AppView) {
265+
if (!this.proto.elementBinders[boundElementIndex].hasDynamicComponent()) {
266+
throw new BaseException(`There is no dynamic component directive at element ${boundElementIndex}`);
267+
}
268+
if (isPresent(this.componentChildViews[boundElementIndex])) {
269+
throw new BaseException(`There already is a bound component at element ${boundElementIndex}`);
270+
}
271+
this.componentChildViews[boundElementIndex] = view;
260272
this.changeDetector.addShadowDomChild(view.changeDetector);
273+
this.proto.renderer.setDynamicComponentView(this.render, boundElementIndex, view.render);
261274
}
262275

263276
// implementation of EventDispatcher#dispatchEvent

modules/angular2/src/core/compiler/view_factory.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export class ViewFactory {
5757
var rootElementInjectors = [];
5858
var preBuiltObjects = ListWrapper.createFixedSize(binders.length);
5959
var viewContainers = ListWrapper.createFixedSize(binders.length);
60-
var componentChildViews = [];
60+
var componentChildViews = ListWrapper.createFixedSize(binders.length);
6161

6262
for (var binderIdx = 0; binderIdx < binders.length; binderIdx++) {
6363
var binder = binders[binderIdx];
@@ -78,13 +78,13 @@ export class ViewFactory {
7878

7979
// componentChildViews
8080
var bindingPropagationConfig = null;
81-
if (isPresent(binder.nestedProtoView) && isPresent(binder.componentDirective)) {
81+
if (binder.hasStaticComponent()) {
8282
var childView = this._createView(binder.nestedProtoView);
83-
changeDetector.addChild(childView.changeDetector);
83+
changeDetector.addShadowDomChild(childView.changeDetector);
8484

8585
bindingPropagationConfig = new BindingPropagationConfig(childView.changeDetector);
8686

87-
ListWrapper.push(componentChildViews, childView);
87+
componentChildViews[binderIdx] = childView;
8888
}
8989

9090
// viewContainers

modules/angular2/src/render/dom/view/element_binder.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {isBlank, isPresent} from 'angular2/src/facade/lang';
12
import {AST} from 'angular2/change_detection';
23
import {SetterFn} from 'angular2/src/reflection/types';
34
import {List, ListWrapper} from 'angular2/src/facade/collection';
@@ -38,6 +39,14 @@ export class ElementBinder {
3839
this.distanceToParent = distanceToParent;
3940
this.propertySetters = propertySetters;
4041
}
42+
43+
hasStaticComponent() {
44+
return isPresent(this.componentId) && isPresent(this.nestedProtoView);
45+
}
46+
47+
hasDynamicComponent() {
48+
return isPresent(this.componentId) && isBlank(this.nestedProtoView);
49+
}
4150
}
4251

4352
export class Event {

modules/angular2/src/render/dom/view/view.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,11 @@ export class RenderView {
165165
var cv = this.componentChildViews[i];
166166
if (isPresent(cv)) {
167167
cv.dehydrate();
168+
if (this.proto.elementBinders[i].hasDynamicComponent()) {
169+
ViewContainer.removeViewNodes(cv);
170+
this.lightDoms[i] = null;
171+
this.componentChildViews[i] = null;
172+
}
168173
}
169174
}
170175

modules/angular2/src/render/dom/view/view_factory.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ export class ViewFactory {
8484
} else {
8585
viewRootNodes = [rootElementClone];
8686
}
87-
8887
var binders = protoView.elementBinders;
8988
var boundTextNodes = [];
9089
var boundElements = ListWrapper.createFixedSize(binders.length);
@@ -133,7 +132,7 @@ export class ViewFactory {
133132
var element = boundElements[binderIdx];
134133

135134
// static child components
136-
if (isPresent(binder.componentId) && isPresent(binder.nestedProtoView)) {
135+
if (binder.hasStaticComponent()) {
137136
var childView = this._createView(binder.nestedProtoView);
138137
view.setComponentView(this._shadowDomStrategy, binderIdx, childView);
139138
}

modules/angular2/src/test_lib/test_lib.dart

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
library test_lib.test_lib;
22

33
import 'package:guinness/guinness.dart' as gns;
4-
export 'package:guinness/guinness.dart' hide Expect, expect, NotExpect, beforeEach, it, iit, xit;
4+
export 'package:guinness/guinness.dart' hide Expect, expect, NotExpect, beforeEach, it, iit, xit, SpyObject;
55
import 'package:unittest/unittest.dart' hide expect;
66

77
import 'dart:async';
@@ -149,6 +149,13 @@ xit(name, fn) {
149149
_it(gns.xit, name, fn);
150150
}
151151

152+
class SpyObject extends gns.SpyObject {
153+
// Need to take an optional type as this is required by
154+
// the JS SpyObject.
155+
SpyObject([type = null]) {
156+
}
157+
}
158+
152159
String elementText(n) {
153160
hasNodes(n) {
154161
var children = DOM.childNodes(n);
Lines changed: 95 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,59 @@
1-
import {ddescribe, describe, it, iit, expect, beforeEach} from 'angular2/test_lib';
1+
import {
2+
AsyncTestCompleter,
3+
beforeEach,
4+
ddescribe,
5+
xdescribe,
6+
describe,
7+
el,
8+
dispatchEvent,
9+
expect,
10+
iit,
11+
inject,
12+
beforeEachBindings,
13+
it,
14+
xit,
15+
SpyObject, proxy
16+
} from 'angular2/test_lib';
17+
import {IMPLEMENTS} from 'angular2/src/facade/lang';
18+
import {MapWrapper, ListWrapper} from 'angular2/src/facade/collection';
19+
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
220
import {DirectiveMetadataReader} from 'angular2/src/core/compiler/directive_metadata_reader';
321
import {DynamicComponentLoader} from 'angular2/src/core/compiler/dynamic_component_loader';
4-
import {Decorator, Viewport} from 'angular2/src/core/annotations/annotations';
5-
6-
@Decorator({selector: 'someDecorator'})
7-
class SomeDecorator {}
8-
9-
@Viewport({selector: 'someViewport'})
10-
class SomeViewport {}
22+
import {Decorator, Viewport, Component} from 'angular2/src/core/annotations/annotations';
23+
import {ElementRef, ElementInjector, ProtoElementInjector, PreBuiltObjects} from 'angular2/src/core/compiler/element_injector';
24+
import {Compiler} from 'angular2/src/core/compiler/compiler';
25+
import {AppProtoView, AppView} from 'angular2/src/core/compiler/view';
26+
import {ViewFactory} from 'angular2/src/core/compiler/view_factory'
27+
import {Renderer} from 'angular2/src/render/api';
1128

1229
export function main() {
1330
describe("DynamicComponentLoader", () => {
31+
var compiler;
32+
var viewFactory;
33+
var directiveMetadataReader;
34+
var renderer;
1435
var loader;
1536

16-
beforeEach(() => {
17-
loader = new DynamicComponentLoader(null, new DirectiveMetadataReader(), null, null);
37+
beforeEach( () => {
38+
compiler = new SpyCompiler();
39+
viewFactory = new SpyViewFactory();
40+
renderer = new SpyRenderer();
41+
directiveMetadataReader = new DirectiveMetadataReader();
42+
loader = new DynamicComponentLoader(compiler, directiveMetadataReader, renderer, viewFactory);;
1843
});
1944

45+
function createProtoView() {
46+
return new AppProtoView(null, null, null);
47+
}
48+
49+
function createElementRef(view, boundElementIndex) {
50+
var peli = new ProtoElementInjector(null, boundElementIndex, []);
51+
var eli = new ElementInjector(peli, null);
52+
var preBuiltObjects = new PreBuiltObjects(view, null, null, null);
53+
eli.instantiateDirectives(null, null, null, preBuiltObjects);
54+
return new ElementRef(eli);
55+
}
56+
2057
describe("loadIntoExistingLocation", () => {
2158
describe('Load errors', () => {
2259
it('should throw when trying to load a decorator', () => {
@@ -29,7 +66,55 @@ export function main() {
2966
.toThrowError("Could not load 'SomeViewport' because it is not a component.");
3067
});
3168
});
69+
70+
it('should add the child view into the host view', inject([AsyncTestCompleter], (async) => {
71+
var log = [];
72+
var hostView = new SpyAppView();
73+
var childView = new SpyAppView();
74+
hostView.spy('setDynamicComponentChildView').andCallFake( (boundElementIndex, childView) => {
75+
ListWrapper.push(log, ['setDynamicComponentChildView', boundElementIndex, childView]);
76+
});
77+
childView.spy('hydrate').andCallFake( (appInjector, hostElementInjector, context, locals) => {
78+
ListWrapper.push(log, 'hydrate');
79+
});
80+
compiler.spy('compile').andCallFake( (_) => PromiseWrapper.resolve(createProtoView()));
81+
viewFactory.spy('getView').andCallFake( (_) => childView);
82+
83+
var elementRef = createElementRef(hostView, 23);
84+
loader.loadIntoExistingLocation(SomeComponent, elementRef).then( (componentRef) => {
85+
expect(log[0]).toEqual('hydrate');
86+
expect(log[1]).toEqual(['setDynamicComponentChildView', 23, childView]);
87+
async.done();
88+
});
89+
}));
90+
3291
});
3392

3493
});
3594
}
95+
96+
@Decorator({selector: 'someDecorator'})
97+
class SomeDecorator {}
98+
99+
@Viewport({selector: 'someViewport'})
100+
class SomeViewport {}
101+
102+
@Component({selector: 'someComponent'})
103+
class SomeComponent {}
104+
105+
106+
@proxy
107+
@IMPLEMENTS(Compiler)
108+
class SpyCompiler extends SpyObject {noSuchMethod(m){return super.noSuchMethod(m)}}
109+
110+
@proxy
111+
@IMPLEMENTS(ViewFactory)
112+
class SpyViewFactory extends SpyObject {noSuchMethod(m){return super.noSuchMethod(m)}}
113+
114+
@proxy
115+
@IMPLEMENTS(Renderer)
116+
class SpyRenderer extends SpyObject {noSuchMethod(m){return super.noSuchMethod(m)}}
117+
118+
@proxy
119+
@IMPLEMENTS(AppView)
120+
class SpyAppView extends SpyObject {noSuchMethod(m){return super.noSuchMethod(m)}}

0 commit comments

Comments
 (0)