Skip to content

Commit

Permalink
feat(view): added support for exportAs, so any directive can be assig…
Browse files Browse the repository at this point in the history
…ned to a variable
  • Loading branch information
vsavkin committed Jun 4, 2015
1 parent 4eb8c9b commit 69b75b7
Show file tree
Hide file tree
Showing 14 changed files with 298 additions and 140 deletions.
34 changes: 32 additions & 2 deletions modules/angular2/src/core/annotations_impl/annotations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -746,9 +746,35 @@ export class Directive extends Injectable {
*/
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({
selector, properties, events, hostListeners, hostProperties, hostAttributes,
hostActions, lifecycle, hostInjector, compileChildren = true,
hostActions, lifecycle, hostInjector, exportAs, compileChildren = true,
}: {
selector?: string,
properties?: List<string>,
Expand All @@ -759,6 +785,7 @@ export class Directive extends Injectable {
hostActions?: StringMap<string, string>,
lifecycle?: List<LifecycleEvent>,
hostInjector?: List<any>,
exportAs?: string,
compileChildren?: boolean
} = {}) {
super();
Expand All @@ -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;
Expand Down Expand Up @@ -973,7 +1001,7 @@ export class Component extends Directive {
viewInjector: List<any>;

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<string>,
Expand All @@ -982,6 +1010,7 @@ export class Component extends Directive {
hostProperties?: StringMap<string, string>,
hostAttributes?: StringMap<string, string>,
hostActions?: StringMap<string, string>,
exportAs?: string,
appInjector?: List<any>,
lifecycle?: List<LifecycleEvent>,
hostInjector?: List<any>,
Expand All @@ -997,6 +1026,7 @@ export class Component extends Directive {
hostProperties: hostProperties,
hostAttributes: hostAttributes,
hostActions: hostActions,
exportAs: exportAs,
hostInjector: hostInjector,
lifecycle: lifecycle,
compileChildren: compileChildren
Expand Down
2 changes: 2 additions & 0 deletions modules/angular2/src/core/compiler/element_binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import * as viewModule from './view';
export class ElementBinder {
nestedProtoView: viewModule.AppProtoView;
hostListeners: StringMap<string, Map<number, AST>>;

constructor(public index: int, public parent: ElementBinder, public distanceToParent: int,
public protoElementInjector: eiModule.ProtoElementInjector,
public directiveVariableBindings: Map<string, number>,
public componentDirective: DirectiveBinding) {
if (isBlank(index)) {
throw new BaseException('null index not allowed.');
Expand Down
25 changes: 3 additions & 22 deletions modules/angular2/src/core/compiler/element_injector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -422,15 +424,6 @@ export class ProtoElementInjector {
eventEmitterAccessors: List<List<EventEmitterAccessor>>;
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;

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

constructor(public parent: ProtoElementInjector, public index: int, bd: List<BindingData>,
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);
Expand Down Expand Up @@ -1164,15 +1154,6 @@ export class ElementInjector extends TreeNode<ElementInjector> {

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; }
Expand Down
63 changes: 49 additions & 14 deletions modules/angular2/src/core/compiler/proto_view_factory.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -309,7 +309,7 @@ function _createElementBinders(protoView, elementBinders, allDirectiveBindings)
componentDirectiveBinding, directiveBindings);

_createElementBinder(protoView, i, renderElementBinder, protoElementInjector,
componentDirectiveBinding);
componentDirectiveBinding, directiveBindings);
}
}

Expand Down Expand Up @@ -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
Expand All @@ -377,6 +369,49 @@ function _createElementBinder(protoView, boundElementIndex, renderElementBinder,
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>) {
for (var boundElementIndex = 0; boundElementIndex < elementBinders.length; ++boundElementIndex) {
var dirs = elementBinders[boundElementIndex].directives;
Expand Down
7 changes: 5 additions & 2 deletions modules/angular2/src/core/compiler/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,12 @@ export class AppProtoView {

bindElement(parent: ElementBinder, distanceToParent: int,
protoElementInjector: ProtoElementInjector,
directiveVariableBindings: Map<string, number>,
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;
}
Expand Down
18 changes: 10 additions & 8 deletions modules/angular2/src/core/compiler/view_manager_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
});
}
}
}
Expand Down
11 changes: 7 additions & 4 deletions modules/angular2/src/render/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export class ElementBinder {
directives: List<DirectiveBinder>;
nestedProtoView: ProtoViewDto;
propertyBindings: Map<string, ASTWithSource>;
variableBindings: Map<string, ASTWithSource>;
variableBindings: Map<string, string>;
// Note: this contains a preprocessed AST
// that replaced the values that should be extracted from the element
// with a local name
Expand All @@ -52,7 +52,7 @@ export class ElementBinder {
directives?: List<DirectiveBinder>,
nestedProtoView?: ProtoViewDto,
propertyBindings?: Map<string, ASTWithSource>,
variableBindings?: Map<string, ASTWithSource>,
variableBindings?: Map<string, string>,
eventBindings?: List<EventBinding>,
textBindings?: List<ASTWithSource>,
readAttributes?: Map<string, string>
Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -180,6 +182,7 @@ export class DirectiveMetadata {
this.callOnInit = callOnInit;
this.callOnAllChangesDone = callOnAllChangesDone;
this.changeDetection = changeDetection;
this.exportAs = exportAs;
}
}

Expand Down
2 changes: 2 additions & 0 deletions modules/angular2/src/render/dom/convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export function directiveMetadataToMap(meta: DirectiveMetadata): Map<string, any
['properties', _cloneIfPresent(meta.properties)],
['readAttributes', _cloneIfPresent(meta.readAttributes)],
['type', meta.type],
['exportAs', meta.exportAs],
['callOnDestroy', meta.callOnDestroy],
['callOnCheck', meta.callOnCheck],
['callOnInit', meta.callOnInit],
Expand All @@ -44,6 +45,7 @@ export function directiveMetadataFromMap(map: Map<string, any>): DirectiveMetada
properties:<List<string>>_cloneIfPresent(MapWrapper.get(map, 'properties')),
readAttributes:<List<string>>_cloneIfPresent(MapWrapper.get(map, 'readAttributes')),
type:<number>MapWrapper.get(map, 'type'),
exportAs:<string>MapWrapper.get(map, 'exportAs'),
callOnDestroy:<boolean>MapWrapper.get(map, 'callOnDestroy'),
callOnCheck:<boolean>MapWrapper.get(map, 'callOnCheck'),
callOnChange:<boolean>MapWrapper.get(map, 'callOnChange'),
Expand Down
4 changes: 2 additions & 2 deletions modules/angular2/test/core/compiler/compiler_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Loading

0 comments on commit 69b75b7

Please sign in to comment.