Skip to content

Commit 69b75b7

Browse files
committed
feat(view): added support for exportAs, so any directive can be assigned to a variable
1 parent 4eb8c9b commit 69b75b7

File tree

14 files changed

+298
-140
lines changed

14 files changed

+298
-140
lines changed

modules/angular2/src/core/annotations_impl/annotations.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -746,9 +746,35 @@ export class Directive extends Injectable {
746746
*/
747747
hostInjector: List<any>;
748748

749+
/**
750+
* Defines the name that can be used in the template to assign this directive to a variable.
751+
*
752+
* ## Simple Example
753+
*
754+
* @Directive({
755+
* selector: 'child-dir',
756+
* exportAs: 'child'
757+
* })
758+
* class ChildDir {
759+
* }
760+
*
761+
* @Component({
762+
* selector: 'main',
763+
* })
764+
* @View({
765+
* template: `<child-dir #c="child"></child-dir>`,
766+
* directives: [ChildDir]
767+
* })
768+
* class MainComponent {
769+
* }
770+
*
771+
* ```
772+
*/
773+
exportAs: string;
774+
749775
constructor({
750776
selector, properties, events, hostListeners, hostProperties, hostAttributes,
751-
hostActions, lifecycle, hostInjector, compileChildren = true,
777+
hostActions, lifecycle, hostInjector, exportAs, compileChildren = true,
752778
}: {
753779
selector?: string,
754780
properties?: List<string>,
@@ -759,6 +785,7 @@ export class Directive extends Injectable {
759785
hostActions?: StringMap<string, string>,
760786
lifecycle?: List<LifecycleEvent>,
761787
hostInjector?: List<any>,
788+
exportAs?: string,
762789
compileChildren?: boolean
763790
} = {}) {
764791
super();
@@ -769,6 +796,7 @@ export class Directive extends Injectable {
769796
this.hostProperties = hostProperties;
770797
this.hostAttributes = hostAttributes;
771798
this.hostActions = hostActions;
799+
this.exportAs = exportAs;
772800
this.lifecycle = lifecycle;
773801
this.compileChildren = compileChildren;
774802
this.hostInjector = hostInjector;
@@ -973,7 +1001,7 @@ export class Component extends Directive {
9731001
viewInjector: List<any>;
9741002

9751003
constructor({selector, properties, events, hostListeners, hostProperties, hostAttributes,
976-
hostActions, appInjector, lifecycle, hostInjector, viewInjector,
1004+
hostActions, exportAs, appInjector, lifecycle, hostInjector, viewInjector,
9771005
changeDetection = DEFAULT, compileChildren = true}: {
9781006
selector?: string,
9791007
properties?: List<string>,
@@ -982,6 +1010,7 @@ export class Component extends Directive {
9821010
hostProperties?: StringMap<string, string>,
9831011
hostAttributes?: StringMap<string, string>,
9841012
hostActions?: StringMap<string, string>,
1013+
exportAs?: string,
9851014
appInjector?: List<any>,
9861015
lifecycle?: List<LifecycleEvent>,
9871016
hostInjector?: List<any>,
@@ -997,6 +1026,7 @@ export class Component extends Directive {
9971026
hostProperties: hostProperties,
9981027
hostAttributes: hostAttributes,
9991028
hostActions: hostActions,
1029+
exportAs: exportAs,
10001030
hostInjector: hostInjector,
10011031
lifecycle: lifecycle,
10021032
compileChildren: compileChildren

modules/angular2/src/core/compiler/element_binder.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import * as viewModule from './view';
88
export class ElementBinder {
99
nestedProtoView: viewModule.AppProtoView;
1010
hostListeners: StringMap<string, Map<number, AST>>;
11+
1112
constructor(public index: int, public parent: ElementBinder, public distanceToParent: int,
1213
public protoElementInjector: eiModule.ProtoElementInjector,
14+
public directiveVariableBindings: Map<string, number>,
1315
public componentDirective: DirectiveBinding) {
1416
if (isBlank(index)) {
1517
throw new BaseException('null index not allowed.');

modules/angular2/src/core/compiler/element_injector.ts

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,9 @@ export class DirectiveBinding extends ResolvedBinding {
317317
callOnAllChangesDone: hasLifecycleHook(onAllChangesDone, rb.key.token, ann),
318318

319319
changeDetection: ann instanceof
320-
Component ? ann.changeDetection : null
320+
Component ? ann.changeDetection : null,
321+
322+
exportAs: ann.exportAs
321323
});
322324
return new DirectiveBinding(rb.key, rb.factory, deps, rb.providedAsPromise,
323325
resolvedAppInjectables, resolvedHostInjectables,
@@ -422,15 +424,6 @@ export class ProtoElementInjector {
422424
eventEmitterAccessors: List<List<EventEmitterAccessor>>;
423425
hostActionAccessors: List<List<HostActionAccessor>>;
424426

425-
/** Whether the element is exported as $implicit. */
426-
exportElement: boolean;
427-
428-
/** Whether the component instance is exported as $implicit. */
429-
exportComponent: boolean;
430-
431-
/** The variable name that will be set to $implicit for the element. */
432-
exportImplicitName: string;
433-
434427
_strategy: _ProtoElementInjectorStrategy;
435428

436429
static create(parent: ProtoElementInjector, index: number, bindings: List<ResolvedBinding>,
@@ -483,9 +476,6 @@ export class ProtoElementInjector {
483476

484477
constructor(public parent: ProtoElementInjector, public index: int, bd: List<BindingData>,
485478
public distanceToParent: number, public _firstBindingIsComponent: boolean) {
486-
this.exportComponent = false;
487-
this.exportElement = false;
488-
489479
var length = bd.length;
490480
this.eventEmitterAccessors = ListWrapper.createFixedSize(length);
491481
this.hostActionAccessors = ListWrapper.createFixedSize(length);
@@ -1164,15 +1154,6 @@ export class ElementInjector extends TreeNode<ElementInjector> {
11641154

11651155
hasInstances(): boolean { return this._constructionCounter > 0; }
11661156

1167-
/** Gets whether this element is exporting a component instance as $implicit. */
1168-
isExportingComponent(): boolean { return this._proto.exportComponent; }
1169-
1170-
/** Gets whether this element is exporting its element as $implicit. */
1171-
isExportingElement(): boolean { return this._proto.exportElement; }
1172-
1173-
/** Get the name to which this element's $implicit is to be assigned. */
1174-
getExportImplicitName(): string { return this._proto.exportImplicitName; }
1175-
11761157
getLightDomAppInjector(): Injector { return this._lightDomAppInjector; }
11771158

11781159
getShadowDomAppInjector(): Injector { return this._shadowDomAppInjector; }

modules/angular2/src/core/compiler/proto_view_factory.ts

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {Injectable} from 'angular2/di';
22

33
import {List, ListWrapper, MapWrapper} from 'angular2/src/facade/collection';
4-
import {isPresent, isBlank} from 'angular2/src/facade/lang';
4+
import {isPresent, isBlank, BaseException} from 'angular2/src/facade/lang';
55
import {reflector} from 'angular2/src/reflection/reflection';
66

77
import {
@@ -309,7 +309,7 @@ function _createElementBinders(protoView, elementBinders, allDirectiveBindings)
309309
componentDirectiveBinding, directiveBindings);
310310

311311
_createElementBinder(protoView, i, renderElementBinder, protoElementInjector,
312-
componentDirectiveBinding);
312+
componentDirectiveBinding, directiveBindings);
313313
}
314314
}
315315

@@ -343,28 +343,20 @@ function _createProtoElementInjector(binderIndex, parentPeiWithDistance, renderE
343343
parentPeiWithDistance.protoElementInjector, binderIndex, directiveBindings,
344344
isPresent(componentDirectiveBinding), parentPeiWithDistance.distance);
345345
protoElementInjector.attributes = renderElementBinder.readAttributes;
346-
if (hasVariables) {
347-
protoElementInjector.exportComponent = isPresent(componentDirectiveBinding);
348-
protoElementInjector.exportElement = isBlank(componentDirectiveBinding);
349-
350-
// experiment
351-
var exportImplicitName = MapWrapper.get(renderElementBinder.variableBindings, '\$implicit');
352-
if (isPresent(exportImplicitName)) {
353-
protoElementInjector.exportImplicitName = exportImplicitName;
354-
}
355-
}
356346
}
357347
return protoElementInjector;
358348
}
359349

360350
function _createElementBinder(protoView, boundElementIndex, renderElementBinder,
361-
protoElementInjector, componentDirectiveBinding): ElementBinder {
351+
protoElementInjector, componentDirectiveBinding, directiveBindings): ElementBinder {
362352
var parent = null;
363353
if (renderElementBinder.parentIndex !== -1) {
364354
parent = protoView.elementBinders[renderElementBinder.parentIndex];
365355
}
356+
357+
var directiveVariableBindings = createDirectiveVariableBindings(renderElementBinder, directiveBindings);
366358
var elBinder = protoView.bindElement(parent, renderElementBinder.distanceToParent,
367-
protoElementInjector, componentDirectiveBinding);
359+
protoElementInjector, directiveVariableBindings, componentDirectiveBinding);
368360
protoView.bindEvent(renderElementBinder.eventBindings, boundElementIndex, -1);
369361
// variables
370362
// 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,
377369
return elBinder;
378370
}
379371

372+
export function createDirectiveVariableBindings(renderElementBinder:renderApi.ElementBinder,
373+
directiveBindings:List<DirectiveBinding>): Map<String, number> {
374+
var directiveVariableBindings = MapWrapper.create();
375+
MapWrapper.forEach(renderElementBinder.variableBindings, (templateName, exportAs) => {
376+
var dirIndex = _findDirectiveIndexByExportAs(renderElementBinder, directiveBindings, exportAs);
377+
MapWrapper.set(directiveVariableBindings, templateName, dirIndex);
378+
});
379+
return directiveVariableBindings;
380+
}
381+
382+
function _findDirectiveIndexByExportAs(renderElementBinder, directiveBindings, exportAs) {
383+
var matchedDirectiveIndex = null;
384+
var matchedDirective;
385+
386+
for (var i = 0; i < directiveBindings.length; ++i) {
387+
var directive = directiveBindings[i];
388+
389+
if (_directiveExportAs(directive) == exportAs) {
390+
if (isPresent(matchedDirective)) {
391+
throw new BaseException(`More than one directive have exportAs = '${exportAs}'. Directives: [${matchedDirective.displayName}, ${directive.displayName}]`);
392+
}
393+
394+
matchedDirectiveIndex = i;
395+
matchedDirective = directive;
396+
}
397+
}
398+
399+
if (isBlank(matchedDirective) && exportAs !== "$implicit") {
400+
throw new BaseException(`Cannot find directive with exportAs = '${exportAs}'`);
401+
}
402+
403+
return matchedDirectiveIndex;
404+
}
405+
406+
function _directiveExportAs(directive):string {
407+
var directiveExportAs = directive.metadata.exportAs;
408+
if (isBlank(directiveExportAs) && directive.metadata.type === renderApi.DirectiveMetadata.COMPONENT_TYPE) {
409+
return "$implicit";
410+
} else {
411+
return directiveExportAs;
412+
}
413+
}
414+
380415
function _bindDirectiveEvents(protoView, elementBinders: List<renderApi.ElementBinder>) {
381416
for (var boundElementIndex = 0; boundElementIndex < elementBinders.length; ++boundElementIndex) {
382417
var dirs = elementBinders[boundElementIndex].directives;

modules/angular2/src/core/compiler/view.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,9 +183,12 @@ export class AppProtoView {
183183

184184
bindElement(parent: ElementBinder, distanceToParent: int,
185185
protoElementInjector: ProtoElementInjector,
186+
directiveVariableBindings: Map<string, number>,
186187
componentDirective: DirectiveBinding = null): ElementBinder {
187-
var elBinder = new ElementBinder(this.elementBinders.length, parent, distanceToParent,
188-
protoElementInjector, componentDirective);
188+
var elBinder =
189+
new ElementBinder(this.elementBinders.length, parent, distanceToParent,
190+
protoElementInjector, directiveVariableBindings, componentDirective);
191+
189192
ListWrapper.push(this.elementBinders, elBinder);
190193
return elBinder;
191194
}

modules/angular2/src/core/compiler/view_manager_utils.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -206,20 +206,22 @@ export class AppViewManagerUtils {
206206

207207
var binders = view.proto.elementBinders;
208208
for (var i = 0; i < binders.length; ++i) {
209+
var binder = binders[i];
209210
var elementInjector = view.elementInjectors[i];
211+
210212
if (isPresent(elementInjector)) {
211213
elementInjector.hydrate(appInjector, hostElementInjector, view.preBuiltObjects[i]);
212214
this._setUpEventEmitters(view, elementInjector, i);
213215
this._setUpHostActions(view, elementInjector, i);
214216

215-
// The exporting of $implicit is a special case. Since multiple elements will all export
216-
// the different values as $implicit, directly assign $implicit bindings to the variable
217-
// name.
218-
var exportImplicitName = elementInjector.getExportImplicitName();
219-
if (elementInjector.isExportingComponent()) {
220-
view.locals.set(exportImplicitName, elementInjector.getComponent());
221-
} else if (elementInjector.isExportingElement()) {
222-
view.locals.set(exportImplicitName, elementInjector.getElementRef().domElement);
217+
if (isPresent(binder.directiveVariableBindings)) {
218+
MapWrapper.forEach(binder.directiveVariableBindings, (directiveIndex, name) => {
219+
if (isBlank(directiveIndex)) {
220+
view.locals.set(name, elementInjector.getElementRef().domElement);
221+
} else {
222+
view.locals.set(name, elementInjector.getDirectiveAtIndex(directiveIndex));
223+
}
224+
});
223225
}
224226
}
225227
}

modules/angular2/src/render/api.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export class ElementBinder {
3636
directives: List<DirectiveBinder>;
3737
nestedProtoView: ProtoViewDto;
3838
propertyBindings: Map<string, ASTWithSource>;
39-
variableBindings: Map<string, ASTWithSource>;
39+
variableBindings: Map<string, string>;
4040
// Note: this contains a preprocessed AST
4141
// that replaced the values that should be extracted from the element
4242
// with a local name
@@ -52,7 +52,7 @@ export class ElementBinder {
5252
directives?: List<DirectiveBinder>,
5353
nestedProtoView?: ProtoViewDto,
5454
propertyBindings?: Map<string, ASTWithSource>,
55-
variableBindings?: Map<string, ASTWithSource>,
55+
variableBindings?: Map<string, string>,
5656
eventBindings?: List<EventBinding>,
5757
textBindings?: List<ASTWithSource>,
5858
readAttributes?: Map<string, string>
@@ -142,9 +142,10 @@ export class DirectiveMetadata {
142142
callOnInit: boolean;
143143
callOnAllChangesDone: boolean;
144144
changeDetection: string;
145+
exportAs: string;
145146
constructor({id, selector, compileChildren, events, hostListeners, hostProperties, hostAttributes,
146147
hostActions, properties, readAttributes, type, callOnDestroy, callOnChange,
147-
callOnCheck, callOnInit, callOnAllChangesDone, changeDetection}: {
148+
callOnCheck, callOnInit, callOnAllChangesDone, changeDetection, exportAs}: {
148149
id?: string,
149150
selector?: string,
150151
compileChildren?: boolean,
@@ -161,7 +162,8 @@ export class DirectiveMetadata {
161162
callOnCheck?: boolean,
162163
callOnInit?: boolean,
163164
callOnAllChangesDone?: boolean,
164-
changeDetection?: string
165+
changeDetection?: string,
166+
exportAs?: string
165167
}) {
166168
this.id = id;
167169
this.selector = selector;
@@ -180,6 +182,7 @@ export class DirectiveMetadata {
180182
this.callOnInit = callOnInit;
181183
this.callOnAllChangesDone = callOnAllChangesDone;
182184
this.changeDetection = changeDetection;
185+
this.exportAs = exportAs;
183186
}
184187
}
185188

modules/angular2/src/render/dom/convert.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export function directiveMetadataToMap(meta: DirectiveMetadata): Map<string, any
1818
['properties', _cloneIfPresent(meta.properties)],
1919
['readAttributes', _cloneIfPresent(meta.readAttributes)],
2020
['type', meta.type],
21+
['exportAs', meta.exportAs],
2122
['callOnDestroy', meta.callOnDestroy],
2223
['callOnCheck', meta.callOnCheck],
2324
['callOnInit', meta.callOnInit],
@@ -44,6 +45,7 @@ export function directiveMetadataFromMap(map: Map<string, any>): DirectiveMetada
4445
properties:<List<string>>_cloneIfPresent(MapWrapper.get(map, 'properties')),
4546
readAttributes:<List<string>>_cloneIfPresent(MapWrapper.get(map, 'readAttributes')),
4647
type:<number>MapWrapper.get(map, 'type'),
48+
exportAs:<string>MapWrapper.get(map, 'exportAs'),
4749
callOnDestroy:<boolean>MapWrapper.get(map, 'callOnDestroy'),
4850
callOnCheck:<boolean>MapWrapper.get(map, 'callOnCheck'),
4951
callOnChange:<boolean>MapWrapper.get(map, 'callOnChange'),

modules/angular2/test/core/compiler/compiler_spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -420,11 +420,11 @@ function createProtoView(elementBinders = null) {
420420

421421
function createComponentElementBinder(directiveResolver, type) {
422422
var binding = createDirectiveBinding(directiveResolver, type);
423-
return new ElementBinder(0, null, 0, null, binding);
423+
return new ElementBinder(0, null, 0, null, null, binding);
424424
}
425425

426426
function createViewportElementBinder(nestedProtoView) {
427-
var elBinder = new ElementBinder(0, null, 0, null, null);
427+
var elBinder = new ElementBinder(0, null, 0, null, null, null);
428428
elBinder.nestedProtoView = nestedProtoView;
429429
return elBinder;
430430
}

0 commit comments

Comments
 (0)