feat(view): add support for instantiation of nested component views.

Include shadowDom creation and integration tests for nested components.

Fix accidentally clobbered modules/core/test/compiler/view_spec.js by
previous commit.
This commit is contained in:
Rado Kirov 2014-11-12 11:40:36 -08:00
parent b07ea6b90e
commit be4cb2db3a
6 changed files with 306 additions and 130 deletions

View File

@ -15,7 +15,7 @@ export class Directive {
bind:Object,
lightDomServices:List,
implementsTypes:List
})
}={})
{
this.selector = selector;
this.lightDomServices = lightDomServices;

View File

@ -129,6 +129,7 @@ export class ProtoElementInjector {
@FIELD('_key9:int')
@FIELD('final parent:ProtoElementInjector')
@FIELD('final index:int')
@FIELD('view:View')
constructor(parent:ProtoElementInjector, index:int, bindings:List, firstBindingIsComponent:boolean = false) {
this.parent = parent;
this.index = index;

View File

@ -30,6 +30,7 @@ export class View {
/// to keep track of the nodes.
@FIELD('final nodes:List<Node>')
@FIELD('final onChangeDispatcher:OnChangeDispatcher')
@FIELD('childViews: List<View>')
constructor(nodes:List<Node>, elementInjectors:List,
rootElementInjectors:List, textNodes:List, bindElements:List,
protoWatchGroup:ProtoWatchGroup, context) {
@ -41,6 +42,9 @@ export class View {
this.bindElements = bindElements;
this.watchGroup = protoWatchGroup.instantiate(this, MapWrapper.create());
this.watchGroup.setContext(context);
// TODO(rado): Since this is only used in tests for now, investigate whether
// we can remove it.
this.childViews = [];
}
onRecordChange(record:Record, target) {
@ -58,6 +62,10 @@ export class View {
DOM.setText(this.textNodes[textNodeIndex], record.currentValue);
}
}
addChild(childView: View) {
ListWrapper.push(this.childViews, childView);
}
}
export class ProtoView {
@ -74,7 +82,7 @@ export class ProtoView {
this.elementsWithBindingCount = 0;
}
instantiate(context, appInjector:Injector):View {
instantiate(context, lightDomAppInjector:Injector, hostElementInjector: ElementInjector):View {
var clone = DOM.clone(this.element);
var elements;
if (clone instanceof TemplateElement) {
@ -89,14 +97,15 @@ export class ProtoView {
/**
* TODO: vsavkin: benchmark
* If this performs poorly, the five loops can be collapsed into one.
* If this performs poorly, the seven loops can be collapsed into one.
*/
var elementInjectors = ProtoView._createElementInjectors(elements, binders);
var elementInjectors = ProtoView._createElementInjectors(elements, binders, hostElementInjector);
var rootElementInjectors = ProtoView._rootElementInjectors(elementInjectors);
var textNodes = ProtoView._textNodes(elements, binders);
var bindElements = ProtoView._bindElements(elements, binders);
var shadowAppInjectors = ProtoView._createShadowAppInjectors(binders, lightDomAppInjector);
var viewNodes;
if (clone instanceof TemplateElement) {
viewNodes = ListWrapper.clone(clone.content.childNodes);
} else {
@ -105,7 +114,10 @@ export class ProtoView {
var view = new View(viewNodes, elementInjectors, rootElementInjectors, textNodes,
bindElements, this.protoWatchGroup, context);
ProtoView._instantiateDirectives(view, elements, elementInjectors, appInjector);
ProtoView._instantiateDirectives(
view, elements, elementInjectors, lightDomAppInjector, shadowAppInjectors);
ProtoView._instantiateChildComponentViews(
elements, binders, elementInjectors, shadowAppInjectors, view);
return view;
}
@ -162,13 +174,13 @@ export class ProtoView {
);
}
static _createElementInjectors(elements, binders) {
static _createElementInjectors(elements, binders, hostElementInjector) {
var injectors = ListWrapper.createFixedSize(binders.length);
for (var i = 0; i < binders.length; ++i) {
var proto = binders[i].protoElementInjector;
if (isPresent(proto)) {
var parentElementInjector = isPresent(proto.parent) ? injectors[proto.parent.index] : null;
injectors[i] = ProtoView._createElementInjector(elements[i], parentElementInjector, proto);
injectors[i] = proto.instantiate(parentElementInjector, hostElementInjector);
} else {
injectors[i] = null;
}
@ -177,17 +189,15 @@ export class ProtoView {
}
static _instantiateDirectives(
view: View, elements:List, injectors:List<ElementInjectors>, appInjector:Injector) {
view: View, elements:List, injectors:List<ElementInjectors>, lightDomAppInjector: Injector,
shadowDomAppInjectors:List<Injectors>) {
for (var i = 0; i < injectors.length; ++i) {
var preBuiltObjs = new PreBuiltObjects(view, new NgElement(elements[i]));
if (injectors[i] != null) injectors[i].instantiateDirectives(appInjector, null, preBuiltObjs);
if (injectors[i] != null) injectors[i].instantiateDirectives(
lightDomAppInjector, shadowDomAppInjectors[i], preBuiltObjs);
}
}
static _createElementInjector(element, parent:ElementInjector, proto:ProtoElementInjector) {
return proto.instantiate(parent, null);
}
static _rootElementInjectors(injectors) {
return ListWrapper.filter(injectors, inj => isPresent(inj) && isBlank(inj.parent));
}
@ -216,6 +226,39 @@ export class ProtoView {
ListWrapper.push(allTextNodes, childNodes[indices[i]]);
}
}
static _instantiateChildComponentViews(elements, binders, injectors,
shadowDomAppInjectors: List<Injector>, view: View) {
for (var i = 0; i < binders.length; ++i) {
var binder = binders[i];
if (isPresent(binder.componentDirective)) {
var injector = injectors[i];
var childView = binder.nestedProtoView.instantiate(
injector.getComponent(), shadowDomAppInjectors[i], injector);
view.addChild(childView);
var shadowRoot = elements[i].createShadowRoot();
// TODO(rado): reuse utility from ViewPort/View.
for (var j = 0; j < childView.nodes.length; ++j) {
DOM.appendChild(shadowRoot, childView.nodes[j]);
}
}
}
}
static _createShadowAppInjectors(binders: List<ElementBinders>, lightDomAppInjector: Injector): List<Injectors> {
var injectors = ListWrapper.createFixedSize(binders.length);
for (var i = 0; i < binders.length; ++i) {
var componentDirective = binders[i].componentDirective;
if (isPresent(componentDirective)) {
var services = componentDirective.annotation.componentServices;
injectors[i] = isPresent(services) ?
lightDomAppInjector.createChild(services) : lightDomAppInjector;
} else {
injectors[i] = null;
}
}
return injectors;
}
}
export class ElementPropertyMemento {

View File

@ -2,6 +2,7 @@ import {describe, xit, it, expect, beforeEach, ddescribe, iit} from 'test_lib/te
import {DOM} from 'facade/dom';
import {Injector} from 'di/di';
import {ChangeDetector} from 'change_detection/change_detector';
import {Parser} from 'change_detection/parser/parser';
import {ClosureMap} from 'change_detection/parser/closure_map';
@ -27,7 +28,7 @@ export function main() {
var view, ctx, cd;
function createView(pv) {
ctx = new MyComp();
view = pv.instantiate(ctx, null);
view = pv.instantiate(ctx, new Injector([]), null);
cd = new ChangeDetector(view.watchGroup);
}
@ -66,6 +67,21 @@ export function main() {
done();
});
});
it('should support nested components.', (done) => {
compiler.compile(MyComp, createElement('<child-cmp></child-cmp>')).then((pv) => {
createView(pv);
cd.detectChanges();
// TODO(rado): this should be removed once watchgroups addChild is implemented.
var childWatchGroup = view.childViews[0].watchGroup;
new ChangeDetector(childWatchGroup).detectChanges();
expect(view.nodes[0].shadowRoot.childNodes[0].nodeValue).toEqual('hello');
done();
});
});
});
});
}
@ -82,7 +98,7 @@ class MyDir {
@Component({
template: new TemplateConfig({
directives: [MyDir]
directives: [MyDir, ChildComp]
})
})
class MyComp {
@ -91,6 +107,26 @@ class MyComp {
}
}
@Component({
selector: 'child-cmp',
componentServices: [MyService],
template: new TemplateConfig({
directives: [MyDir],
inline: '{{ctxProp}}'
})
})
class ChildComp {
constructor(service: MyService) {
this.ctxProp = service.greeting;
}
}
class MyService {
constructor() {
this.greeting = 'hello';
}
}
function createElement(html) {
return DOM.createTemplate(html).content.firstChild;

View File

@ -21,12 +21,14 @@ import {Parser} from 'change_detection/parser/parser';
import {Lexer} from 'change_detection/parser/lexer';
import {ClosureMap} from 'change_detection/parser/closure_map';
import {ChangeDetector} from 'change_detection/change_detector';
import {Injector} from 'di/di';
export function main() {
describe('ElementBinderBuilder', () => {
var evalContext, view, changeDetector;
function createPipeline({textNodeBindings, propertyBindings, directives, protoElementInjector}={}) {
function createPipeline({textNodeBindings, propertyBindings, directives, protoElementInjector
}={}) {
var reflector = new Reflector();
var closureMap = new ClosureMap();
return new CompilePipeline([
@ -69,7 +71,7 @@ export function main() {
function instantiateView(protoView) {
evalContext = new Context();
view = protoView.instantiate(evalContext, null);
view = protoView.instantiate(evalContext, new Injector([]), null);
changeDetector = new ChangeDetector(view.watchGroup);
}
@ -174,7 +176,7 @@ export function main() {
'boundprop3': 'prop3'
});
var directives = [SomeDecoratorDirectiveWithBinding, SomeTemplateDirectiveWithBinding, SomeComponentDirectiveWithBinding];
var protoElementInjector = new ProtoElementInjector(null, 0, directives);
var protoElementInjector = new ProtoElementInjector(null, 0, directives, true);
var pipeline = createPipeline({
propertyBindings: propertyBindings,
directives: directives,
@ -182,6 +184,8 @@ export function main() {
});
var results = pipeline.process(createElement('<div viewroot prop-binding directives></div>'));
var pv = results[0].inheritedProtoView;
results[0].inheritedElementBinder.nestedProtoView = new ProtoView(
createElement('<div></div>'), new ProtoWatchGroup());
instantiateView(pv);
evalContext.prop1 = 'a';

View File

@ -1,14 +1,19 @@
import {describe, xit, it, expect, beforeEach} from 'test_lib/test_lib';
import {describe, xit, it, expect, beforeEach, ddescribe, iit} from 'test_lib/test_lib';
import {ProtoView, ElementPropertyMemento, DirectivePropertyMemento} from 'core/compiler/view';
import {Record, ProtoRecord} from 'change_detection/record';
import {ProtoElementInjector, ElementInjector} from 'core/compiler/element_injector';
import {Reflector} from 'core/compiler/reflector';
import {Component} from 'core/annotations/component';
import {Decorator} from 'core/annotations/decorator';
import {ProtoWatchGroup} from 'change_detection/watch_group';
import {ChangeDetector} from 'change_detection/change_detector';
import {TemplateConfig} from 'core/annotations/template_config';
import {Parser} from 'change_detection/parser/parser';
import {ClosureMap} from 'change_detection/parser/closure_map';
import {Lexer} from 'change_detection/parser/lexer';
import {DOM, Element} from 'facade/dom';
import {FIELD} from 'facade/lang';
import {Injector} from 'di/di';
import {View} from 'core/compiler/view';
export function main() {
describe('view', function() {
@ -21,120 +26,185 @@ export function main() {
describe('ProtoView.instantiate', function() {
describe('collect root nodes', () => {
function createCollectDomNodesTestCases(useTemplateElement:boolean) {
it('should use the ProtoView element if it is no TemplateElement', () => {
var pv = new ProtoView(createElement('<div id="1"></div>'), new ProtoWatchGroup());
var view = pv.instantiate(null, null);
expect(view.nodes.length).toBe(1);
expect(view.nodes[0].getAttribute('id')).toEqual('1');
});
it('should use the ProtoView elements children if it is a TemplateElement', () => {
var pv = new ProtoView(createElement('<template><div id="1"></div></template>'),
new ProtoWatchGroup());
var view = pv.instantiate(null, null);
expect(view.nodes.length).toBe(1);
expect(view.nodes[0].getAttribute('id')).toEqual('1');
});
});
describe('collect elements with property bindings', () => {
it('should collect property bindings on the root element if it has the ng-binding class', () => {
var pv = new ProtoView(createElement('<div [prop]="a" class="ng-binding"></div>'), new ProtoWatchGroup());
pv.bindElement(null);
pv.bindElementProperty('prop', parser.parseBinding('a'));
var view = pv.instantiate(null, null);
expect(view.bindElements.length).toEqual(1);
expect(view.bindElements[0]).toBe(view.nodes[0]);
});
it('should collect property bindings on child elements with ng-binding class', () => {
var pv = new ProtoView(createElement('<div><span></span><span class="ng-binding"></span></div>'),
new ProtoWatchGroup());
pv.bindElement(null);
pv.bindElementProperty('a', parser.parseBinding('b'));
var view = pv.instantiate(null, null);
expect(view.bindElements.length).toEqual(1);
expect(view.bindElements[0]).toBe(view.nodes[0].childNodes[1]);
});
});
describe('collect text nodes with bindings', () => {
it('should collect text nodes under the root element', () => {
var pv = new ProtoView(createElement('<div class="ng-binding">{{}}<span></span>{{}}</div>'), new ProtoWatchGroup());
pv.bindElement(null);
pv.bindTextNode(0, parser.parseBinding('a'));
pv.bindTextNode(2, parser.parseBinding('b'));
var view = pv.instantiate(null, null);
expect(view.textNodes.length).toEqual(2);
expect(view.textNodes[0]).toBe(view.nodes[0].childNodes[0]);
expect(view.textNodes[1]).toBe(view.nodes[0].childNodes[2]);
});
it('should collect text nodes with bindings on child elements with ng-binding class', () => {
var pv = new ProtoView(createElement('<div><span> </span><span class="ng-binding">{{}}</span></div>'),
new ProtoWatchGroup());
pv.bindElement(null);
pv.bindTextNode(0, parser.parseBinding('b'));
var view = pv.instantiate(null, null);
expect(view.textNodes.length).toEqual(1);
expect(view.textNodes[0]).toBe(view.nodes[0].childNodes[1].childNodes[0]);
});
});
describe('react to watch group changes', function() {
var ctx, view, cd;
function createView(protoView) {
ctx = new MyEvaluationContext();
view = protoView.instantiate(ctx, null);
cd = new ChangeDetector(view.watchGroup);
function templateAwareCreateElement(html) {
return createElement(useTemplateElement ? `<template>${html}</template>` : html);
}
it('should consume text node changes', () => {
var pv = new ProtoView(createElement('<div class="ng-binding">{{}}</div>'),
new ProtoWatchGroup());
pv.bindElement(null);
pv.bindTextNode(0, parser.parseBinding('foo'));
createView(pv);
ctx.foo = 'buz';
cd.detectChanges();
expect(view.textNodes[0].nodeValue).toEqual('buz');
it('should collect the root node in the ProtoView element', () => {
var pv = new ProtoView(templateAwareCreateElement('<div id="1"></div>'), new ProtoWatchGroup());
var view = pv.instantiate(null, null, null);
expect(view.nodes.length).toBe(1);
expect(view.nodes[0].getAttribute('id')).toEqual('1');
});
it('should consume element binding changes', () => {
var pv = new ProtoView(createElement('<div class="ng-binding"></div>'),
new ProtoWatchGroup());
pv.bindElement(null);
pv.bindElementProperty('id', parser.parseBinding('foo'));
createView(pv);
describe('collect elements with property bindings', () => {
it('should collect property bindings on the root element if it has the ng-binding class', () => {
var pv = new ProtoView(templateAwareCreateElement('<div [prop]="a" class="ng-binding"></div>'), new ProtoWatchGroup());
pv.bindElement(null);
pv.bindElementProperty('prop', parser.parseBinding('a'));
var view = pv.instantiate(null, null, null);
expect(view.bindElements.length).toEqual(1);
expect(view.bindElements[0]).toBe(view.nodes[0]);
});
it('should collect property bindings on child elements with ng-binding class', () => {
var pv = new ProtoView(templateAwareCreateElement('<div><span></span><span class="ng-binding"></span></div>'),
new ProtoWatchGroup());
pv.bindElement(null);
pv.bindElementProperty('a', parser.parseBinding('b'));
var view = pv.instantiate(null, null, null);
expect(view.bindElements.length).toEqual(1);
expect(view.bindElements[0]).toBe(view.nodes[0].childNodes[1]);
});
ctx.foo = 'buz';
cd.detectChanges();
expect(view.bindElements[0].id).toEqual('buz');
});
it('should consume directive watch expression change.', () => {
var pv = new ProtoView(createElement('<div class="ng-binding"></div>'),
new ProtoWatchGroup());
pv.bindElement(new ProtoElementInjector(null, 0, [Directive]));
pv.bindDirectiveProperty( 0, parser.parseBinding('foo'), 'prop', closureMap.setter('prop'));
createView(pv);
describe('collect text nodes with bindings', () => {
ctx.foo = 'buz';
cd.detectChanges();
expect(view.elementInjectors[0].get(Directive).prop).toEqual('buz');
it('should collect text nodes under the root element', () => {
var pv = new ProtoView(templateAwareCreateElement('<div class="ng-binding">{{}}<span></span>{{}}</div>'), new ProtoWatchGroup());
pv.bindElement(null);
pv.bindTextNode(0, parser.parseBinding('a'));
pv.bindTextNode(2, parser.parseBinding('b'));
var view = pv.instantiate(null, null, null);
expect(view.textNodes.length).toEqual(2);
expect(view.textNodes[0]).toBe(view.nodes[0].childNodes[0]);
expect(view.textNodes[1]).toBe(view.nodes[0].childNodes[2]);
});
it('should collect text nodes with bindings on child elements with ng-binding class', () => {
var pv = new ProtoView(templateAwareCreateElement('<div><span> </span><span class="ng-binding">{{}}</span></div>'),
new ProtoWatchGroup());
pv.bindElement(null);
pv.bindTextNode(0, parser.parseBinding('b'));
var view = pv.instantiate(null, null, null);
expect(view.textNodes.length).toEqual(1);
expect(view.textNodes[0]).toBe(view.nodes[0].childNodes[1].childNodes[0]);
});
});
}
describe('collect dom nodes with a regular element as root', () => {
createCollectDomNodesTestCases(false);
});
describe('collect dom nodes with a template element as root', () => {
createCollectDomNodesTestCases(true);
});
describe('create ElementInjectors', () => {
it('should use the directives of the ProtoElementInjector', () => {
var pv = new ProtoView(createElement('<div class="ng-binding"></div>'), new ProtoWatchGroup());
pv.bindElement(new ProtoElementInjector(null, 1, [SomeDirective]));
var view = pv.instantiate(null, null, null);
expect(view.elementInjectors.length).toBe(1);
expect(view.elementInjectors[0].get(SomeDirective) instanceof SomeDirective).toBe(true);
});
it('should use the correct parent', () => {
var pv = new ProtoView(createElement('<div class="ng-binding"><span class="ng-binding"></span></div>'),
new ProtoWatchGroup());
var protoParent = new ProtoElementInjector(null, 0, [SomeDirective]);
pv.bindElement(protoParent);
pv.bindElement(new ProtoElementInjector(protoParent, 1, [AnotherDirective]));
var view = pv.instantiate(null, null, null);
expect(view.elementInjectors.length).toBe(2);
expect(view.elementInjectors[0].get(SomeDirective) instanceof SomeDirective).toBe(true);
expect(view.elementInjectors[1].parent).toBe(view.elementInjectors[0]);
});
});
describe('collect root element injectors', () => {
it('should collect a single root element injector', () => {
var pv = new ProtoView(createElement('<div class="ng-binding"><span class="ng-binding"></span></div>'),
new ProtoWatchGroup());
var protoParent = new ProtoElementInjector(null, 0, [SomeDirective]);
pv.bindElement(protoParent);
pv.bindElement(new ProtoElementInjector(protoParent, 1, [AnotherDirective]));
var view = pv.instantiate(null, null, null);
expect(view.rootElementInjectors.length).toBe(1);
expect(view.rootElementInjectors[0].get(SomeDirective) instanceof SomeDirective).toBe(true);
});
it('should collect multiple root element injectors', () => {
var pv = new ProtoView(createElement('<div><span class="ng-binding"></span><span class="ng-binding"></span></div>'),
new ProtoWatchGroup());
pv.bindElement(new ProtoElementInjector(null, 1, [SomeDirective]));
pv.bindElement(new ProtoElementInjector(null, 2, [AnotherDirective]));
var view = pv.instantiate(null, null, null);
expect(view.rootElementInjectors.length).toBe(2)
expect(view.rootElementInjectors[0].get(SomeDirective) instanceof SomeDirective).toBe(true);
expect(view.rootElementInjectors[1].get(AnotherDirective) instanceof AnotherDirective).toBe(true);
});
});
describe('recurse over child component views', () => {
var view, ctx;
function createComponentWithSubPV(subProtoView) {
var pv = new ProtoView(createElement('<cmp class="ng-binding"></cmp>'), new ProtoWatchGroup());
var binder = pv.bindElement(new ProtoElementInjector(null, 0, [SomeComponent], true));
binder.componentDirective = new Reflector().annotatedType(SomeComponent);
binder.nestedProtoView = subProtoView;
return pv;
}
function createNestedView(protoView) {
ctx = new MyEvaluationContext();
return protoView.instantiate(ctx, new Injector([]), null);
}
it('should create shadow dom', () => {
var subpv = new ProtoView(createElement('<span>hello shadow dom</span>'), new ProtoWatchGroup());
var pv = createComponentWithSubPV(subpv);
var view = createNestedView(pv);
expect(view.nodes[0].shadowRoot.childNodes[0].childNodes[0].nodeValue).toEqual('hello shadow dom');
});
it('should expose component services to the component', () => {
var subpv = new ProtoView(createElement('<span></span>'), new ProtoWatchGroup());
var pv = createComponentWithSubPV(subpv);
var view = createNestedView(pv);
var comp = view.rootElementInjectors[0].get(SomeComponent);
expect(comp.service).toBeAnInstanceOf(SomeService);
});
it('should expose component services and component instance to directives in the shadow Dom',
() => {
var subpv = new ProtoView(
createElement('<div dec class="ng-binding">hello shadow dom</div>'), new ProtoWatchGroup());
var subBinder = subpv.bindElement(
new ProtoElementInjector(null, 0, [ServiceDependentDecorator]));
var pv = createComponentWithSubPV(subpv);
var view = createNestedView(pv);
var subView = view.childViews[0];
var subInj = subView.rootElementInjectors[0];
var subDecorator = subInj.get(ServiceDependentDecorator);
var comp = view.rootElementInjectors[0].get(SomeComponent);
expect(subDecorator).toBeAnInstanceOf(ServiceDependentDecorator);
expect(subDecorator.service).toBe(comp.service);
expect(subDecorator.component).toBe(comp);
});
});
@ -143,7 +213,7 @@ export function main() {
function createView(protoView) {
ctx = new MyEvaluationContext();
view = protoView.instantiate(ctx, null);
view = protoView.instantiate(ctx, null, null);
cd = new ChangeDetector(view.watchGroup);
}
@ -174,26 +244,48 @@ export function main() {
it('should consume directive watch expression change.', () => {
var pv = new ProtoView(createElement('<div class="ng-binding"></div>'),
new ProtoWatchGroup());
pv.bindElement(new ProtoElementInjector(null, 0, [Directive]));
pv.bindElement(new ProtoElementInjector(null, 0, [SomeDirective]));
pv.bindDirectiveProperty( 0, parser.parseBinding('foo'), 'prop', closureMap.setter('prop'));
createView(pv);
ctx.foo = 'buz';
cd.detectChanges();
expect(view.elementInjectors[0].get(Directive).prop).toEqual('buz');
expect(view.elementInjectors[0].get(SomeDirective).prop).toEqual('buz');
});
});
});
});
}
class Directive {
class SomeDirective {
@FIELD('prop')
constructor() {
this.prop = 'foo';
}
}
class SomeService {}
@Component({
componentServices: [SomeService]
})
class SomeComponent {
constructor(service: SomeService) {
this.service = service;
}
}
@Decorator({
selector: '[dec]'
})
class ServiceDependentDecorator {
constructor(component: SomeComponent, service: SomeService) {
this.component = component;
this.service = service;
}
}
class AnotherDirective {
@FIELD('prop')
constructor() {