From 69b75b7fd8206f47ed513ccc9cf6a6bb4e6b3789 Mon Sep 17 00:00:00 2001 From: vsavkin Date: Thu, 4 Jun 2015 13:45:08 -0700 Subject: [PATCH] feat(view): added support for exportAs, so any directive can be assigned to a variable --- .../src/core/annotations_impl/annotations.ts | 34 +++- .../src/core/compiler/element_binder.ts | 2 + .../src/core/compiler/element_injector.ts | 25 +-- .../src/core/compiler/proto_view_factory.ts | 63 ++++-- modules/angular2/src/core/compiler/view.ts | 7 +- .../src/core/compiler/view_manager_utils.ts | 18 +- modules/angular2/src/render/api.ts | 11 +- modules/angular2/src/render/dom/convert.ts | 2 + .../test/core/compiler/compiler_spec.ts | 4 +- .../test/core/compiler/integration_spec.ts | 181 ++++++++++-------- .../core/compiler/proto_view_factory_spec.ts | 79 +++++++- .../test/core/compiler/view_manager_spec.ts | 4 +- .../core/compiler/view_manager_utils_spec.ts | 4 +- .../angular2/test/render/dom/convert_spec.ts | 4 + 14 files changed, 298 insertions(+), 140 deletions(-) diff --git a/modules/angular2/src/core/annotations_impl/annotations.ts b/modules/angular2/src/core/annotations_impl/annotations.ts index 3501d3f23a..7e32907cc8 100644 --- a/modules/angular2/src/core/annotations_impl/annotations.ts +++ b/modules/angular2/src/core/annotations_impl/annotations.ts @@ -746,9 +746,35 @@ export class Directive extends Injectable { */ hostInjector: List; + /** + * Defines the name that can be used in the template to assign this directive to a variable. + * + * ## Simple Example + * + * @Directive({ + * selector: 'child-dir', + * exportAs: 'child' + * }) + * class ChildDir { + * } + * + * @Component({ + * selector: 'main', + * }) + * @View({ + * template: ``, + * directives: [ChildDir] + * }) + * class MainComponent { + * } + * + * ``` + */ + exportAs: string; + constructor({ selector, properties, events, hostListeners, hostProperties, hostAttributes, - hostActions, lifecycle, hostInjector, compileChildren = true, + hostActions, lifecycle, hostInjector, exportAs, compileChildren = true, }: { selector?: string, properties?: List, @@ -759,6 +785,7 @@ export class Directive extends Injectable { hostActions?: StringMap, lifecycle?: List, hostInjector?: List, + exportAs?: string, compileChildren?: boolean } = {}) { super(); @@ -769,6 +796,7 @@ export class Directive extends Injectable { this.hostProperties = hostProperties; this.hostAttributes = hostAttributes; this.hostActions = hostActions; + this.exportAs = exportAs; this.lifecycle = lifecycle; this.compileChildren = compileChildren; this.hostInjector = hostInjector; @@ -973,7 +1001,7 @@ export class Component extends Directive { viewInjector: List; constructor({selector, properties, events, hostListeners, hostProperties, hostAttributes, - hostActions, appInjector, lifecycle, hostInjector, viewInjector, + hostActions, exportAs, appInjector, lifecycle, hostInjector, viewInjector, changeDetection = DEFAULT, compileChildren = true}: { selector?: string, properties?: List, @@ -982,6 +1010,7 @@ export class Component extends Directive { hostProperties?: StringMap, hostAttributes?: StringMap, hostActions?: StringMap, + exportAs?: string, appInjector?: List, lifecycle?: List, hostInjector?: List, @@ -997,6 +1026,7 @@ export class Component extends Directive { hostProperties: hostProperties, hostAttributes: hostAttributes, hostActions: hostActions, + exportAs: exportAs, hostInjector: hostInjector, lifecycle: lifecycle, compileChildren: compileChildren diff --git a/modules/angular2/src/core/compiler/element_binder.ts b/modules/angular2/src/core/compiler/element_binder.ts index 7038d39554..9e629de41a 100644 --- a/modules/angular2/src/core/compiler/element_binder.ts +++ b/modules/angular2/src/core/compiler/element_binder.ts @@ -8,8 +8,10 @@ import * as viewModule from './view'; export class ElementBinder { nestedProtoView: viewModule.AppProtoView; hostListeners: StringMap>; + constructor(public index: int, public parent: ElementBinder, public distanceToParent: int, public protoElementInjector: eiModule.ProtoElementInjector, + public directiveVariableBindings: Map, public componentDirective: DirectiveBinding) { if (isBlank(index)) { throw new BaseException('null index not allowed.'); diff --git a/modules/angular2/src/core/compiler/element_injector.ts b/modules/angular2/src/core/compiler/element_injector.ts index 791a8d8f18..1ddc95dcc8 100644 --- a/modules/angular2/src/core/compiler/element_injector.ts +++ b/modules/angular2/src/core/compiler/element_injector.ts @@ -317,7 +317,9 @@ export class DirectiveBinding extends ResolvedBinding { callOnAllChangesDone: hasLifecycleHook(onAllChangesDone, rb.key.token, ann), changeDetection: ann instanceof - Component ? ann.changeDetection : null + Component ? ann.changeDetection : null, + + exportAs: ann.exportAs }); return new DirectiveBinding(rb.key, rb.factory, deps, rb.providedAsPromise, resolvedAppInjectables, resolvedHostInjectables, @@ -422,15 +424,6 @@ export class ProtoElementInjector { eventEmitterAccessors: List>; hostActionAccessors: List>; - /** Whether the element is exported as $implicit. */ - exportElement: boolean; - - /** Whether the component instance is exported as $implicit. */ - exportComponent: boolean; - - /** The variable name that will be set to $implicit for the element. */ - exportImplicitName: string; - _strategy: _ProtoElementInjectorStrategy; static create(parent: ProtoElementInjector, index: number, bindings: List, @@ -483,9 +476,6 @@ export class ProtoElementInjector { constructor(public parent: ProtoElementInjector, public index: int, bd: List, public distanceToParent: number, public _firstBindingIsComponent: boolean) { - this.exportComponent = false; - this.exportElement = false; - var length = bd.length; this.eventEmitterAccessors = ListWrapper.createFixedSize(length); this.hostActionAccessors = ListWrapper.createFixedSize(length); @@ -1164,15 +1154,6 @@ export class ElementInjector extends TreeNode { hasInstances(): boolean { return this._constructionCounter > 0; } - /** Gets whether this element is exporting a component instance as $implicit. */ - isExportingComponent(): boolean { return this._proto.exportComponent; } - - /** Gets whether this element is exporting its element as $implicit. */ - isExportingElement(): boolean { return this._proto.exportElement; } - - /** Get the name to which this element's $implicit is to be assigned. */ - getExportImplicitName(): string { return this._proto.exportImplicitName; } - getLightDomAppInjector(): Injector { return this._lightDomAppInjector; } getShadowDomAppInjector(): Injector { return this._shadowDomAppInjector; } diff --git a/modules/angular2/src/core/compiler/proto_view_factory.ts b/modules/angular2/src/core/compiler/proto_view_factory.ts index 5a35a7c71f..b55cb69a76 100644 --- a/modules/angular2/src/core/compiler/proto_view_factory.ts +++ b/modules/angular2/src/core/compiler/proto_view_factory.ts @@ -1,7 +1,7 @@ import {Injectable} from 'angular2/di'; import {List, ListWrapper, MapWrapper} from 'angular2/src/facade/collection'; -import {isPresent, isBlank} from 'angular2/src/facade/lang'; +import {isPresent, isBlank, BaseException} from 'angular2/src/facade/lang'; import {reflector} from 'angular2/src/reflection/reflection'; import { @@ -309,7 +309,7 @@ function _createElementBinders(protoView, elementBinders, allDirectiveBindings) componentDirectiveBinding, directiveBindings); _createElementBinder(protoView, i, renderElementBinder, protoElementInjector, - componentDirectiveBinding); + componentDirectiveBinding, directiveBindings); } } @@ -343,28 +343,20 @@ function _createProtoElementInjector(binderIndex, parentPeiWithDistance, renderE parentPeiWithDistance.protoElementInjector, binderIndex, directiveBindings, isPresent(componentDirectiveBinding), parentPeiWithDistance.distance); protoElementInjector.attributes = renderElementBinder.readAttributes; - if (hasVariables) { - protoElementInjector.exportComponent = isPresent(componentDirectiveBinding); - protoElementInjector.exportElement = isBlank(componentDirectiveBinding); - - // experiment - var exportImplicitName = MapWrapper.get(renderElementBinder.variableBindings, '\$implicit'); - if (isPresent(exportImplicitName)) { - protoElementInjector.exportImplicitName = exportImplicitName; - } - } } return protoElementInjector; } function _createElementBinder(protoView, boundElementIndex, renderElementBinder, - protoElementInjector, componentDirectiveBinding): ElementBinder { + protoElementInjector, componentDirectiveBinding, directiveBindings): ElementBinder { var parent = null; if (renderElementBinder.parentIndex !== -1) { parent = protoView.elementBinders[renderElementBinder.parentIndex]; } + + var directiveVariableBindings = createDirectiveVariableBindings(renderElementBinder, directiveBindings); var elBinder = protoView.bindElement(parent, renderElementBinder.distanceToParent, - protoElementInjector, componentDirectiveBinding); + protoElementInjector, directiveVariableBindings, componentDirectiveBinding); protoView.bindEvent(renderElementBinder.eventBindings, boundElementIndex, -1); // variables // The view's locals needs to have a full set of variable names at construction time @@ -377,6 +369,49 @@ function _createElementBinder(protoView, boundElementIndex, renderElementBinder, return elBinder; } +export function createDirectiveVariableBindings(renderElementBinder:renderApi.ElementBinder, + directiveBindings:List): Map { + var directiveVariableBindings = MapWrapper.create(); + MapWrapper.forEach(renderElementBinder.variableBindings, (templateName, exportAs) => { + var dirIndex = _findDirectiveIndexByExportAs(renderElementBinder, directiveBindings, exportAs); + MapWrapper.set(directiveVariableBindings, templateName, dirIndex); + }); + return directiveVariableBindings; +} + +function _findDirectiveIndexByExportAs(renderElementBinder, directiveBindings, exportAs) { + var matchedDirectiveIndex = null; + var matchedDirective; + + for (var i = 0; i < directiveBindings.length; ++i) { + var directive = directiveBindings[i]; + + if (_directiveExportAs(directive) == exportAs) { + if (isPresent(matchedDirective)) { + throw new BaseException(`More than one directive have exportAs = '${exportAs}'. Directives: [${matchedDirective.displayName}, ${directive.displayName}]`); + } + + matchedDirectiveIndex = i; + matchedDirective = directive; + } + } + + if (isBlank(matchedDirective) && exportAs !== "$implicit") { + throw new BaseException(`Cannot find directive with exportAs = '${exportAs}'`); + } + + return matchedDirectiveIndex; +} + +function _directiveExportAs(directive):string { + var directiveExportAs = directive.metadata.exportAs; + if (isBlank(directiveExportAs) && directive.metadata.type === renderApi.DirectiveMetadata.COMPONENT_TYPE) { + return "$implicit"; + } else { + return directiveExportAs; + } +} + function _bindDirectiveEvents(protoView, elementBinders: List) { for (var boundElementIndex = 0; boundElementIndex < elementBinders.length; ++boundElementIndex) { var dirs = elementBinders[boundElementIndex].directives; diff --git a/modules/angular2/src/core/compiler/view.ts b/modules/angular2/src/core/compiler/view.ts index fa55fc336b..2b383f40ed 100644 --- a/modules/angular2/src/core/compiler/view.ts +++ b/modules/angular2/src/core/compiler/view.ts @@ -183,9 +183,12 @@ export class AppProtoView { bindElement(parent: ElementBinder, distanceToParent: int, protoElementInjector: ProtoElementInjector, + directiveVariableBindings: Map, componentDirective: DirectiveBinding = null): ElementBinder { - var elBinder = new ElementBinder(this.elementBinders.length, parent, distanceToParent, - protoElementInjector, componentDirective); + var elBinder = + new ElementBinder(this.elementBinders.length, parent, distanceToParent, + protoElementInjector, directiveVariableBindings, componentDirective); + ListWrapper.push(this.elementBinders, elBinder); return elBinder; } diff --git a/modules/angular2/src/core/compiler/view_manager_utils.ts b/modules/angular2/src/core/compiler/view_manager_utils.ts index a070263073..46cf5720f0 100644 --- a/modules/angular2/src/core/compiler/view_manager_utils.ts +++ b/modules/angular2/src/core/compiler/view_manager_utils.ts @@ -206,20 +206,22 @@ export class AppViewManagerUtils { var binders = view.proto.elementBinders; for (var i = 0; i < binders.length; ++i) { + var binder = binders[i]; var elementInjector = view.elementInjectors[i]; + if (isPresent(elementInjector)) { elementInjector.hydrate(appInjector, hostElementInjector, view.preBuiltObjects[i]); this._setUpEventEmitters(view, elementInjector, i); this._setUpHostActions(view, elementInjector, i); - // The exporting of $implicit is a special case. Since multiple elements will all export - // the different values as $implicit, directly assign $implicit bindings to the variable - // name. - var exportImplicitName = elementInjector.getExportImplicitName(); - if (elementInjector.isExportingComponent()) { - view.locals.set(exportImplicitName, elementInjector.getComponent()); - } else if (elementInjector.isExportingElement()) { - view.locals.set(exportImplicitName, elementInjector.getElementRef().domElement); + if (isPresent(binder.directiveVariableBindings)) { + MapWrapper.forEach(binder.directiveVariableBindings, (directiveIndex, name) => { + if (isBlank(directiveIndex)) { + view.locals.set(name, elementInjector.getElementRef().domElement); + } else { + view.locals.set(name, elementInjector.getDirectiveAtIndex(directiveIndex)); + } + }); } } } diff --git a/modules/angular2/src/render/api.ts b/modules/angular2/src/render/api.ts index 3abf9ac063..4b8e0f6b1d 100644 --- a/modules/angular2/src/render/api.ts +++ b/modules/angular2/src/render/api.ts @@ -36,7 +36,7 @@ export class ElementBinder { directives: List; nestedProtoView: ProtoViewDto; propertyBindings: Map; - variableBindings: Map; + variableBindings: Map; // Note: this contains a preprocessed AST // that replaced the values that should be extracted from the element // with a local name @@ -52,7 +52,7 @@ export class ElementBinder { directives?: List, nestedProtoView?: ProtoViewDto, propertyBindings?: Map, - variableBindings?: Map, + variableBindings?: Map, eventBindings?: List, textBindings?: List, readAttributes?: Map @@ -142,9 +142,10 @@ export class DirectiveMetadata { callOnInit: boolean; callOnAllChangesDone: boolean; changeDetection: string; + exportAs: string; constructor({id, selector, compileChildren, events, hostListeners, hostProperties, hostAttributes, hostActions, properties, readAttributes, type, callOnDestroy, callOnChange, - callOnCheck, callOnInit, callOnAllChangesDone, changeDetection}: { + callOnCheck, callOnInit, callOnAllChangesDone, changeDetection, exportAs}: { id?: string, selector?: string, compileChildren?: boolean, @@ -161,7 +162,8 @@ export class DirectiveMetadata { callOnCheck?: boolean, callOnInit?: boolean, callOnAllChangesDone?: boolean, - changeDetection?: string + changeDetection?: string, + exportAs?: string }) { this.id = id; this.selector = selector; @@ -180,6 +182,7 @@ export class DirectiveMetadata { this.callOnInit = callOnInit; this.callOnAllChangesDone = callOnAllChangesDone; this.changeDetection = changeDetection; + this.exportAs = exportAs; } } diff --git a/modules/angular2/src/render/dom/convert.ts b/modules/angular2/src/render/dom/convert.ts index 960274466a..87471dae23 100644 --- a/modules/angular2/src/render/dom/convert.ts +++ b/modules/angular2/src/render/dom/convert.ts @@ -18,6 +18,7 @@ export function directiveMetadataToMap(meta: DirectiveMetadata): Map): DirectiveMetada properties:>_cloneIfPresent(MapWrapper.get(map, 'properties')), readAttributes:>_cloneIfPresent(MapWrapper.get(map, 'readAttributes')), type:MapWrapper.get(map, 'type'), + exportAs:MapWrapper.get(map, 'exportAs'), callOnDestroy:MapWrapper.get(map, 'callOnDestroy'), callOnCheck:MapWrapper.get(map, 'callOnCheck'), callOnChange:MapWrapper.get(map, 'callOnChange'), diff --git a/modules/angular2/test/core/compiler/compiler_spec.ts b/modules/angular2/test/core/compiler/compiler_spec.ts index f68dacccc7..87e0c6e93d 100644 --- a/modules/angular2/test/core/compiler/compiler_spec.ts +++ b/modules/angular2/test/core/compiler/compiler_spec.ts @@ -420,11 +420,11 @@ function createProtoView(elementBinders = null) { function createComponentElementBinder(directiveResolver, type) { var binding = createDirectiveBinding(directiveResolver, type); - return new ElementBinder(0, null, 0, null, binding); + return new ElementBinder(0, null, 0, null, null, binding); } function createViewportElementBinder(nestedProtoView) { - var elBinder = new ElementBinder(0, null, 0, null, null); + var elBinder = new ElementBinder(0, null, 0, null, null, null); elBinder.nestedProtoView = nestedProtoView; return elBinder; } diff --git a/modules/angular2/test/core/compiler/integration_spec.ts b/modules/angular2/test/core/compiler/integration_spec.ts index b1815230a5..4e6a78100d 100644 --- a/modules/angular2/test/core/compiler/integration_spec.ts +++ b/modules/angular2/test/core/compiler/integration_spec.ts @@ -422,110 +422,127 @@ export function main() { }); })); - it('should assign the component instance to a var-', - inject([TestBed, AsyncTestCompleter], (tb, async) => { - tb.overrideView(MyComp, new viewAnn.View({ - template: '

', - directives: [ChildComp] + describe("variable bindings", () => { + it('should assign a component to a var-', + inject([TestBed, AsyncTestCompleter], (tb, async) => { + tb.overrideView(MyComp, new viewAnn.View({ + template: '

', + directives: [ChildComp] + })); + + tb.createView(MyComp, {context: ctx}) + .then((view) => { + expect(view.rawView.locals).not.toBe(null); + expect(view.rawView.locals.get('alice')).toBeAnInstanceOf(ChildComp); + + async.done(); + }) })); - tb.createView(MyComp, {context: ctx}) - .then((view) => { - expect(view.rawView.locals).not.toBe(null); - expect(view.rawView.locals.get('alice')).toBeAnInstanceOf(ChildComp); + it('should assign a directive to a var-', + inject([TestBed, AsyncTestCompleter], (tb, async) => { + tb.overrideView(MyComp, new viewAnn.View({ + template: '

', + directives: [ExportDir] + })); - async.done(); - }) - })); + tb.createView(MyComp, {context: ctx}) + .then((view) => { + expect(view.rawView.locals).not.toBe(null); + expect(view.rawView.locals.get('localdir')).toBeAnInstanceOf(ExportDir); - it('should make the assigned component accessible in property bindings', - inject([TestBed, AsyncTestCompleter], (tb, async) => { - tb.overrideView(MyComp, new viewAnn.View({ - template: '

{{alice.ctxProp}}

', - directives: [ChildComp] + async.done(); + }); })); - tb.createView(MyComp, {context: ctx}) - .then((view) => { - view.detectChanges(); + it('should make the assigned component accessible in property bindings', + inject([TestBed, AsyncTestCompleter], (tb, async) => { + tb.overrideView(MyComp, new viewAnn.View({ + template: '

{{alice.ctxProp}}

', + directives: [ChildComp] + })); - expect(view.rootNodes).toHaveText('hellohello'); // this first one is the - // component, the second one is - // the text binding - async.done(); - }) - })); + tb.createView(MyComp, {context: ctx}) + .then((view) => { + view.detectChanges(); - it('should assign two component instances each with a var-', - inject([TestBed, AsyncTestCompleter], (tb, async) => { - tb.overrideView(MyComp, new viewAnn.View({ - template: '

', - directives: [ChildComp] + expect(view.rootNodes).toHaveText('hellohello'); // this first one is the + // component, the second one is + // the text binding + async.done(); + }) })); - tb.createView(MyComp, {context: ctx}) - .then((view) => { + it('should assign two component instances each with a var-', + inject([TestBed, AsyncTestCompleter], (tb, async) => { + tb.overrideView(MyComp, new viewAnn.View({ + template: '

', + directives: [ChildComp] + })); - expect(view.rawView.locals).not.toBe(null); - expect(view.rawView.locals.get('alice')).toBeAnInstanceOf(ChildComp); - expect(view.rawView.locals.get('bob')).toBeAnInstanceOf(ChildComp); - expect(view.rawView.locals.get('alice')).not.toBe(view.rawView.locals.get('bob')); + tb.createView(MyComp, {context: ctx}) + .then((view) => { - async.done(); - }) - })); + expect(view.rawView.locals).not.toBe(null); + expect(view.rawView.locals.get('alice')).toBeAnInstanceOf(ChildComp); + expect(view.rawView.locals.get('bob')).toBeAnInstanceOf(ChildComp); + expect(view.rawView.locals.get('alice')) + .not.toBe(view.rawView.locals.get('bob')); - it('should assign the component instance to a var- with shorthand syntax', - inject([TestBed, AsyncTestCompleter], (tb, async) => { - tb.overrideView( - MyComp, new viewAnn.View( - {template: '', directives: [ChildComp]})); + async.done(); + }) + })); - tb.createView(MyComp, {context: ctx}) - .then((view) => { + it('should assign the component instance to a var- with shorthand syntax', + inject([TestBed, AsyncTestCompleter], (tb, async) => { + tb.overrideView( + MyComp, + new viewAnn.View( + {template: '', directives: [ChildComp]})); - expect(view.rawView.locals).not.toBe(null); - expect(view.rawView.locals.get('alice')).toBeAnInstanceOf(ChildComp); + tb.createView(MyComp, {context: ctx}) + .then((view) => { - async.done(); - }) - })); + expect(view.rawView.locals).not.toBe(null); + expect(view.rawView.locals.get('alice')).toBeAnInstanceOf(ChildComp); - it('should assign the element instance to a user-defined variable', - inject([TestBed, AsyncTestCompleter], (tb, async) => { - tb.overrideView( - MyComp, new viewAnn.View({template: '

Hello

'})); + async.done(); + }) + })); - tb.createView(MyComp, {context: ctx}) - .then((view) => { - expect(view.rawView.locals).not.toBe(null); + it('should assign the element instance to a user-defined variable', + inject([TestBed, AsyncTestCompleter], (tb, async) => { + tb.overrideView( + MyComp, new viewAnn.View({template: '

Hello

'})); - var value = view.rawView.locals.get('alice'); - expect(value).not.toBe(null); - expect(value.tagName.toLowerCase()).toEqual('div'); + tb.createView(MyComp, {context: ctx}) + .then((view) => { + expect(view.rawView.locals).not.toBe(null); - async.done(); - }) - })); + var value = view.rawView.locals.get('alice'); + expect(value).not.toBe(null); + expect(value.tagName.toLowerCase()).toEqual('div'); + async.done(); + }) + })); - it('should assign the element instance to a user-defined variable with camelCase using dash-case', - inject([TestBed, AsyncTestCompleter], (tb, async) => { - tb.overrideView( - MyComp, - new viewAnn.View({template: '

Hello

'})); + it('should change dash-case to camel-case', + inject([TestBed, AsyncTestCompleter], (tb, async) => { + tb.overrideView(MyComp, new viewAnn.View({ + template: '

', + directives: [ChildComp] + })); - tb.createView(MyComp, {context: ctx}) - .then((view) => { - expect(view.rawView.locals).not.toBe(null); + tb.createView(MyComp, {context: ctx}) + .then((view) => { + expect(view.rawView.locals).not.toBe(null); + expect(view.rawView.locals.get('superAlice')).toBeAnInstanceOf(ChildComp); - var value = view.rawView.locals.get('superAlice'); - expect(value).not.toBe(null); - expect(value.tagName.toLowerCase()).toEqual('div'); - - async.done(); - }) - })); + async.done(); + }); + })); + }); describe("ON_PUSH components", () => { it("should use ChangeDetectorRef to manually request a check", @@ -1696,3 +1713,7 @@ class SomeImperativeViewport { } } } + +@Directive({selector: '[export-dir]', exportAs: 'dir'}) +class ExportDir { +} \ No newline at end of file diff --git a/modules/angular2/test/core/compiler/proto_view_factory_spec.ts b/modules/angular2/test/core/compiler/proto_view_factory_spec.ts index 71444f4dfc..31c55cdc70 100644 --- a/modules/angular2/test/core/compiler/proto_view_factory_spec.ts +++ b/modules/angular2/test/core/compiler/proto_view_factory_spec.ts @@ -20,9 +20,11 @@ import {MapWrapper} from 'angular2/src/facade/collection'; import {ChangeDetection, ChangeDetectorDefinition} from 'angular2/change_detection'; import { ProtoViewFactory, - getChangeDetectorDefinitions + getChangeDetectorDefinitions, + createDirectiveVariableBindings } from 'angular2/src/core/compiler/proto_view_factory'; import {Component, Directive} from 'angular2/annotations'; +import {Key} from 'angular2/di'; import {DirectiveResolver} from 'angular2/src/core/compiler/directive_resolver'; import {DirectiveBinding} from 'angular2/src/core/compiler/element_injector'; import * as renderApi from 'angular2/src/render/api'; @@ -65,12 +67,85 @@ export function main() { expect(pvs.length).toBe(1); expect(pvs[0].render).toBe(renderPv.render); }); - }); + describe("createDirectiveVariableBindings", () => { + it("should calculate directive variable bindings", () => { + var dvbs = createDirectiveVariableBindings( + new renderApi.ElementBinder( + {variableBindings: MapWrapper.createFromStringMap({"exportName": "templateName"})}), + [ + directiveBinding( + {metadata: new renderApi.DirectiveMetadata({exportAs: 'exportName'})}), + directiveBinding( + {metadata: new renderApi.DirectiveMetadata({exportAs: 'otherName'})}) + ]); + + expect(dvbs).toEqual(MapWrapper.createFromStringMap({"templateName": 0})); + }); + + it("should set exportAs to $implicit for component with exportAs = null", () => { + var dvbs = createDirectiveVariableBindings( + new renderApi.ElementBinder( + {variableBindings: MapWrapper.createFromStringMap({"$implicit": "templateName"})}), + [ + directiveBinding({ + metadata: new renderApi.DirectiveMetadata( + {exportAs: null, type: renderApi.DirectiveMetadata.COMPONENT_TYPE}) + }) + ]); + + expect(dvbs).toEqual(MapWrapper.createFromStringMap({"templateName": 0})); + }); + + it("should throw we no directive exported with this name", () => { + expect(() => { + createDirectiveVariableBindings( + new renderApi.ElementBinder({ + variableBindings: + MapWrapper.createFromStringMap({"someInvalidName": "templateName"}) + }), + [ + directiveBinding( + {metadata: new renderApi.DirectiveMetadata({exportAs: 'exportName'})}) + ]); + }).toThrowError(new RegExp("Cannot find directive with exportAs = 'someInvalidName'")); + }); + + it("should throw when binding to a name exported by two directives", () => { + expect(() => { + createDirectiveVariableBindings( + new renderApi.ElementBinder({ + variableBindings: MapWrapper.createFromStringMap({"exportName": "templateName"}) + }), + [ + directiveBinding( + {metadata: new renderApi.DirectiveMetadata({exportAs: 'exportName'})}), + directiveBinding( + {metadata: new renderApi.DirectiveMetadata({exportAs: 'exportName'})}) + ]); + }).toThrowError(new RegExp("More than one directive have exportAs = 'exportName'")); + }); + + it("should not throw when not binding to a name exported by two directives", () => { + expect(() => { + createDirectiveVariableBindings( + new renderApi.ElementBinder({variableBindings: MapWrapper.create()}), [ + directiveBinding( + {metadata: new renderApi.DirectiveMetadata({exportAs: 'exportName'})}), + directiveBinding( + {metadata: new renderApi.DirectiveMetadata({exportAs: 'exportName'})}) + ]); + }).not.toThrow(); + }); + }); }); } +function directiveBinding({metadata}: {metadata?: any} = {}) { + return new DirectiveBinding(Key.get("dummy"), null, [], false, [], [], [], metadata); +} + function createRenderProtoView(elementBinders = null, type: number = null) { if (isBlank(type)) { type = renderApi.ProtoViewDto.COMPONENT_VIEW_TYPE; diff --git a/modules/angular2/test/core/compiler/view_manager_spec.ts b/modules/angular2/test/core/compiler/view_manager_spec.ts index b33a859eb7..d72b5f3a2a 100644 --- a/modules/angular2/test/core/compiler/view_manager_spec.ts +++ b/modules/angular2/test/core/compiler/view_manager_spec.ts @@ -58,11 +58,11 @@ export function main() { return DirectiveBinding.createFromType(type, annotation); } - function createEmptyElBinder() { return new ElementBinder(0, null, 0, null, null); } + function createEmptyElBinder() { return new ElementBinder(0, null, 0, null, null, null); } function createComponentElBinder(nestedProtoView = null) { var binding = createDirectiveBinding(SomeComponent); - var binder = new ElementBinder(0, null, 0, null, binding); + var binder = new ElementBinder(0, null, 0, null, null, binding); binder.nestedProtoView = nestedProtoView; return binder; } diff --git a/modules/angular2/test/core/compiler/view_manager_utils_spec.ts b/modules/angular2/test/core/compiler/view_manager_utils_spec.ts index 318083e498..71de3793f8 100644 --- a/modules/angular2/test/core/compiler/view_manager_utils_spec.ts +++ b/modules/angular2/test/core/compiler/view_manager_utils_spec.ts @@ -48,11 +48,11 @@ export function main() { return DirectiveBinding.createFromType(type, annotation); } - function createEmptyElBinder() { return new ElementBinder(0, null, 0, null, null); } + function createEmptyElBinder() { return new ElementBinder(0, null, 0, null, null, null); } function createComponentElBinder(nestedProtoView = null) { var binding = createDirectiveBinding(SomeComponent); - var binder = new ElementBinder(0, null, 0, null, binding); + var binder = new ElementBinder(0, null, 0, null, null, binding); binder.nestedProtoView = nestedProtoView; return binder; } diff --git a/modules/angular2/test/render/dom/convert_spec.ts b/modules/angular2/test/render/dom/convert_spec.ts index 71a479c19d..8b924c35a1 100644 --- a/modules/angular2/test/render/dom/convert_spec.ts +++ b/modules/angular2/test/render/dom/convert_spec.ts @@ -16,6 +16,7 @@ export function main() { readAttributes: ['read1', 'read2'], selector: 'some-comp', type: DirectiveMetadata.COMPONENT_TYPE, + exportAs: 'aaa', callOnDestroy: true, callOnChange: true, callOnCheck: true, @@ -40,6 +41,7 @@ export function main() { expect(MapWrapper.get(map, 'callOnChange')).toEqual(true); expect(MapWrapper.get(map, 'callOnInit')).toEqual(true); expect(MapWrapper.get(map, 'callOnAllChangesDone')).toEqual(true); + expect(MapWrapper.get(map, 'exportAs')).toEqual('aaa'); }); it('mapToDirectiveMetadata', () => { @@ -53,6 +55,7 @@ export function main() { ['readAttributes', ['readTest1', 'readTest2']], ['selector', 'testSelector'], ['type', DirectiveMetadata.DIRECTIVE_TYPE], + ['exportAs', 'aaa'], ['callOnDestroy', true], ['callOnCheck', true], ['callOnInit', true], @@ -71,6 +74,7 @@ export function main() { expect(meta.readAttributes).toEqual(['readTest1', 'readTest2']); expect(meta.selector).toEqual('testSelector'); expect(meta.type).toEqual(DirectiveMetadata.DIRECTIVE_TYPE); + expect(meta.exportAs).toEqual('aaa'); expect(meta.callOnDestroy).toEqual(true); expect(meta.callOnCheck).toEqual(true); expect(meta.callOnInit).toEqual(true);