feat(viewPort): adds initial implementation of ViewPort.

ViewPort is the mechanism backing @Template directives. Those
directives can use the viewport to dynamically create, attach and
detach views.
This commit is contained in:
Rado Kirov
2014-11-21 15:13:01 -08:00
parent 9a28fa8590
commit c6f14dd833
9 changed files with 403 additions and 30 deletions

View File

@ -6,6 +6,7 @@ import {Parent, Ancestor} from 'core/annotations/visibility';
import {Injector, Inject, bind} from 'di/di';
import {View} from 'core/compiler/view';
import {ProtoRecordRange} from 'change_detection/record_range';
import {ViewPort} from 'core/compiler/viewport';
import {NgElement} from 'core/dom/element';
//TODO: vsavkin: use a spy object
@ -66,7 +67,7 @@ class NeedsView {
}
export function main() {
var defaultPreBuiltObjects = new PreBuiltObjects(null, null);
var defaultPreBuiltObjects = new PreBuiltObjects(null, null, null);
function humanize(tree, names:List) {
var lookupName = (item) =>
@ -177,7 +178,7 @@ export function main() {
it("should instantiate directives that depend on pre built objects", function () {
var view = new DummyView();
var inj = injector([NeedsView], null, null, new PreBuiltObjects(view, null));
var inj = injector([NeedsView], null, null, new PreBuiltObjects(view, null, null));
expect(inj.get(NeedsView).view).toBe(view);
});
@ -282,17 +283,24 @@ export function main() {
describe("pre built objects", function () {
it("should return view", function () {
var view = new DummyView();
var inj = injector([], null, null, new PreBuiltObjects(view, null));
var inj = injector([], null, null, new PreBuiltObjects(view, null, null));
expect(inj.get(View)).toEqual(view);
});
it("should return element", function () {
var element = new NgElement(null);
var inj = injector([], null, null, new PreBuiltObjects(null, element));
var inj = injector([], null, null, new PreBuiltObjects(null, element, null));
expect(inj.get(NgElement)).toEqual(element);
});
it('should return viewPort', function () {
var viewPort = new ViewPort(null, null, null, null);
var inj = injector([], null, null, new PreBuiltObjects(null, null, viewPort));
expect(inj.get(ViewPort)).toEqual(viewPort);
});
});
});
}

View File

@ -2,7 +2,7 @@ import {describe, xit, it, expect, beforeEach, ddescribe, iit} from 'test_lib/te
import {ProtoView, ElementPropertyMemento, DirectivePropertyMemento} from 'core/compiler/view';
import {ProtoElementInjector, ElementInjector} from 'core/compiler/element_injector';
import {DirectiveMetadataReader} from 'core/compiler/directive_metadata_reader';
import {Component, Decorator} from 'core/annotations/annotations';
import {Component, Decorator, Template} from 'core/annotations/annotations';
import {ProtoRecordRange} from 'change_detection/record_range';
import {ChangeDetector} from 'change_detection/change_detector';
import {TemplateConfig} from 'core/annotations/template_config';
@ -12,15 +12,17 @@ import {DOM, Element} from 'facade/dom';
import {FIELD} from 'facade/lang';
import {Injector} from 'di/di';
import {View} from 'core/compiler/view';
import {ViewPort} from 'core/compiler/viewport';
import {reflector} from 'reflection/reflection';
export function main() {
describe('view', function() {
var parser, someComponentDirective;
var parser, someComponentDirective, someTemplateDirective;
beforeEach(() => {
parser = new Parser(new Lexer());
someComponentDirective = new DirectiveMetadataReader().annotatedType(SomeComponent);
someTemplateDirective = new DirectiveMetadataReader().annotatedType(SomeTemplate);
});
@ -213,7 +215,7 @@ export function main() {
var view = createNestedView(pv);
var subView = view.childViews[0];
var subView = view.componentChildViews[0];
var subInj = subView.rootElementInjectors[0];
var subDecorator = subInj.get(ServiceDependentDecorator);
var comp = view.rootElementInjectors[0].get(SomeComponent);
@ -224,6 +226,28 @@ export function main() {
});
});
describe('recurse over child templateViews', () => {
var ctx, view, cd;
function createView(protoView) {
ctx = new MyEvaluationContext();
view = protoView.instantiate(ctx, null, null);
}
it('should create a viewPort for the template directive', () => {
var templateProtoView = new ProtoView(
createElement('<div id="1"></div>'), new ProtoRecordRange());
var pv = new ProtoView(createElement('<someTmpl class="ng-binding"></someTmpl>'), new ProtoRecordRange());
var binder = pv.bindElement(new ProtoElementInjector(null, 0, [SomeTemplate]));
binder.templateDirective = someTemplateDirective;
binder.nestedProtoView = templateProtoView;
createView(pv);
var tmplComp = view.rootElementInjectors[0].get(SomeTemplate);
expect(tmplComp.viewPort).not.toBe(null);
});
});
describe('react to record changes', () => {
var view, cd, ctx;
@ -324,6 +348,17 @@ class ServiceDependentDecorator {
}
}
@Template({
selector: 'someTmpl'
})
class SomeTemplate {
viewPort: ViewPort;
constructor(viewPort: ViewPort) {
this.viewPort = viewPort;
}
}
class AnotherDirective {
prop:string;
constructor() {

View File

@ -0,0 +1,153 @@
import {describe, xit, it, expect, beforeEach, ddescribe, iit} from 'test_lib/test_lib';
import {View, ProtoView} from 'core/compiler/view';
import {ViewPort} from 'core/compiler/viewport';
import {DOM} from 'facade/dom';
import {ListWrapper, MapWrapper} from 'facade/collection';
import {Injector} from 'di/di';
import {ProtoElementInjector, ElementInjector} from 'core/compiler/element_injector';
import {ProtoRecordRange} from 'change_detection/record_range';
import {Parser} from 'change_detection/parser/parser';
import {Lexer} from 'change_detection/parser/lexer';
function createElement(html) {
return DOM.createTemplate(html).content.firstChild;
}
function createView(nodes) {
return new View(nodes, [], [], [], [], new ProtoRecordRange(), null);
}
export function main() {
describe('viewport', () => {
var viewPort, parentView, protoView, dom, customViewWithOneNode,
customViewWithTwoNodes, elementInjector;
beforeEach(() => {
dom = createElement(`<div><stuff></stuff><div insert-after-me></div><stuff></stuff></div>`);
var insertionElement = dom.childNodes[1];
parentView = createView([dom.childNodes[0]]);
protoView = new ProtoView(createElement('<div>hi</div>'), new ProtoRecordRange());
elementInjector = new ElementInjector(null, null, null);
viewPort = new ViewPort(parentView, insertionElement, protoView, elementInjector);
customViewWithOneNode = createView([createElement('<div>single</div>')]);
customViewWithTwoNodes = createView([createElement('<div>one</div>'), createElement('<div>two</div>')]);
});
describe('when detached', () => {
it('should throw if create is called', () => {
expect(() => viewPort.create()).toThrowError();
});
});
describe('when attached', () => {
function textInViewPort() {
var out = '';
// skipping starting filler, insert-me and final filler.
for (var i = 2; i < dom.childNodes.length - 1; i++) {
if (i != 2) out += ' ';
out += DOM.getInnerHTML(dom.childNodes[i]);
}
return out;
}
beforeEach(() => {
viewPort.attach(new Injector([]), null);
var fillerView = createView([createElement('<filler>filler</filler>')]);
viewPort.insert(fillerView);
});
it('should create new views from protoView', () => {
viewPort.create();
expect(textInViewPort()).toEqual('filler hi');
expect(viewPort.length).toBe(2);
});
it('should create new views from protoView at index', () => {
viewPort.create(0);
expect(textInViewPort()).toEqual('hi filler');
expect(viewPort.length).toBe(2);
});
it('should insert new views at the end by default', () => {
viewPort.insert(customViewWithOneNode);
expect(textInViewPort()).toEqual('filler single');
expect(viewPort.get(1)).toBe(customViewWithOneNode);
expect(viewPort.length).toBe(2);
});
it('should insert new views at the given index', () => {
viewPort.insert(customViewWithOneNode, 0);
expect(textInViewPort()).toEqual('single filler');
expect(viewPort.get(0)).toBe(customViewWithOneNode);
expect(viewPort.length).toBe(2);
});
it('should remove the last view by default', () => {
viewPort.insert(customViewWithOneNode);
var removedView = viewPort.remove();
expect(textInViewPort()).toEqual('filler');
expect(removedView).toBe(customViewWithOneNode);
expect(viewPort.length).toBe(1);
});
it('should remove the view at a given index', () => {
viewPort.insert(customViewWithOneNode);
viewPort.insert(customViewWithTwoNodes);
var removedView = viewPort.remove(1);
expect(removedView).toBe(customViewWithOneNode);
expect(textInViewPort()).toEqual('filler one two');
expect(viewPort.get(1)).toBe(customViewWithTwoNodes);
expect(viewPort.length).toBe(2);
});
it('should support adding/removing views with more than one node', () => {
viewPort.insert(customViewWithTwoNodes);
viewPort.insert(customViewWithOneNode);
expect(textInViewPort()).toEqual('filler one two single');
viewPort.remove(1);
expect(textInViewPort()).toEqual('filler single');
});
});
describe('should update injectors and parent views.', () => {
var fancyView;
beforeEach(() => {
var parser = new Parser(new Lexer());
viewPort.attach(new Injector([]), null);
var pv = new ProtoView(createElement('<div class="ng-binding">{{}}</div>'),
new ProtoRecordRange());
pv.bindElement(new ProtoElementInjector(null, 1, [SomeDirective]));
pv.bindTextNode(0, parser.parseBinding('foo').ast);
fancyView = pv.instantiate(new Object(), null, null);
});
it('attaching should update rootElementInjectors and parent RR', () => {
viewPort.insert(fancyView);
ListWrapper.forEach(fancyView.rootElementInjectors, (inj) =>
expect(inj.parent).toBe(elementInjector));
expect(parentView.recordRange.findFirstEnabledRecord()).not.toBe(null);
});
it('detaching should update rootElementInjectors and parent RR', () => {
viewPort.insert(fancyView);
viewPort.remove();
ListWrapper.forEach(fancyView.rootElementInjectors, (inj) =>
expect(inj.parent).toBe(null));
expect(parentView.recordRange.findFirstEnabledRecord()).toBe(null);
});
});
});
}
class SomeDirective {
prop;
constructor() {
this.prop = 'foo';
}
}