feat(view): added support for exportAs, so any directive can be assigned to a variable

This commit is contained in:
vsavkin 2015-06-04 13:45:08 -07:00
parent 4eb8c9b2dd
commit 69b75b7fd8
14 changed files with 298 additions and 140 deletions

View File

@ -746,9 +746,35 @@ export class Directive extends Injectable {
*/ */
hostInjector: List<any>; hostInjector: List<any>;
/**
* 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: `<child-dir #c="child"></child-dir>`,
* directives: [ChildDir]
* })
* class MainComponent {
* }
*
* ```
*/
exportAs: string;
constructor({ constructor({
selector, properties, events, hostListeners, hostProperties, hostAttributes, selector, properties, events, hostListeners, hostProperties, hostAttributes,
hostActions, lifecycle, hostInjector, compileChildren = true, hostActions, lifecycle, hostInjector, exportAs, compileChildren = true,
}: { }: {
selector?: string, selector?: string,
properties?: List<string>, properties?: List<string>,
@ -759,6 +785,7 @@ export class Directive extends Injectable {
hostActions?: StringMap<string, string>, hostActions?: StringMap<string, string>,
lifecycle?: List<LifecycleEvent>, lifecycle?: List<LifecycleEvent>,
hostInjector?: List<any>, hostInjector?: List<any>,
exportAs?: string,
compileChildren?: boolean compileChildren?: boolean
} = {}) { } = {}) {
super(); super();
@ -769,6 +796,7 @@ export class Directive extends Injectable {
this.hostProperties = hostProperties; this.hostProperties = hostProperties;
this.hostAttributes = hostAttributes; this.hostAttributes = hostAttributes;
this.hostActions = hostActions; this.hostActions = hostActions;
this.exportAs = exportAs;
this.lifecycle = lifecycle; this.lifecycle = lifecycle;
this.compileChildren = compileChildren; this.compileChildren = compileChildren;
this.hostInjector = hostInjector; this.hostInjector = hostInjector;
@ -973,7 +1001,7 @@ export class Component extends Directive {
viewInjector: List<any>; viewInjector: List<any>;
constructor({selector, properties, events, hostListeners, hostProperties, hostAttributes, constructor({selector, properties, events, hostListeners, hostProperties, hostAttributes,
hostActions, appInjector, lifecycle, hostInjector, viewInjector, hostActions, exportAs, appInjector, lifecycle, hostInjector, viewInjector,
changeDetection = DEFAULT, compileChildren = true}: { changeDetection = DEFAULT, compileChildren = true}: {
selector?: string, selector?: string,
properties?: List<string>, properties?: List<string>,
@ -982,6 +1010,7 @@ export class Component extends Directive {
hostProperties?: StringMap<string, string>, hostProperties?: StringMap<string, string>,
hostAttributes?: StringMap<string, string>, hostAttributes?: StringMap<string, string>,
hostActions?: StringMap<string, string>, hostActions?: StringMap<string, string>,
exportAs?: string,
appInjector?: List<any>, appInjector?: List<any>,
lifecycle?: List<LifecycleEvent>, lifecycle?: List<LifecycleEvent>,
hostInjector?: List<any>, hostInjector?: List<any>,
@ -997,6 +1026,7 @@ export class Component extends Directive {
hostProperties: hostProperties, hostProperties: hostProperties,
hostAttributes: hostAttributes, hostAttributes: hostAttributes,
hostActions: hostActions, hostActions: hostActions,
exportAs: exportAs,
hostInjector: hostInjector, hostInjector: hostInjector,
lifecycle: lifecycle, lifecycle: lifecycle,
compileChildren: compileChildren compileChildren: compileChildren

View File

@ -8,8 +8,10 @@ import * as viewModule from './view';
export class ElementBinder { export class ElementBinder {
nestedProtoView: viewModule.AppProtoView; nestedProtoView: viewModule.AppProtoView;
hostListeners: StringMap<string, Map<number, AST>>; hostListeners: StringMap<string, Map<number, AST>>;
constructor(public index: int, public parent: ElementBinder, public distanceToParent: int, constructor(public index: int, public parent: ElementBinder, public distanceToParent: int,
public protoElementInjector: eiModule.ProtoElementInjector, public protoElementInjector: eiModule.ProtoElementInjector,
public directiveVariableBindings: Map<string, number>,
public componentDirective: DirectiveBinding) { public componentDirective: DirectiveBinding) {
if (isBlank(index)) { if (isBlank(index)) {
throw new BaseException('null index not allowed.'); throw new BaseException('null index not allowed.');

View File

@ -317,7 +317,9 @@ export class DirectiveBinding extends ResolvedBinding {
callOnAllChangesDone: hasLifecycleHook(onAllChangesDone, rb.key.token, ann), callOnAllChangesDone: hasLifecycleHook(onAllChangesDone, rb.key.token, ann),
changeDetection: ann instanceof changeDetection: ann instanceof
Component ? ann.changeDetection : null Component ? ann.changeDetection : null,
exportAs: ann.exportAs
}); });
return new DirectiveBinding(rb.key, rb.factory, deps, rb.providedAsPromise, return new DirectiveBinding(rb.key, rb.factory, deps, rb.providedAsPromise,
resolvedAppInjectables, resolvedHostInjectables, resolvedAppInjectables, resolvedHostInjectables,
@ -422,15 +424,6 @@ export class ProtoElementInjector {
eventEmitterAccessors: List<List<EventEmitterAccessor>>; eventEmitterAccessors: List<List<EventEmitterAccessor>>;
hostActionAccessors: List<List<HostActionAccessor>>; hostActionAccessors: List<List<HostActionAccessor>>;
/** 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; _strategy: _ProtoElementInjectorStrategy;
static create(parent: ProtoElementInjector, index: number, bindings: List<ResolvedBinding>, static create(parent: ProtoElementInjector, index: number, bindings: List<ResolvedBinding>,
@ -483,9 +476,6 @@ export class ProtoElementInjector {
constructor(public parent: ProtoElementInjector, public index: int, bd: List<BindingData>, constructor(public parent: ProtoElementInjector, public index: int, bd: List<BindingData>,
public distanceToParent: number, public _firstBindingIsComponent: boolean) { public distanceToParent: number, public _firstBindingIsComponent: boolean) {
this.exportComponent = false;
this.exportElement = false;
var length = bd.length; var length = bd.length;
this.eventEmitterAccessors = ListWrapper.createFixedSize(length); this.eventEmitterAccessors = ListWrapper.createFixedSize(length);
this.hostActionAccessors = ListWrapper.createFixedSize(length); this.hostActionAccessors = ListWrapper.createFixedSize(length);
@ -1164,15 +1154,6 @@ export class ElementInjector extends TreeNode<ElementInjector> {
hasInstances(): boolean { return this._constructionCounter > 0; } 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; } getLightDomAppInjector(): Injector { return this._lightDomAppInjector; }
getShadowDomAppInjector(): Injector { return this._shadowDomAppInjector; } getShadowDomAppInjector(): Injector { return this._shadowDomAppInjector; }

View File

@ -1,7 +1,7 @@
import {Injectable} from 'angular2/di'; import {Injectable} from 'angular2/di';
import {List, ListWrapper, MapWrapper} from 'angular2/src/facade/collection'; 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 {reflector} from 'angular2/src/reflection/reflection';
import { import {
@ -309,7 +309,7 @@ function _createElementBinders(protoView, elementBinders, allDirectiveBindings)
componentDirectiveBinding, directiveBindings); componentDirectiveBinding, directiveBindings);
_createElementBinder(protoView, i, renderElementBinder, protoElementInjector, _createElementBinder(protoView, i, renderElementBinder, protoElementInjector,
componentDirectiveBinding); componentDirectiveBinding, directiveBindings);
} }
} }
@ -343,28 +343,20 @@ function _createProtoElementInjector(binderIndex, parentPeiWithDistance, renderE
parentPeiWithDistance.protoElementInjector, binderIndex, directiveBindings, parentPeiWithDistance.protoElementInjector, binderIndex, directiveBindings,
isPresent(componentDirectiveBinding), parentPeiWithDistance.distance); isPresent(componentDirectiveBinding), parentPeiWithDistance.distance);
protoElementInjector.attributes = renderElementBinder.readAttributes; 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; return protoElementInjector;
} }
function _createElementBinder(protoView, boundElementIndex, renderElementBinder, function _createElementBinder(protoView, boundElementIndex, renderElementBinder,
protoElementInjector, componentDirectiveBinding): ElementBinder { protoElementInjector, componentDirectiveBinding, directiveBindings): ElementBinder {
var parent = null; var parent = null;
if (renderElementBinder.parentIndex !== -1) { if (renderElementBinder.parentIndex !== -1) {
parent = protoView.elementBinders[renderElementBinder.parentIndex]; parent = protoView.elementBinders[renderElementBinder.parentIndex];
} }
var directiveVariableBindings = createDirectiveVariableBindings(renderElementBinder, directiveBindings);
var elBinder = protoView.bindElement(parent, renderElementBinder.distanceToParent, var elBinder = protoView.bindElement(parent, renderElementBinder.distanceToParent,
protoElementInjector, componentDirectiveBinding); protoElementInjector, directiveVariableBindings, componentDirectiveBinding);
protoView.bindEvent(renderElementBinder.eventBindings, boundElementIndex, -1); protoView.bindEvent(renderElementBinder.eventBindings, boundElementIndex, -1);
// variables // variables
// The view's locals needs to have a full set of variable names at construction time // 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; return elBinder;
} }
export function createDirectiveVariableBindings(renderElementBinder:renderApi.ElementBinder,
directiveBindings:List<DirectiveBinding>): Map<String, number> {
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<renderApi.ElementBinder>) { function _bindDirectiveEvents(protoView, elementBinders: List<renderApi.ElementBinder>) {
for (var boundElementIndex = 0; boundElementIndex < elementBinders.length; ++boundElementIndex) { for (var boundElementIndex = 0; boundElementIndex < elementBinders.length; ++boundElementIndex) {
var dirs = elementBinders[boundElementIndex].directives; var dirs = elementBinders[boundElementIndex].directives;

View File

@ -183,9 +183,12 @@ export class AppProtoView {
bindElement(parent: ElementBinder, distanceToParent: int, bindElement(parent: ElementBinder, distanceToParent: int,
protoElementInjector: ProtoElementInjector, protoElementInjector: ProtoElementInjector,
directiveVariableBindings: Map<string, number>,
componentDirective: DirectiveBinding = null): ElementBinder { componentDirective: DirectiveBinding = null): ElementBinder {
var elBinder = new ElementBinder(this.elementBinders.length, parent, distanceToParent, var elBinder =
protoElementInjector, componentDirective); new ElementBinder(this.elementBinders.length, parent, distanceToParent,
protoElementInjector, directiveVariableBindings, componentDirective);
ListWrapper.push(this.elementBinders, elBinder); ListWrapper.push(this.elementBinders, elBinder);
return elBinder; return elBinder;
} }

View File

@ -206,20 +206,22 @@ export class AppViewManagerUtils {
var binders = view.proto.elementBinders; var binders = view.proto.elementBinders;
for (var i = 0; i < binders.length; ++i) { for (var i = 0; i < binders.length; ++i) {
var binder = binders[i];
var elementInjector = view.elementInjectors[i]; var elementInjector = view.elementInjectors[i];
if (isPresent(elementInjector)) { if (isPresent(elementInjector)) {
elementInjector.hydrate(appInjector, hostElementInjector, view.preBuiltObjects[i]); elementInjector.hydrate(appInjector, hostElementInjector, view.preBuiltObjects[i]);
this._setUpEventEmitters(view, elementInjector, i); this._setUpEventEmitters(view, elementInjector, i);
this._setUpHostActions(view, elementInjector, i); this._setUpHostActions(view, elementInjector, i);
// The exporting of $implicit is a special case. Since multiple elements will all export if (isPresent(binder.directiveVariableBindings)) {
// the different values as $implicit, directly assign $implicit bindings to the variable MapWrapper.forEach(binder.directiveVariableBindings, (directiveIndex, name) => {
// name. if (isBlank(directiveIndex)) {
var exportImplicitName = elementInjector.getExportImplicitName(); view.locals.set(name, elementInjector.getElementRef().domElement);
if (elementInjector.isExportingComponent()) { } else {
view.locals.set(exportImplicitName, elementInjector.getComponent()); view.locals.set(name, elementInjector.getDirectiveAtIndex(directiveIndex));
} else if (elementInjector.isExportingElement()) { }
view.locals.set(exportImplicitName, elementInjector.getElementRef().domElement); });
} }
} }
} }

View File

@ -36,7 +36,7 @@ export class ElementBinder {
directives: List<DirectiveBinder>; directives: List<DirectiveBinder>;
nestedProtoView: ProtoViewDto; nestedProtoView: ProtoViewDto;
propertyBindings: Map<string, ASTWithSource>; propertyBindings: Map<string, ASTWithSource>;
variableBindings: Map<string, ASTWithSource>; variableBindings: Map<string, string>;
// Note: this contains a preprocessed AST // Note: this contains a preprocessed AST
// that replaced the values that should be extracted from the element // that replaced the values that should be extracted from the element
// with a local name // with a local name
@ -52,7 +52,7 @@ export class ElementBinder {
directives?: List<DirectiveBinder>, directives?: List<DirectiveBinder>,
nestedProtoView?: ProtoViewDto, nestedProtoView?: ProtoViewDto,
propertyBindings?: Map<string, ASTWithSource>, propertyBindings?: Map<string, ASTWithSource>,
variableBindings?: Map<string, ASTWithSource>, variableBindings?: Map<string, string>,
eventBindings?: List<EventBinding>, eventBindings?: List<EventBinding>,
textBindings?: List<ASTWithSource>, textBindings?: List<ASTWithSource>,
readAttributes?: Map<string, string> readAttributes?: Map<string, string>
@ -142,9 +142,10 @@ export class DirectiveMetadata {
callOnInit: boolean; callOnInit: boolean;
callOnAllChangesDone: boolean; callOnAllChangesDone: boolean;
changeDetection: string; changeDetection: string;
exportAs: string;
constructor({id, selector, compileChildren, events, hostListeners, hostProperties, hostAttributes, constructor({id, selector, compileChildren, events, hostListeners, hostProperties, hostAttributes,
hostActions, properties, readAttributes, type, callOnDestroy, callOnChange, hostActions, properties, readAttributes, type, callOnDestroy, callOnChange,
callOnCheck, callOnInit, callOnAllChangesDone, changeDetection}: { callOnCheck, callOnInit, callOnAllChangesDone, changeDetection, exportAs}: {
id?: string, id?: string,
selector?: string, selector?: string,
compileChildren?: boolean, compileChildren?: boolean,
@ -161,7 +162,8 @@ export class DirectiveMetadata {
callOnCheck?: boolean, callOnCheck?: boolean,
callOnInit?: boolean, callOnInit?: boolean,
callOnAllChangesDone?: boolean, callOnAllChangesDone?: boolean,
changeDetection?: string changeDetection?: string,
exportAs?: string
}) { }) {
this.id = id; this.id = id;
this.selector = selector; this.selector = selector;
@ -180,6 +182,7 @@ export class DirectiveMetadata {
this.callOnInit = callOnInit; this.callOnInit = callOnInit;
this.callOnAllChangesDone = callOnAllChangesDone; this.callOnAllChangesDone = callOnAllChangesDone;
this.changeDetection = changeDetection; this.changeDetection = changeDetection;
this.exportAs = exportAs;
} }
} }

View File

@ -18,6 +18,7 @@ export function directiveMetadataToMap(meta: DirectiveMetadata): Map<string, any
['properties', _cloneIfPresent(meta.properties)], ['properties', _cloneIfPresent(meta.properties)],
['readAttributes', _cloneIfPresent(meta.readAttributes)], ['readAttributes', _cloneIfPresent(meta.readAttributes)],
['type', meta.type], ['type', meta.type],
['exportAs', meta.exportAs],
['callOnDestroy', meta.callOnDestroy], ['callOnDestroy', meta.callOnDestroy],
['callOnCheck', meta.callOnCheck], ['callOnCheck', meta.callOnCheck],
['callOnInit', meta.callOnInit], ['callOnInit', meta.callOnInit],
@ -44,6 +45,7 @@ export function directiveMetadataFromMap(map: Map<string, any>): DirectiveMetada
properties:<List<string>>_cloneIfPresent(MapWrapper.get(map, 'properties')), properties:<List<string>>_cloneIfPresent(MapWrapper.get(map, 'properties')),
readAttributes:<List<string>>_cloneIfPresent(MapWrapper.get(map, 'readAttributes')), readAttributes:<List<string>>_cloneIfPresent(MapWrapper.get(map, 'readAttributes')),
type:<number>MapWrapper.get(map, 'type'), type:<number>MapWrapper.get(map, 'type'),
exportAs:<string>MapWrapper.get(map, 'exportAs'),
callOnDestroy:<boolean>MapWrapper.get(map, 'callOnDestroy'), callOnDestroy:<boolean>MapWrapper.get(map, 'callOnDestroy'),
callOnCheck:<boolean>MapWrapper.get(map, 'callOnCheck'), callOnCheck:<boolean>MapWrapper.get(map, 'callOnCheck'),
callOnChange:<boolean>MapWrapper.get(map, 'callOnChange'), callOnChange:<boolean>MapWrapper.get(map, 'callOnChange'),

View File

@ -420,11 +420,11 @@ function createProtoView(elementBinders = null) {
function createComponentElementBinder(directiveResolver, type) { function createComponentElementBinder(directiveResolver, type) {
var binding = createDirectiveBinding(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) { 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; elBinder.nestedProtoView = nestedProtoView;
return elBinder; return elBinder;
} }

View File

@ -422,7 +422,8 @@ export function main() {
}); });
})); }));
it('should assign the component instance to a var-', describe("variable bindings", () => {
it('should assign a component to a var-',
inject([TestBed, AsyncTestCompleter], (tb, async) => { inject([TestBed, AsyncTestCompleter], (tb, async) => {
tb.overrideView(MyComp, new viewAnn.View({ tb.overrideView(MyComp, new viewAnn.View({
template: '<p><child-cmp var-alice></child-cmp></p>', template: '<p><child-cmp var-alice></child-cmp></p>',
@ -438,6 +439,22 @@ export function main() {
}) })
})); }));
it('should assign a directive to a var-',
inject([TestBed, AsyncTestCompleter], (tb, async) => {
tb.overrideView(MyComp, new viewAnn.View({
template: '<p><div [export-dir] #localdir="dir"></div></p>',
directives: [ExportDir]
}));
tb.createView(MyComp, {context: ctx})
.then((view) => {
expect(view.rawView.locals).not.toBe(null);
expect(view.rawView.locals.get('localdir')).toBeAnInstanceOf(ExportDir);
async.done();
});
}));
it('should make the assigned component accessible in property bindings', it('should make the assigned component accessible in property bindings',
inject([TestBed, AsyncTestCompleter], (tb, async) => { inject([TestBed, AsyncTestCompleter], (tb, async) => {
tb.overrideView(MyComp, new viewAnn.View({ tb.overrideView(MyComp, new viewAnn.View({
@ -469,7 +486,8 @@ export function main() {
expect(view.rawView.locals).not.toBe(null); expect(view.rawView.locals).not.toBe(null);
expect(view.rawView.locals.get('alice')).toBeAnInstanceOf(ChildComp); expect(view.rawView.locals.get('alice')).toBeAnInstanceOf(ChildComp);
expect(view.rawView.locals.get('bob')).toBeAnInstanceOf(ChildComp); expect(view.rawView.locals.get('bob')).toBeAnInstanceOf(ChildComp);
expect(view.rawView.locals.get('alice')).not.toBe(view.rawView.locals.get('bob')); expect(view.rawView.locals.get('alice'))
.not.toBe(view.rawView.locals.get('bob'));
async.done(); async.done();
}) })
@ -478,7 +496,8 @@ export function main() {
it('should assign the component instance to a var- with shorthand syntax', it('should assign the component instance to a var- with shorthand syntax',
inject([TestBed, AsyncTestCompleter], (tb, async) => { inject([TestBed, AsyncTestCompleter], (tb, async) => {
tb.overrideView( tb.overrideView(
MyComp, new viewAnn.View( MyComp,
new viewAnn.View(
{template: '<child-cmp #alice></child-cmp>', directives: [ChildComp]})); {template: '<child-cmp #alice></child-cmp>', directives: [ChildComp]}));
tb.createView(MyComp, {context: ctx}) tb.createView(MyComp, {context: ctx})
@ -508,24 +527,22 @@ export function main() {
}) })
})); }));
it('should change dash-case to camel-case',
it('should assign the element instance to a user-defined variable with camelCase using dash-case',
inject([TestBed, AsyncTestCompleter], (tb, async) => { inject([TestBed, AsyncTestCompleter], (tb, async) => {
tb.overrideView( tb.overrideView(MyComp, new viewAnn.View({
MyComp, template: '<p><child-cmp var-super-alice></child-cmp></p>',
new viewAnn.View({template: '<p><div var-super-alice><i>Hello</i></div></p>'})); directives: [ChildComp]
}));
tb.createView(MyComp, {context: ctx}) tb.createView(MyComp, {context: ctx})
.then((view) => { .then((view) => {
expect(view.rawView.locals).not.toBe(null); 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", () => { describe("ON_PUSH components", () => {
it("should use ChangeDetectorRef to manually request a check", it("should use ChangeDetectorRef to manually request a check",
@ -1696,3 +1713,7 @@ class SomeImperativeViewport {
} }
} }
} }
@Directive({selector: '[export-dir]', exportAs: 'dir'})
class ExportDir {
}

View File

@ -20,9 +20,11 @@ import {MapWrapper} from 'angular2/src/facade/collection';
import {ChangeDetection, ChangeDetectorDefinition} from 'angular2/change_detection'; import {ChangeDetection, ChangeDetectorDefinition} from 'angular2/change_detection';
import { import {
ProtoViewFactory, ProtoViewFactory,
getChangeDetectorDefinitions getChangeDetectorDefinitions,
createDirectiveVariableBindings
} from 'angular2/src/core/compiler/proto_view_factory'; } from 'angular2/src/core/compiler/proto_view_factory';
import {Component, Directive} from 'angular2/annotations'; import {Component, Directive} from 'angular2/annotations';
import {Key} from 'angular2/di';
import {DirectiveResolver} from 'angular2/src/core/compiler/directive_resolver'; import {DirectiveResolver} from 'angular2/src/core/compiler/directive_resolver';
import {DirectiveBinding} from 'angular2/src/core/compiler/element_injector'; import {DirectiveBinding} from 'angular2/src/core/compiler/element_injector';
import * as renderApi from 'angular2/src/render/api'; import * as renderApi from 'angular2/src/render/api';
@ -65,10 +67,83 @@ export function main() {
expect(pvs.length).toBe(1); expect(pvs.length).toBe(1);
expect(pvs[0].render).toBe(renderPv.render); 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) { function createRenderProtoView(elementBinders = null, type: number = null) {

View File

@ -58,11 +58,11 @@ export function main() {
return DirectiveBinding.createFromType(type, annotation); 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) { function createComponentElBinder(nestedProtoView = null) {
var binding = createDirectiveBinding(SomeComponent); 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; binder.nestedProtoView = nestedProtoView;
return binder; return binder;
} }

View File

@ -48,11 +48,11 @@ export function main() {
return DirectiveBinding.createFromType(type, annotation); 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) { function createComponentElBinder(nestedProtoView = null) {
var binding = createDirectiveBinding(SomeComponent); 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; binder.nestedProtoView = nestedProtoView;
return binder; return binder;
} }

View File

@ -16,6 +16,7 @@ export function main() {
readAttributes: ['read1', 'read2'], readAttributes: ['read1', 'read2'],
selector: 'some-comp', selector: 'some-comp',
type: DirectiveMetadata.COMPONENT_TYPE, type: DirectiveMetadata.COMPONENT_TYPE,
exportAs: 'aaa',
callOnDestroy: true, callOnDestroy: true,
callOnChange: true, callOnChange: true,
callOnCheck: true, callOnCheck: true,
@ -40,6 +41,7 @@ export function main() {
expect(MapWrapper.get(map, 'callOnChange')).toEqual(true); expect(MapWrapper.get(map, 'callOnChange')).toEqual(true);
expect(MapWrapper.get(map, 'callOnInit')).toEqual(true); expect(MapWrapper.get(map, 'callOnInit')).toEqual(true);
expect(MapWrapper.get(map, 'callOnAllChangesDone')).toEqual(true); expect(MapWrapper.get(map, 'callOnAllChangesDone')).toEqual(true);
expect(MapWrapper.get(map, 'exportAs')).toEqual('aaa');
}); });
it('mapToDirectiveMetadata', () => { it('mapToDirectiveMetadata', () => {
@ -53,6 +55,7 @@ export function main() {
['readAttributes', ['readTest1', 'readTest2']], ['readAttributes', ['readTest1', 'readTest2']],
['selector', 'testSelector'], ['selector', 'testSelector'],
['type', DirectiveMetadata.DIRECTIVE_TYPE], ['type', DirectiveMetadata.DIRECTIVE_TYPE],
['exportAs', 'aaa'],
['callOnDestroy', true], ['callOnDestroy', true],
['callOnCheck', true], ['callOnCheck', true],
['callOnInit', true], ['callOnInit', true],
@ -71,6 +74,7 @@ export function main() {
expect(meta.readAttributes).toEqual(['readTest1', 'readTest2']); expect(meta.readAttributes).toEqual(['readTest1', 'readTest2']);
expect(meta.selector).toEqual('testSelector'); expect(meta.selector).toEqual('testSelector');
expect(meta.type).toEqual(DirectiveMetadata.DIRECTIVE_TYPE); expect(meta.type).toEqual(DirectiveMetadata.DIRECTIVE_TYPE);
expect(meta.exportAs).toEqual('aaa');
expect(meta.callOnDestroy).toEqual(true); expect(meta.callOnDestroy).toEqual(true);
expect(meta.callOnCheck).toEqual(true); expect(meta.callOnCheck).toEqual(true);
expect(meta.callOnInit).toEqual(true); expect(meta.callOnInit).toEqual(true);