diff --git a/modules/core/src/annotations/directive.js b/modules/core/src/annotations/directive.js index 402199c14b..0e86898f93 100644 --- a/modules/core/src/annotations/directive.js +++ b/modules/core/src/annotations/directive.js @@ -15,7 +15,7 @@ export class Directive { bind:Object, lightDomServices:List, implementsTypes:List - }) + }={}) { this.selector = selector; this.lightDomServices = lightDomServices; diff --git a/modules/core/src/compiler/element_injector.js b/modules/core/src/compiler/element_injector.js index 55a645999e..7c5083230a 100644 --- a/modules/core/src/compiler/element_injector.js +++ b/modules/core/src/compiler/element_injector.js @@ -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; diff --git a/modules/core/src/compiler/view.js b/modules/core/src/compiler/view.js index 7d8eaf4ba6..17a35db79e 100644 --- a/modules/core/src/compiler/view.js +++ b/modules/core/src/compiler/view.js @@ -30,6 +30,7 @@ export class View { /// to keep track of the nodes. @FIELD('final nodes:List') @FIELD('final onChangeDispatcher:OnChangeDispatcher') + @FIELD('childViews: List') constructor(nodes:List, 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, appInjector:Injector) { + view: View, elements:List, injectors:List, lightDomAppInjector: Injector, + shadowDomAppInjectors:List) { 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, 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, lightDomAppInjector: Injector): List { + 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 { diff --git a/modules/core/test/compiler/integration_spec.js b/modules/core/test/compiler/integration_spec.js index c43eefa1ce..8877ae6494 100644 --- a/modules/core/test/compiler/integration_spec.js +++ b/modules/core/test/compiler/integration_spec.js @@ -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('')).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; diff --git a/modules/core/test/compiler/pipeline/element_binder_builder_spec.js b/modules/core/test/compiler/pipeline/element_binder_builder_spec.js index e8de27d5db..f0619a2dc3 100644 --- a/modules/core/test/compiler/pipeline/element_binder_builder_spec.js +++ b/modules/core/test/compiler/pipeline/element_binder_builder_spec.js @@ -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('
')); var pv = results[0].inheritedProtoView; + results[0].inheritedElementBinder.nestedProtoView = new ProtoView( + createElement('
'), new ProtoWatchGroup()); instantiateView(pv); evalContext.prop1 = 'a'; diff --git a/modules/core/test/compiler/view_spec.js b/modules/core/test/compiler/view_spec.js index a7f0082eda..61c0ba3d05 100644 --- a/modules/core/test/compiler/view_spec.js +++ b/modules/core/test/compiler/view_spec.js @@ -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('
'), 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(''), - 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('
'), 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('
'), - 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('
{{}}{{}}
'), 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('
{{}}
'), - 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 ? `` : html); } - it('should consume text node changes', () => { - var pv = new ProtoView(createElement('
{{}}
'), - 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('
'), 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('
'), - 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('
'), 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('
'), + 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('
'), - 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('
{{}}{{}}
'), 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('
{{}}
'), + 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('
'), 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('
'), + 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('
'), + 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('
'), + 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(''), 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('hello shadow dom'), 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(''), 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('
hello shadow dom
'), 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('
'), 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() {