Skip to content

Commit 5030ffb

Browse files
committed
feat(view): introduce free embedded views
Free embedded views are view instances that are created logically in the same was as views of a ViewContainer, but their dom nodes are not attached. BREAKING CHANGE: - `Renderer.detachFreeHostView` was renamed to `Renderer.detachFreeView` - `DomRenderer.getHostElement()` was generalized into `DomRenderer.getRootNodes()`
1 parent 9ce0870 commit 5030ffb

File tree

10 files changed

+230
-32
lines changed

10 files changed

+230
-32
lines changed

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

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,9 @@ import * as renderApi from 'angular2/src/render/api';
2323
import {EventDispatcher} from 'angular2/src/render/api';
2424

2525
export class AppViewContainer {
26-
views: List<AppView>;
27-
28-
constructor() {
29-
// The order in this list matches the DOM order.
30-
this.views = [];
31-
}
26+
// The order in this list matches the DOM order.
27+
views: List<AppView> = [];
28+
freeViews: List<AppView> = [];
3229
}
3330

3431
/**

modules/angular2/src/core/compiler/view_manager.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,24 @@ export class AppViewManager {
115115
this._destroyFreeHostView(parentView, hostView);
116116
}
117117

118+
createFreeEmbeddedView(location: ElementRef, protoViewRef: ProtoViewRef,
119+
injector: Injector = null): ViewRef {
120+
var protoView = internalProtoView(protoViewRef);
121+
var parentView = internalView(location.parentView);
122+
var boundElementIndex = location.boundElementIndex;
123+
124+
var view = this._createPooledView(protoView);
125+
this._utils.attachAndHydrateFreeEmbeddedView(parentView, boundElementIndex, view, injector);
126+
this._viewHydrateRecurse(view);
127+
return new ViewRef(view);
128+
}
129+
130+
destroyFreeEmbeddedView(location: ElementRef, viewRef: ViewRef) {
131+
var parentView = internalView(location.parentView);
132+
var boundElementIndex = location.boundElementIndex;
133+
this._destroyFreeEmbeddedView(parentView, boundElementIndex, internalView(viewRef));
134+
}
135+
118136
createViewInContainer(viewContainerLocation: ElementRef, atIndex: number,
119137
protoViewRef: ProtoViewRef, context: ElementRef = null,
120138
injector: Injector = null): ViewRef {
@@ -225,11 +243,18 @@ export class AppViewManager {
225243

226244
_destroyFreeHostView(parentView, hostView) {
227245
this._viewDehydrateRecurse(hostView, true);
228-
this._renderer.detachFreeHostView(parentView.render, hostView.render);
246+
this._renderer.detachFreeView(hostView.render);
229247
this._utils.detachFreeHostView(parentView, hostView);
230248
this._destroyPooledView(hostView);
231249
}
232250

251+
_destroyFreeEmbeddedView(parentView, boundElementIndex, view) {
252+
this._viewDehydrateRecurse(view, false);
253+
this._renderer.detachFreeView(view.render);
254+
this._utils.detachFreeEmbeddedView(parentView, boundElementIndex, view);
255+
this._destroyPooledView(view);
256+
}
257+
233258
_viewHydrateRecurse(view: viewModule.AppView) {
234259
this._renderer.hydrateView(view.render);
235260

@@ -260,6 +285,9 @@ export class AppViewManager {
260285
for (var j = vc.views.length - 1; j >= 0; j--) {
261286
this._destroyViewInContainer(view, i, j);
262287
}
288+
for (var j = vc.freeViews.length - 1; j >= 0; j--) {
289+
this._destroyFreeEmbeddedView(view, i, j);
290+
}
263291
}
264292
}
265293

modules/angular2/src/core/compiler/view_manager_utils.ts

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,28 @@ export class AppViewManagerUtils {
110110
ListWrapper.remove(parentView.freeHostViews, hostView);
111111
}
112112

113+
attachAndHydrateFreeEmbeddedView(parentView: viewModule.AppView, boundElementIndex: number,
114+
view: viewModule.AppView, injector: Injector = null) {
115+
parentView.changeDetector.addChild(view.changeDetector);
116+
var viewContainer = this._getOrCreateViewContainer(parentView, boundElementIndex);
117+
ListWrapper.push(viewContainer.freeViews, view);
118+
var elementInjector = parentView.elementInjectors[boundElementIndex];
119+
for (var i = view.rootElementInjectors.length - 1; i >= 0; i--) {
120+
view.rootElementInjectors[i].link(elementInjector);
121+
}
122+
this._hydrateView(view, injector, elementInjector, parentView.context, parentView.locals);
123+
}
124+
125+
detachFreeEmbeddedView(parentView: viewModule.AppView, boundElementIndex: number,
126+
view: viewModule.AppView) {
127+
var viewContainer = parentView.viewContainers[boundElementIndex];
128+
view.changeDetector.remove();
129+
ListWrapper.remove(viewContainer.freeViews, view);
130+
for (var i = 0; i < view.rootElementInjectors.length; ++i) {
131+
view.rootElementInjectors[i].unlink();
132+
}
133+
}
134+
113135
attachViewInContainer(parentView: viewModule.AppView, boundElementIndex: number,
114136
contextView: viewModule.AppView, contextBoundElementIndex: number,
115137
atIndex: number, view: viewModule.AppView) {
@@ -118,11 +140,7 @@ export class AppViewManagerUtils {
118140
contextBoundElementIndex = boundElementIndex;
119141
}
120142
parentView.changeDetector.addChild(view.changeDetector);
121-
var viewContainer = parentView.viewContainers[boundElementIndex];
122-
if (isBlank(viewContainer)) {
123-
viewContainer = new viewModule.AppViewContainer();
124-
parentView.viewContainers[boundElementIndex] = viewContainer;
125-
}
143+
var viewContainer = this._getOrCreateViewContainer(parentView, boundElementIndex);
126144
ListWrapper.insert(viewContainer.views, atIndex, view);
127145
var sibling;
128146
if (atIndex == 0) {
@@ -208,6 +226,15 @@ export class AppViewManagerUtils {
208226
view.changeDetector.hydrate(view.context, view.locals, view);
209227
}
210228

229+
_getOrCreateViewContainer(parentView: viewModule.AppView, boundElementIndex: number) {
230+
var viewContainer = parentView.viewContainers[boundElementIndex];
231+
if (isBlank(viewContainer)) {
232+
viewContainer = new viewModule.AppViewContainer();
233+
parentView.viewContainers[boundElementIndex] = viewContainer;
234+
}
235+
return viewContainer;
236+
}
237+
211238
_setUpEventEmitters(view: viewModule.AppView, elementInjector: eli.ElementInjector,
212239
boundElementIndex: number) {
213240
var emitters = elementInjector.getEventEmitterAccessors();

modules/angular2/src/render/api.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,9 +237,9 @@ export class Renderer {
237237
}
238238

239239
/**
240-
* Detaches a free host view's element from the DOM.
240+
* Detaches a free view's element from the DOM.
241241
*/
242-
detachFreeHostView(parentHostViewRef: RenderViewRef, hostViewRef: RenderViewRef) {}
242+
detachFreeView(view: RenderViewRef) {}
243243

244244
/**
245245
* Creates a regular view out of the given ProtoView

modules/angular2/src/render/dom/dom_renderer.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ export class DomRenderer extends Renderer {
4747
return new DomViewRef(this._createView(hostProtoView, element));
4848
}
4949

50-
detachFreeHostView(parentHostViewRef: RenderViewRef, hostViewRef: RenderViewRef) {
51-
var hostView = resolveInternalDomView(hostViewRef);
52-
this._removeViewNodes(hostView);
50+
detachFreeView(viewRef: RenderViewRef) {
51+
var view = resolveInternalDomView(viewRef);
52+
this._removeViewNodes(view);
5353
}
5454

5555
createView(protoViewRef: RenderProtoViewRef): RenderViewRef {
@@ -83,9 +83,8 @@ export class DomRenderer extends Renderer {
8383
this._moveViewNodesIntoParent(componentView.shadowRoot, componentView);
8484
}
8585

86-
getHostElement(hostViewRef: RenderViewRef) {
87-
var hostView = resolveInternalDomView(hostViewRef);
88-
return hostView.boundElements[0];
86+
getRootNodes(viewRef: RenderViewRef): List</*node*/ any> {
87+
return resolveInternalDomView(viewRef).rootNodes;
8988
}
9089

9190
detachComponentView(hostViewRef: RenderViewRef, boundElementIndex: number,

modules/angular2/test/core/compiler/dynamic_component_loader_spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ class ImperativeViewComponentUsingNgComponent {
257257
renderer.setComponentViewRootNodes(shadowViewRef.render, [div]);
258258
this.done = dynamicComponentLoader.loadIntoNewLocation(ChildComp, self, null)
259259
.then((componentRef) => {
260-
var element = renderer.getHostElement(componentRef.hostView.render);
260+
var element = renderer.getRootNodes(componentRef.hostView.render)[0];
261261
DOM.appendChild(div, element);
262262
return componentRef;
263263
});

modules/angular2/test/core/compiler/integration_spec.ts

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,12 @@ import {
2929
isJsObject,
3030
global,
3131
stringify,
32-
CONST
32+
CONST,
33+
CONST_EXPR
3334
} from 'angular2/src/facade/lang';
3435
import {PromiseWrapper, EventEmitter, ObservableWrapper} from 'angular2/src/facade/async';
3536

36-
import {Injector, bind, Injectable, Binding, FORWARD_REF} from 'angular2/di';
37+
import {Injector, bind, Injectable, Binding, FORWARD_REF, OpaqueToken, Inject} from 'angular2/di';
3738
import {
3839
PipeRegistry,
3940
defaultPipeRegistry,
@@ -63,17 +64,21 @@ import {NgIf} from 'angular2/src/directives/ng_if';
6364
import {NgFor} from 'angular2/src/directives/ng_for';
6465

6566
import {ViewContainerRef} from 'angular2/src/core/compiler/view_container_ref';
66-
import {ProtoViewRef} from 'angular2/src/core/compiler/view_ref';
67+
import {ProtoViewRef, ViewRef} from 'angular2/src/core/compiler/view_ref';
6768
import {Compiler} from 'angular2/src/core/compiler/compiler';
6869
import {ElementRef} from 'angular2/src/core/compiler/element_ref';
6970

7071
import {DomRenderer} from 'angular2/src/render/dom/dom_renderer';
7172
import {AppViewManager} from 'angular2/src/core/compiler/view_manager';
7273

74+
const ANCHOR_ELEMENT = CONST_EXPR(new OpaqueToken('AnchorElement'));
75+
7376
export function main() {
7477
describe('integration tests', function() {
7578
var ctx;
7679

80+
beforeEachBindings(() => [bind(ANCHOR_ELEMENT).toValue(el('<div></div>'))]);
81+
7782
beforeEach(() => { ctx = new MyComp(); });
7883

7984

@@ -1124,6 +1129,28 @@ export function main() {
11241129
});
11251130
}));
11261131

1132+
it('should support free embedded views',
1133+
inject([TestBed, AsyncTestCompleter, ANCHOR_ELEMENT], (tb, async, anchorElement) => {
1134+
tb.overrideView(MyComp, new viewAnn.View({
1135+
template: '<div><div *some-impvp="ctxBoolProp">hello</div></div>',
1136+
directives: [SomeImperativeViewport]
1137+
}));
1138+
tb.createView(MyComp).then((view) => {
1139+
view.detectChanges();
1140+
expect(anchorElement).toHaveText('');
1141+
1142+
view.context.ctxBoolProp = true;
1143+
view.detectChanges();
1144+
expect(anchorElement).toHaveText('hello');
1145+
1146+
view.context.ctxBoolProp = false;
1147+
view.detectChanges();
1148+
expect(view.rootNodes).toHaveText('');
1149+
1150+
async.done();
1151+
});
1152+
}));
1153+
11271154
// Disabled until a solution is found, refs:
11281155
// - https://github.com/angular/angular/issues/776
11291156
// - https://github.com/angular/angular/commit/81f3f32
@@ -1640,3 +1667,30 @@ class ChildConsumingEventBus {
16401667

16411668
constructor(@Unbounded() bus: EventBus) { this.bus = bus; }
16421669
}
1670+
1671+
@Directive({selector: '[some-impvp]', properties: ['someImpvp']})
1672+
@Injectable()
1673+
class SomeImperativeViewport {
1674+
view: ViewRef;
1675+
anchor;
1676+
constructor(public element: ElementRef, public protoView: ProtoViewRef,
1677+
public viewManager: AppViewManager, public renderer: DomRenderer,
1678+
@Inject(ANCHOR_ELEMENT) anchor) {
1679+
this.view = null;
1680+
this.anchor = anchor;
1681+
}
1682+
1683+
set someImpvp(value: boolean) {
1684+
if (isPresent(this.view)) {
1685+
this.viewManager.destroyFreeEmbeddedView(this.element, this.view);
1686+
this.view = null;
1687+
}
1688+
if (value) {
1689+
this.view = this.viewManager.createFreeEmbeddedView(this.element, this.protoView);
1690+
var nodes = this.renderer.getRootNodes(this.view.render);
1691+
for (var i = 0; i < nodes.length; i++) {
1692+
DOM.appendChild(this.anchor, nodes[i]);
1693+
}
1694+
}
1695+
}
1696+
}

modules/angular2/test/core/compiler/view_manager_spec.ts

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -377,8 +377,7 @@ export function main() {
377377

378378
it('should detach the render view', () => {
379379
manager.destroyFreeHostView(elementRef(wrapView(parentHostView), 0), wrapView(hostView));
380-
expect(renderer.spy('detachFreeHostView'))
381-
.toHaveBeenCalledWith(parentView.render, hostRenderViewRef);
380+
expect(renderer.spy('detachFreeView')).toHaveBeenCalledWith(hostRenderViewRef);
382381
});
383382

384383
it('should return the view to the pool', () => {
@@ -402,6 +401,101 @@ export function main() {
402401

403402
});
404403

404+
describe('createFreeEmbeddedView', () => {
405+
406+
// Note: We don't add tests for recursion or viewpool here as we assume that
407+
// this is using the same mechanism as the other methods...
408+
409+
describe('basic functionality', () => {
410+
var parentView, childProtoView;
411+
beforeEach(() => {
412+
parentView = createView(createProtoView([createEmptyElBinder()]));
413+
childProtoView = createProtoView();
414+
});
415+
416+
it('should create the view', () => {
417+
expect(internalView(manager.createFreeEmbeddedView(elementRef(wrapView(parentView), 0),
418+
wrapPv(childProtoView), null)))
419+
.toBe(createdViews[0]);
420+
expect(createdViews[0].proto).toBe(childProtoView);
421+
expect(viewListener.spy('viewCreated')).toHaveBeenCalledWith(createdViews[0]);
422+
});
423+
424+
it('should attachAndHydrate the view', () => {
425+
var injector = new Injector([], null, false);
426+
manager.createFreeEmbeddedView(elementRef(wrapView(parentView), 0),
427+
wrapPv(childProtoView), injector);
428+
expect(utils.spy('attachAndHydrateFreeEmbeddedView'))
429+
.toHaveBeenCalledWith(parentView, 0, createdViews[0], injector);
430+
expect(renderer.spy('hydrateView')).toHaveBeenCalledWith(createdViews[0].render);
431+
});
432+
433+
it('should create and set the render view', () => {
434+
manager.createFreeEmbeddedView(elementRef(wrapView(parentView), 0),
435+
wrapPv(childProtoView), null);
436+
expect(renderer.spy('createView')).toHaveBeenCalledWith(childProtoView.render);
437+
expect(createdViews[0].render).toBe(createdRenderViews[0]);
438+
});
439+
440+
it('should set the event dispatcher', () => {
441+
manager.createFreeEmbeddedView(elementRef(wrapView(parentView), 0),
442+
wrapPv(childProtoView), null);
443+
var cmpView = createdViews[0];
444+
expect(renderer.spy('setEventDispatcher')).toHaveBeenCalledWith(cmpView.render, cmpView);
445+
});
446+
});
447+
448+
});
449+
450+
451+
describe('destroyFreeEmbeddedView', () => {
452+
describe('basic functionality', () => {
453+
var parentView, childProtoView, childView;
454+
beforeEach(() => {
455+
parentView = createView(createProtoView([createEmptyElBinder()]));
456+
childProtoView = createProtoView();
457+
childView = internalView(manager.createFreeEmbeddedView(
458+
elementRef(wrapView(parentView), 0), wrapPv(childProtoView), null));
459+
});
460+
461+
it('should detach', () => {
462+
manager.destroyFreeEmbeddedView(elementRef(wrapView(parentView), 0), wrapView(childView));
463+
expect(utils.spy('detachFreeEmbeddedView'))
464+
.toHaveBeenCalledWith(parentView, 0, childView);
465+
});
466+
467+
it('should dehydrate', () => {
468+
manager.destroyFreeEmbeddedView(elementRef(wrapView(parentView), 0), wrapView(childView));
469+
expect(utils.spy('dehydrateView')).toHaveBeenCalledWith(childView);
470+
expect(renderer.spy('dehydrateView')).toHaveBeenCalledWith(childView.render);
471+
});
472+
473+
it('should detach the render view', () => {
474+
manager.destroyFreeEmbeddedView(elementRef(wrapView(parentView), 0), wrapView(childView));
475+
expect(renderer.spy('detachFreeView')).toHaveBeenCalledWith(childView.render);
476+
});
477+
478+
it('should return the view to the pool', () => {
479+
manager.destroyFreeEmbeddedView(elementRef(wrapView(parentView), 0), wrapView(childView));
480+
expect(viewPool.spy('returnView')).toHaveBeenCalledWith(childView);
481+
expect(renderer.spy('destroyView')).not.toHaveBeenCalled();
482+
});
483+
484+
it('should destroy the view if the pool is full', () => {
485+
viewPool.spy('returnView').andReturn(false);
486+
manager.destroyFreeEmbeddedView(elementRef(wrapView(parentView), 0), wrapView(childView));
487+
expect(renderer.spy('destroyView')).toHaveBeenCalledWith(childView.render);
488+
expect(viewListener.spy('viewDestroyed')).toHaveBeenCalledWith(childView);
489+
});
490+
491+
});
492+
493+
describe('recursively destroyFreeEmbeddedView', () => {
494+
// TODO
495+
});
496+
497+
});
498+
405499
describe('createRootHostView', () => {
406500

407501
var hostProtoView;

0 commit comments

Comments
 (0)