feat(compiler): added support for host actions

This commit is contained in:
vsavkin
2015-05-11 12:31:16 -07:00
parent a9ce0f7afb
commit f9c1de46b3
22 changed files with 330 additions and 17 deletions

View File

@ -620,6 +620,33 @@ export class Directive extends Injectable {
*/ */
hostAttributes:any; // String map hostAttributes:any; // String map
/**
* Specifies which DOM methods a directive can invoke.
*
* ## Syntax
*
* ```
* @Directive({
* selector: 'input',
* hostActions: {
* 'emitFocus': 'focus()'
* }
* })
* class InputDirective {
* constructor() {
* this.emitFocus = new EventEmitter();
* }
*
* focus() {
* this.emitFocus.next();
* }
* }
*
* In this example calling focus on InputDirective will result in calling focus on the DOM element.
* ```
*/
hostActions:any; // String map
/** /**
* Specifies a set of lifecycle hostListeners in which the directive participates. * Specifies a set of lifecycle hostListeners in which the directive participates.
* *
@ -641,6 +668,7 @@ export class Directive extends Injectable {
hostListeners, hostListeners,
hostProperties, hostProperties,
hostAttributes, hostAttributes,
hostActions,
lifecycle, lifecycle,
compileChildren = true, compileChildren = true,
}:{ }:{
@ -650,6 +678,7 @@ export class Directive extends Injectable {
hostListeners: any, hostListeners: any,
hostProperties: any, hostProperties: any,
hostAttributes: any, hostAttributes: any,
hostActions: any,
lifecycle:List, lifecycle:List,
compileChildren:boolean compileChildren:boolean
}={}) }={})
@ -661,6 +690,7 @@ export class Directive extends Injectable {
this.hostListeners = hostListeners; this.hostListeners = hostListeners;
this.hostProperties = hostProperties; this.hostProperties = hostProperties;
this.hostAttributes = hostAttributes; this.hostAttributes = hostAttributes;
this.hostActions = hostActions;
this.lifecycle = lifecycle; this.lifecycle = lifecycle;
this.compileChildren = compileChildren; this.compileChildren = compileChildren;
} }
@ -858,6 +888,7 @@ export class Component extends Directive {
hostListeners, hostListeners,
hostProperties, hostProperties,
hostAttributes, hostAttributes,
hostActions,
injectables, injectables,
lifecycle, lifecycle,
changeDetection = DEFAULT, changeDetection = DEFAULT,
@ -870,6 +901,7 @@ export class Component extends Directive {
hostListeners:any, hostListeners:any,
hostProperties:any, hostProperties:any,
hostAttributes:any, hostAttributes:any,
hostActions:any,
injectables:List, injectables:List,
lifecycle:List, lifecycle:List,
changeDetection:string, changeDetection:string,
@ -884,6 +916,7 @@ export class Component extends Directive {
hostListeners: hostListeners, hostListeners: hostListeners,
hostProperties: hostProperties, hostProperties: hostProperties,
hostAttributes: hostAttributes, hostAttributes: hostAttributes,
hostActions: hostActions,
lifecycle: lifecycle, lifecycle: lifecycle,
compileChildren: compileChildren compileChildren: compileChildren
}); });

View File

@ -233,6 +233,7 @@ export class Compiler {
hostListeners: isPresent(ann.hostListeners) ? MapWrapper.createFromStringMap(ann.hostListeners) : null, hostListeners: isPresent(ann.hostListeners) ? MapWrapper.createFromStringMap(ann.hostListeners) : null,
hostProperties: isPresent(ann.hostProperties) ? MapWrapper.createFromStringMap(ann.hostProperties) : null, hostProperties: isPresent(ann.hostProperties) ? MapWrapper.createFromStringMap(ann.hostProperties) : null,
hostAttributes: isPresent(ann.hostAttributes) ? MapWrapper.createFromStringMap(ann.hostAttributes) : null, hostAttributes: isPresent(ann.hostAttributes) ? MapWrapper.createFromStringMap(ann.hostAttributes) : null,
hostActions: isPresent(ann.hostActions) ? MapWrapper.createFromStringMap(ann.hostActions) : null,
properties: isPresent(ann.properties) ? MapWrapper.createFromStringMap(ann.properties) : null, properties: isPresent(ann.properties) ? MapWrapper.createFromStringMap(ann.properties) : null,
readAttributes: readAttributes readAttributes: readAttributes
}); });

View File

@ -1,7 +1,7 @@
import {isPresent, isBlank, Type, int, BaseException} from 'angular2/src/facade/lang'; import {isPresent, isBlank, Type, int, BaseException, stringify} from 'angular2/src/facade/lang';
import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async'; import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async';
import {Math} from 'angular2/src/facade/math'; import {Math} from 'angular2/src/facade/math';
import {List, ListWrapper, MapWrapper} from 'angular2/src/facade/collection'; import {List, ListWrapper, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
import {Injector, Key, Dependency, bind, Binding, ResolvedBinding, NoBindingError, import {Injector, Key, Dependency, bind, Binding, ResolvedBinding, NoBindingError,
AbstractBindingError, CyclicDependencyError} from 'angular2/di'; AbstractBindingError, CyclicDependencyError} from 'angular2/di';
import {Parent, Ancestor} from 'angular2/src/core/annotations_impl/visibility'; import {Parent, Ancestor} from 'angular2/src/core/annotations_impl/visibility';
@ -248,6 +248,10 @@ export class DirectiveBinding extends ResolvedBinding {
return isPresent(this.annotation) && isPresent(this.annotation.events) ? this.annotation.events : []; return isPresent(this.annotation) && isPresent(this.annotation.events) ? this.annotation.events : [];
} }
get hostActions() { //StringMap
return isPresent(this.annotation) && isPresent(this.annotation.hostActions) ? this.annotation.hostActions : {};
}
get changeDetection() { get changeDetection() {
if (this.annotation instanceof Component) { if (this.annotation instanceof Component) {
var c:Component = this.annotation; var c:Component = this.annotation;
@ -297,6 +301,22 @@ class EventEmitterAccessor {
} }
} }
class HostActionAccessor {
actionExpression:string;
getter:Function;
constructor(actionExpression:string, getter:Function) {
this.actionExpression = actionExpression;
this.getter = getter;
}
subscribe(view:viewModule.AppView, boundElementIndex:number, directive:Object) {
var eventEmitter = this.getter(directive);
return ObservableWrapper.subscribe(eventEmitter,
actionObj => view.callAction(boundElementIndex, this.actionExpression, actionObj));
}
}
/** /**
Difference between di.Injector and ElementInjector Difference between di.Injector and ElementInjector
@ -346,6 +366,7 @@ export class ProtoElementInjector {
distanceToParent:number; distanceToParent:number;
attributes:Map; attributes:Map;
eventEmitterAccessors:List<List<EventEmitterAccessor>>; eventEmitterAccessors:List<List<EventEmitterAccessor>>;
hostActionAccessors:List<List<HostActionAccessor>>;
numberOfDirectives:number; numberOfDirectives:number;
@ -380,56 +401,67 @@ export class ProtoElementInjector {
this.numberOfDirectives = bindings.length; this.numberOfDirectives = bindings.length;
var length = bindings.length; var length = bindings.length;
this.eventEmitterAccessors = ListWrapper.createFixedSize(length); this.eventEmitterAccessors = ListWrapper.createFixedSize(length);
this.hostActionAccessors = ListWrapper.createFixedSize(length);
if (length > 0) { if (length > 0) {
this._binding0 = this._createBinding(bindings[0]); this._binding0 = this._createBinding(bindings[0]);
this._keyId0 = this._binding0.key.id; this._keyId0 = this._binding0.key.id;
this.eventEmitterAccessors[0] = this._createEventEmitterAccessors(this._binding0); this.eventEmitterAccessors[0] = this._createEventEmitterAccessors(this._binding0);
this.hostActionAccessors[0] = this._createHostActionAccessors(this._binding0);
} }
if (length > 1) { if (length > 1) {
this._binding1 = this._createBinding(bindings[1]); this._binding1 = this._createBinding(bindings[1]);
this._keyId1 = this._binding1.key.id; this._keyId1 = this._binding1.key.id;
this.eventEmitterAccessors[1] = this._createEventEmitterAccessors(this._binding1); this.eventEmitterAccessors[1] = this._createEventEmitterAccessors(this._binding1);
this.hostActionAccessors[1] = this._createHostActionAccessors(this._binding1);
} }
if (length > 2) { if (length > 2) {
this._binding2 = this._createBinding(bindings[2]); this._binding2 = this._createBinding(bindings[2]);
this._keyId2 = this._binding2.key.id; this._keyId2 = this._binding2.key.id;
this.eventEmitterAccessors[2] = this._createEventEmitterAccessors(this._binding2); this.eventEmitterAccessors[2] = this._createEventEmitterAccessors(this._binding2);
this.hostActionAccessors[2] = this._createHostActionAccessors(this._binding2);
} }
if (length > 3) { if (length > 3) {
this._binding3 = this._createBinding(bindings[3]); this._binding3 = this._createBinding(bindings[3]);
this._keyId3 = this._binding3.key.id; this._keyId3 = this._binding3.key.id;
this.eventEmitterAccessors[3] = this._createEventEmitterAccessors(this._binding3); this.eventEmitterAccessors[3] = this._createEventEmitterAccessors(this._binding3);
this.hostActionAccessors[3] = this._createHostActionAccessors(this._binding3);
} }
if (length > 4) { if (length > 4) {
this._binding4 = this._createBinding(bindings[4]); this._binding4 = this._createBinding(bindings[4]);
this._keyId4 = this._binding4.key.id; this._keyId4 = this._binding4.key.id;
this.eventEmitterAccessors[4] = this._createEventEmitterAccessors(this._binding4); this.eventEmitterAccessors[4] = this._createEventEmitterAccessors(this._binding4);
this.hostActionAccessors[4] = this._createHostActionAccessors(this._binding4);
} }
if (length > 5) { if (length > 5) {
this._binding5 = this._createBinding(bindings[5]); this._binding5 = this._createBinding(bindings[5]);
this._keyId5 = this._binding5.key.id; this._keyId5 = this._binding5.key.id;
this.eventEmitterAccessors[5] = this._createEventEmitterAccessors(this._binding5); this.eventEmitterAccessors[5] = this._createEventEmitterAccessors(this._binding5);
this.hostActionAccessors[5] = this._createHostActionAccessors(this._binding5);
} }
if (length > 6) { if (length > 6) {
this._binding6 = this._createBinding(bindings[6]); this._binding6 = this._createBinding(bindings[6]);
this._keyId6 = this._binding6.key.id; this._keyId6 = this._binding6.key.id;
this.eventEmitterAccessors[6] = this._createEventEmitterAccessors(this._binding6); this.eventEmitterAccessors[6] = this._createEventEmitterAccessors(this._binding6);
this.hostActionAccessors[6] = this._createHostActionAccessors(this._binding6);
} }
if (length > 7) { if (length > 7) {
this._binding7 = this._createBinding(bindings[7]); this._binding7 = this._createBinding(bindings[7]);
this._keyId7 = this._binding7.key.id; this._keyId7 = this._binding7.key.id;
this.eventEmitterAccessors[7] = this._createEventEmitterAccessors(this._binding7); this.eventEmitterAccessors[7] = this._createEventEmitterAccessors(this._binding7);
this.hostActionAccessors[7] = this._createHostActionAccessors(this._binding7);
} }
if (length > 8) { if (length > 8) {
this._binding8 = this._createBinding(bindings[8]); this._binding8 = this._createBinding(bindings[8]);
this._keyId8 = this._binding8.key.id; this._keyId8 = this._binding8.key.id;
this.eventEmitterAccessors[8] = this._createEventEmitterAccessors(this._binding8); this.eventEmitterAccessors[8] = this._createEventEmitterAccessors(this._binding8);
this.hostActionAccessors[8] = this._createHostActionAccessors(this._binding8);
} }
if (length > 9) { if (length > 9) {
this._binding9 = this._createBinding(bindings[9]); this._binding9 = this._createBinding(bindings[9]);
this._keyId9 = this._binding9.key.id; this._keyId9 = this._binding9.key.id;
this.eventEmitterAccessors[9] = this._createEventEmitterAccessors(this._binding9); this.eventEmitterAccessors[9] = this._createEventEmitterAccessors(this._binding9);
this.hostActionAccessors[9] = this._createHostActionAccessors(this._binding9);
} }
if (length > 10) { if (length > 10) {
throw 'Maximum number of directives per element has been reached.'; throw 'Maximum number of directives per element has been reached.';
@ -442,6 +474,14 @@ export class ProtoElementInjector {
); );
} }
_createHostActionAccessors(b:DirectiveBinding) {
var res = [];
StringMapWrapper.forEach(b.hostActions, (actionExpression, actionName) => {
ListWrapper.push(res, new HostActionAccessor(actionExpression, reflector.getter(actionName)))
});
return res;
}
instantiate(parent:ElementInjector):ElementInjector { instantiate(parent:ElementInjector):ElementInjector {
return new ElementInjector(this, parent); return new ElementInjector(this, parent);
} }
@ -661,6 +701,10 @@ export class ElementInjector extends TreeNode {
return this._proto.eventEmitterAccessors; return this._proto.eventEmitterAccessors;
} }
getHostActionAccessors() {
return this._proto.hostActionAccessors;
}
getComponent() { getComponent() {
if (this._proto._binding0IsComponent) { if (this._proto._binding0IsComponent) {
return this._obj0; return this._obj0;

View File

@ -126,6 +126,10 @@ export class AppView {
return isPresent(childView) ? childView.changeDetector : null; return isPresent(childView) ? childView.changeDetector : null;
} }
callAction(elementIndex:number, actionExpression:string, action:Object) {
this.renderer.callAction(this.render, elementIndex, actionExpression, action);
}
// implementation of EventDispatcher#dispatchEvent // implementation of EventDispatcher#dispatchEvent
// returns false if preventDefault must be applied to the DOM event // returns false if preventDefault must be applied to the DOM event
dispatchEvent(elementIndex:number, eventName:string, locals:Map<string, any>): boolean { dispatchEvent(elementIndex:number, eventName:string, locals:Map<string, any>): boolean {

View File

@ -192,6 +192,7 @@ export class AppViewManagerUtils {
if (isPresent(elementInjector)) { if (isPresent(elementInjector)) {
elementInjector.instantiateDirectives(appInjector, hostElementInjector, view.preBuiltObjects[i]); elementInjector.instantiateDirectives(appInjector, hostElementInjector, view.preBuiltObjects[i]);
this._setUpEventEmitters(view, elementInjector, 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 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 // the different values as $implicit, directly assign $implicit bindings to the variable
@ -220,6 +221,19 @@ export class AppViewManagerUtils {
} }
} }
_setUpHostActions(view:viewModule.AppView, elementInjector:eli.ElementInjector, boundElementIndex:number) {
var hostActions = elementInjector.getHostActionAccessors();
for (var directiveIndex = 0; directiveIndex < hostActions.length; ++directiveIndex) {
var directiveHostActions = hostActions[directiveIndex];
var directive = elementInjector.getDirectiveAtIndex(directiveIndex);
for (var index = 0; index < directiveHostActions.length; ++index) {
var hostActionAccessor = directiveHostActions[index];
hostActionAccessor.subscribe(view, boundElementIndex, directive);
}
}
}
dehydrateView(view:viewModule.AppView) { dehydrateView(view:viewModule.AppView) {
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) {

View File

@ -82,6 +82,9 @@ class StringMapWrapper {
} }
return m; return m;
} }
static List<String> keys(Map<String,dynamic> a) {
return a.keys.toList();
}
static bool isEmpty(Map m) => m.isEmpty; static bool isEmpty(Map m) => m.isEmpty;
} }

View File

@ -53,6 +53,7 @@ export class StringMapWrapper {
static contains(map, key) { return map.hasOwnProperty(key); } static contains(map, key) { return map.hasOwnProperty(key); }
static get(map, key) { return map.hasOwnProperty(key) ? map[key] : undefined; } static get(map, key) { return map.hasOwnProperty(key) ? map[key] : undefined; }
static set(map, key, value) { map[key] = value; } static set(map, key, value) { map[key] = value; }
static keys(map) { return Object.keys(map); }
static isEmpty(map) { static isEmpty(map) {
for (var prop in map) { for (var prop in map) {
return false; return false;

View File

@ -117,16 +117,18 @@ export class DirectiveMetadata {
hostListeners:Map<string, string>; hostListeners:Map<string, string>;
hostProperties:Map<string, string>; hostProperties:Map<string, string>;
hostAttributes:Map<string, string>; hostAttributes:Map<string, string>;
hostActions:Map<string, string>;
properties:Map<string, string>; properties:Map<string, string>;
readAttributes:List<string>; readAttributes:List<string>;
type:number; type:number;
constructor({id, selector, compileChildren, hostListeners, hostProperties, hostAttributes, properties, readAttributes, type}) { constructor({id, selector, compileChildren, hostListeners, hostProperties, hostAttributes, hostActions, properties, readAttributes, type}) {
this.id = id; this.id = id;
this.selector = selector; this.selector = selector;
this.compileChildren = isPresent(compileChildren) ? compileChildren : true; this.compileChildren = isPresent(compileChildren) ? compileChildren : true;
this.hostListeners = hostListeners; this.hostListeners = hostListeners;
this.hostProperties = hostProperties; this.hostProperties = hostProperties;
this.hostAttributes = hostAttributes; this.hostAttributes = hostAttributes;
this.hostActions = hostActions;
this.properties = properties; this.properties = properties;
this.readAttributes = readAttributes; this.readAttributes = readAttributes;
this.type = type; this.type = type;
@ -228,7 +230,7 @@ export class Renderer {
/** /**
* Hydrates a view after it has been attached. Hydration/dehydration is used for reusing views inside of the view pool. * Hydrates a view after it has been attached. Hydration/dehydration is used for reusing views inside of the view pool.
*/ */
hydrateView(hviewRef:RenderViewRef) { hydrateView(viewRef:RenderViewRef) {
} }
/** /**
@ -238,14 +240,22 @@ export class Renderer {
} }
/** /**
* Sets a porperty on an element. * Sets a property on an element.
* Note: This will fail if the property was not mentioned previously as a host property * Note: This will fail if the property was not mentioned previously as a host property
* in the ProtoView * in the ProtoView
*/ */
setElementProperty(viewRef:RenderViewRef, elementIndex:number, propertyName:string, propertyValue:any):void { setElementProperty(viewRef:RenderViewRef, elementIndex:number, propertyName:string, propertyValue:any):void {
} }
/* /**
* Calls an action.
* Note: This will fail if the action was not mentioned previously as a host action
* in the ProtoView
*/
callAction(viewRef:RenderViewRef, elementIndex:number, actionExpression:string, actionArgs:any):void {
}
/**
* Sets the value of a text node. * Sets the value of a text node.
*/ */
setText(viewRef:RenderViewRef, textNodeIndex:number, text:string):void { setText(viewRef:RenderViewRef, textNodeIndex:number, text:string):void {

View File

@ -73,6 +73,11 @@ export class DirectiveParser extends CompileStep {
this._bindDirectiveEvent(eventName, action, current, directiveBinderBuilder); this._bindDirectiveEvent(eventName, action, current, directiveBinderBuilder);
}); });
} }
if (isPresent(directive.hostActions)) {
MapWrapper.forEach(directive.hostActions, (action, actionName) => {
this._bindHostAction(actionName, action, current, directiveBinderBuilder);
});
}
if (isPresent(directive.hostProperties)) { if (isPresent(directive.hostProperties)) {
MapWrapper.forEach(directive.hostProperties, (hostPropertyName, directivePropertyName) => { MapWrapper.forEach(directive.hostProperties, (hostPropertyName, directivePropertyName) => {
this._bindHostProperty(hostPropertyName, directivePropertyName, current, directiveBinderBuilder); this._bindHostProperty(hostPropertyName, directivePropertyName, current, directiveBinderBuilder);
@ -136,7 +141,11 @@ export class DirectiveParser extends CompileStep {
} else { } else {
directiveBinderBuilder.bindEvent(eventName, ast); directiveBinderBuilder.bindEvent(eventName, ast);
} }
}
_bindHostAction(actionName, actionExpression, compileElement, directiveBinderBuilder) {
var ast = this._parser.parseAction(actionExpression, compileElement.elementDescription);
directiveBinderBuilder.bindHostAction(actionName, actionExpression, ast);
} }
_bindHostProperty(hostPropertyName, directivePropertyName, compileElement, directiveBinderBuilder) { _bindHostProperty(hostPropertyName, directivePropertyName, compileElement, directiveBinderBuilder) {

View File

@ -14,6 +14,7 @@ export function directiveMetadataToMap(meta: DirectiveMetadata): Map {
['hostListeners', _cloneIfPresent(meta.hostListeners)], ['hostListeners', _cloneIfPresent(meta.hostListeners)],
['hostProperties', _cloneIfPresent(meta.hostProperties)], ['hostProperties', _cloneIfPresent(meta.hostProperties)],
['hostAttributes', _cloneIfPresent(meta.hostAttributes)], ['hostAttributes', _cloneIfPresent(meta.hostAttributes)],
['hostActions', _cloneIfPresent(meta.hostActions)],
['properties', _cloneIfPresent(meta.properties)], ['properties', _cloneIfPresent(meta.properties)],
['readAttributes', _cloneIfPresent(meta.readAttributes)], ['readAttributes', _cloneIfPresent(meta.readAttributes)],
['type', meta.type], ['type', meta.type],
@ -33,6 +34,7 @@ export function directiveMetadataFromMap(map: Map): DirectiveMetadata {
compileChildren: MapWrapper.get(map, 'compileChildren'), compileChildren: MapWrapper.get(map, 'compileChildren'),
hostListeners: _cloneIfPresent(MapWrapper.get(map, 'hostListeners')), hostListeners: _cloneIfPresent(MapWrapper.get(map, 'hostListeners')),
hostProperties: _cloneIfPresent(MapWrapper.get(map, 'hostProperties')), hostProperties: _cloneIfPresent(MapWrapper.get(map, 'hostProperties')),
hostActions: _cloneIfPresent(MapWrapper.get(map, 'hostActions')),
hostAttributes: _cloneIfPresent(MapWrapper.get(map, 'hostAttributes')), hostAttributes: _cloneIfPresent(MapWrapper.get(map, 'hostAttributes')),
properties: _cloneIfPresent(MapWrapper.get(map, 'properties')), properties: _cloneIfPresent(MapWrapper.get(map, 'properties')),
readAttributes: _cloneIfPresent(MapWrapper.get(map, 'readAttributes')), readAttributes: _cloneIfPresent(MapWrapper.get(map, 'readAttributes')),

View File

@ -192,6 +192,11 @@ export class DomRenderer extends Renderer {
view.setElementProperty(elementIndex, propertyName, propertyValue); view.setElementProperty(elementIndex, propertyName, propertyValue);
} }
callAction(viewRef:RenderViewRef, elementIndex:number, actionExpression:string, actionArgs:any):void {
var view = resolveInternalDomView(viewRef);
view.callAction(elementIndex, actionExpression, actionArgs);
}
setText(viewRef:RenderViewRef, textNodeIndex:number, text:string):void { setText(viewRef:RenderViewRef, textNodeIndex:number, text:string):void {
var view = resolveInternalDomView(viewRef); var view = resolveInternalDomView(viewRef);
DOM.setText(view.boundTextNodes[textNodeIndex], text); DOM.setText(view.boundTextNodes[textNodeIndex], text);

View File

@ -14,6 +14,7 @@ export class ElementBinder {
parentIndex:number; parentIndex:number;
distanceToParent:number; distanceToParent:number;
propertySetters: Map<string, SetterFn>; propertySetters: Map<string, SetterFn>;
hostActions: Map<string, AST>;
constructor({ constructor({
textNodeIndices, textNodeIndices,
@ -23,6 +24,7 @@ export class ElementBinder {
eventLocals, eventLocals,
localEvents, localEvents,
globalEvents, globalEvents,
hostActions,
parentIndex, parentIndex,
distanceToParent, distanceToParent,
propertySetters propertySetters
@ -34,6 +36,7 @@ export class ElementBinder {
this.eventLocals = eventLocals; this.eventLocals = eventLocals;
this.localEvents = localEvents; this.localEvents = localEvents;
this.globalEvents = globalEvents; this.globalEvents = globalEvents;
this.hostActions = hostActions;
this.parentIndex = parentIndex; this.parentIndex = parentIndex;
this.distanceToParent = distanceToParent; this.distanceToParent = distanceToParent;
this.propertySetters = propertySetters; this.propertySetters = propertySetters;
@ -51,3 +54,15 @@ export class Event {
this.fullName = fullName; this.fullName = fullName;
} }
} }
export class HostAction {
actionName: string;
actionExpression: string;
expression: AST;
constructor(actionName: string, actionExpression: string, expression: AST) {
this.actionName = actionName;
this.actionExpression = actionExpression;
this.expression = expression;
}
}

View File

@ -7,7 +7,7 @@ import {
} from 'angular2/change_detection'; } from 'angular2/change_detection';
import {DomProtoView, DomProtoViewRef, resolveInternalDomProtoView} from './proto_view'; import {DomProtoView, DomProtoViewRef, resolveInternalDomProtoView} from './proto_view';
import {ElementBinder, Event} from './element_binder'; import {ElementBinder, Event, HostAction} from './element_binder';
import {setterFactory} from './property_setter_factory'; import {setterFactory} from './property_setter_factory';
import * as api from '../../api'; import * as api from '../../api';
@ -48,6 +48,7 @@ export class ProtoViewBuilder {
var apiElementBinders = []; var apiElementBinders = [];
ListWrapper.forEach(this.elements, (ebb) => { ListWrapper.forEach(this.elements, (ebb) => {
var propertySetters = MapWrapper.create(); var propertySetters = MapWrapper.create();
var hostActions = MapWrapper.create();
var apiDirectiveBinders = ListWrapper.map(ebb.directives, (dbb) => { var apiDirectiveBinders = ListWrapper.map(ebb.directives, (dbb) => {
ebb.eventBuilder.merge(dbb.eventBuilder); ebb.eventBuilder.merge(dbb.eventBuilder);
@ -56,6 +57,10 @@ export class ProtoViewBuilder {
MapWrapper.set(propertySetters, hostPropertyName, setterFactory(hostPropertyName)); MapWrapper.set(propertySetters, hostPropertyName, setterFactory(hostPropertyName));
}); });
ListWrapper.forEach(dbb.hostActions, (hostAction) => {
MapWrapper.set(hostActions, hostAction.actionExpression, hostAction.expression);
});
return new api.DirectiveBinder({ return new api.DirectiveBinder({
directiveIndex: dbb.directiveIndex, directiveIndex: dbb.directiveIndex,
propertyBindings: dbb.propertyBindings, propertyBindings: dbb.propertyBindings,
@ -90,6 +95,7 @@ export class ProtoViewBuilder {
eventLocals: new LiteralArray(ebb.eventBuilder.buildEventLocals()), eventLocals: new LiteralArray(ebb.eventBuilder.buildEventLocals()),
localEvents: ebb.eventBuilder.buildLocalEvents(), localEvents: ebb.eventBuilder.buildLocalEvents(),
globalEvents: ebb.eventBuilder.buildGlobalEvents(), globalEvents: ebb.eventBuilder.buildGlobalEvents(),
hostActions: hostActions,
propertySetters: propertySetters propertySetters: propertySetters
})); }));
}); });
@ -213,6 +219,7 @@ export class DirectiveBuilder {
directiveIndex:number; directiveIndex:number;
propertyBindings: Map<string, ASTWithSource>; propertyBindings: Map<string, ASTWithSource>;
hostPropertyBindings: Map<string, ASTWithSource>; hostPropertyBindings: Map<string, ASTWithSource>;
hostActions: List<HostAction>;
eventBindings: List<api.EventBinding>; eventBindings: List<api.EventBinding>;
eventBuilder: EventBuilder; eventBuilder: EventBuilder;
@ -220,6 +227,7 @@ export class DirectiveBuilder {
this.directiveIndex = directiveIndex; this.directiveIndex = directiveIndex;
this.propertyBindings = MapWrapper.create(); this.propertyBindings = MapWrapper.create();
this.hostPropertyBindings = MapWrapper.create(); this.hostPropertyBindings = MapWrapper.create();
this.hostActions = ListWrapper.create();
this.eventBindings = ListWrapper.create(); this.eventBindings = ListWrapper.create();
this.eventBuilder = new EventBuilder(); this.eventBuilder = new EventBuilder();
} }
@ -232,6 +240,10 @@ export class DirectiveBuilder {
MapWrapper.set(this.hostPropertyBindings, name, expression); MapWrapper.set(this.hostPropertyBindings, name, expression);
} }
bindHostAction(actionName:string, actionExpression:string, expression:ASTWithSource) {
ListWrapper.push(this.hostActions, new HostAction(actionName, actionExpression, expression));
}
bindEvent(name, expression, target = null) { bindEvent(name, expression, target = null) {
ListWrapper.push(this.eventBindings, this.eventBuilder.add(name, expression, target)); ListWrapper.push(this.eventBindings, this.eventBuilder.add(name, expression, target));
} }

View File

@ -1,5 +1,6 @@
import {DOM} from 'angular2/src/dom/dom_adapter'; import {DOM} from 'angular2/src/dom/dom_adapter';
import {ListWrapper, MapWrapper, Map, StringMapWrapper, List} from 'angular2/src/facade/collection'; import {ListWrapper, MapWrapper, Map, StringMapWrapper, List} from 'angular2/src/facade/collection';
import {Locals} from 'angular2/change_detection';
import {int, isPresent, isBlank, BaseException} from 'angular2/src/facade/lang'; import {int, isPresent, isBlank, BaseException} from 'angular2/src/facade/lang';
import {DomViewContainer} from './view_container'; import {DomViewContainer} from './view_container';
@ -80,6 +81,18 @@ export class DomView {
setter(this.boundElements[elementIndex], value); setter(this.boundElements[elementIndex], value);
} }
callAction(elementIndex:number, actionExpression:string, actionArgs:any) {
var binder = this.proto.elementBinders[elementIndex];
var hostAction = MapWrapper.get(binder.hostActions, actionExpression);
hostAction.eval(this.boundElements[elementIndex], this._localsWithAction(actionArgs));
}
_localsWithAction(action:Object):Locals {
var map = MapWrapper.create();
MapWrapper.set(map, '$action', action);
return new Locals(null, map);
}
setText(textIndex:number, value:string) { setText(textIndex:number, value:string) {
DOM.setText(this.boundTextNodes[textIndex], value); DOM.setText(this.boundTextNodes[textIndex], value);
} }

View File

@ -17,7 +17,7 @@ import {ViewRef, Renderer} from 'angular2/src/render/api';
import {QueryList} from 'angular2/src/core/compiler/query_list'; import {QueryList} from 'angular2/src/core/compiler/query_list';
class DummyDirective extends Directive { class DummyDirective extends Directive {
constructor({lifecycle, events} = {}) { super({lifecycle: lifecycle, events: events}); } constructor({lifecycle, events, hostActions} = {}) { super({lifecycle: lifecycle, events: events, hostActions:hostActions}); }
} }
@proxy @proxy
@ -97,6 +97,13 @@ class HasEventEmitter {
} }
} }
class HasHostAction {
hostActionName;
constructor() {
this.hostActionName = "hostAction";
}
}
class NeedsAttribute { class NeedsAttribute {
typeAttribute; typeAttribute;
titleAttribute; titleAttribute;
@ -381,7 +388,7 @@ export function main() {
}); });
describe('event emitters', () => { describe('event emitters', () => {
it('should return a list of event emitter accessors', () => { it('should return a list of event accessors', () => {
var binding = DirectiveBinding.createFromType( var binding = DirectiveBinding.createFromType(
HasEventEmitter, new DummyDirective({events: ['emitter']})); HasEventEmitter, new DummyDirective({events: ['emitter']}));
@ -392,6 +399,18 @@ export function main() {
expect(accessor.eventName).toEqual('emitter'); expect(accessor.eventName).toEqual('emitter');
expect(accessor.getter(new HasEventEmitter())).toEqual('emitter'); expect(accessor.getter(new HasEventEmitter())).toEqual('emitter');
}); });
it('should return a list of hostAction accessors', () => {
var binding = DirectiveBinding.createFromType(
HasEventEmitter, new DummyDirective({hostActions: {'hostActionName' : 'onAction'}}));
var inj = new ProtoElementInjector(null, 0, [binding]);
expect(inj.hostActionAccessors.length).toEqual(1);
var accessor = inj.hostActionAccessors[0][0];
expect(accessor.actionExpression).toEqual('onAction');
expect(accessor.getter(new HasHostAction())).toEqual('hostAction');
});
}); });
}); });

View File

@ -581,15 +581,37 @@ export function main() {
expect(listener.msg).toEqual(''); expect(listener.msg).toEqual('');
emitter.fireEvent('fired !'); ObservableWrapper.subscribe(emitter.event, (_) => {
PromiseWrapper.setTimeout(() => {
expect(listener.msg).toEqual('fired !'); expect(listener.msg).toEqual('fired !');
async.done(); async.done();
}, 0); });
emitter.fireEvent('fired !');
}); });
})); }));
if (DOM.supportsDOMEvents()) {
it("should support invoking methods on the host element via hostActions", inject([TestBed, AsyncTestCompleter], (tb, async) => {
tb.overrideView(MyComp, new View({
template: '<div update-host-actions></div>',
directives: [DirectiveUpdatingHostActions]
}));
tb.createView(MyComp, {context: ctx}).then((view) => {
var injector = view.rawView.elementInjectors[0];
var domElement = view.rootNodes[0];
var updateHost = injector.get(DirectiveUpdatingHostActions);
ObservableWrapper.subscribe(updateHost.setAttr, (_) => {
expect(DOM.getOuterHTML(domElement)).toEqual('<div update-host-actions="" class="ng-binding" key="value"></div>');
async.done();
});
updateHost.triggerSetAttr('value');
});
}));
}
it('should support render events', inject([TestBed, AsyncTestCompleter], (tb, async) => { it('should support render events', inject([TestBed, AsyncTestCompleter], (tb, async) => {
tb.overrideView(MyComp, new View({ tb.overrideView(MyComp, new View({
template: '<div listener></div>', template: '<div listener></div>',
@ -671,6 +693,7 @@ export function main() {
}); });
})); }));
if (DOM.supportsDOMEvents()) { if (DOM.supportsDOMEvents()) {
it('should support preventing default on render events', inject([TestBed, AsyncTestCompleter], (tb, async) => { it('should support preventing default on render events', inject([TestBed, AsyncTestCompleter], (tb, async) => {
tb.overrideView(MyComp, new View({ tb.overrideView(MyComp, new View({
@ -1218,6 +1241,24 @@ class DirectiveUpdatingHostProperties {
} }
} }
@Directive({
selector: '[update-host-actions]',
hostActions: {
'setAttr': 'setAttribute("key", $action["attrValue"])'
}
})
class DirectiveUpdatingHostActions {
setAttr:EventEmitter;
constructor() {
this.setAttr = new EventEmitter();
}
triggerSetAttr(attrValue) {
ObservableWrapper.callNext(this.setAttr, {'attrValue': attrValue});
}
}
@Directive({ @Directive({
selector: '[listener]', selector: '[listener]',
hostListeners: {'event': 'onEvent($event)'} hostListeners: {'event': 'onEvent($event)'}

View File

@ -31,6 +31,7 @@ import {AppViewManagerUtils} from 'angular2/src/core/compiler/view_manager_utils
export function main() { export function main() {
// TODO(tbosch): add more tests here! // TODO(tbosch): add more tests here!
describe('AppViewManagerUtils', () => { describe('AppViewManagerUtils', () => {
var metadataReader; var metadataReader;
@ -71,6 +72,7 @@ export function main() {
'isExportingComponent' : false, 'isExportingComponent' : false,
'isExportingElement' : false, 'isExportingElement' : false,
'getEventEmitterAccessors' : [], 'getEventEmitterAccessors' : [],
'getHostActionAccessors' : [],
'getComponent' : null, 'getComponent' : null,
'getDynamicallyLoadedComponent': null, 'getDynamicallyLoadedComponent': null,
'getHost': host 'getHost': host
@ -154,11 +156,13 @@ export function main() {
var hostView = createView(hostPv); var hostView = createView(hostPv);
var spyEventAccessor1 = SpyObject.stub({"subscribe" : null}); var spyEventAccessor1 = SpyObject.stub({"subscribe" : null});
SpyObject.stub(hostView.elementInjectors[0], { SpyObject.stub(hostView.elementInjectors[0], {
'getHostActionAccessors': [],
'getEventEmitterAccessors': [[spyEventAccessor1]], 'getEventEmitterAccessors': [[spyEventAccessor1]],
'getDirectiveAtIndex': dir 'getDirectiveAtIndex': dir
}); });
var spyEventAccessor2 = SpyObject.stub({"subscribe" : null}); var spyEventAccessor2 = SpyObject.stub({"subscribe" : null});
SpyObject.stub(hostView.elementInjectors[1], { SpyObject.stub(hostView.elementInjectors[1], {
'getHostActionAccessors': [],
'getEventEmitterAccessors': [[spyEventAccessor2]], 'getEventEmitterAccessors': [[spyEventAccessor2]],
'getDirectiveAtIndex': dir 'getDirectiveAtIndex': dir
}); });
@ -172,6 +176,36 @@ export function main() {
expect(spyEventAccessor2.spy('subscribe')).toHaveBeenCalledWith(hostView, 1, dir); expect(spyEventAccessor2.spy('subscribe')).toHaveBeenCalledWith(hostView, 1, dir);
}); });
it("should set up host action listeners", () => {
var dir = new Object();
var hostPv = createProtoView([
createComponentElBinder(null),
createEmptyElBinder()
]);
var hostView = createView(hostPv);
var spyActionAccessor1 = SpyObject.stub({"subscribe" : null});
SpyObject.stub(hostView.elementInjectors[0], {
'getHostActionAccessors': [[spyActionAccessor1]],
'getEventEmitterAccessors': [],
'getDirectiveAtIndex': dir
});
var spyActionAccessor2 = SpyObject.stub({"subscribe" : null});
SpyObject.stub(hostView.elementInjectors[1], {
'getHostActionAccessors': [[spyActionAccessor2]],
'getEventEmitterAccessors': [],
'getDirectiveAtIndex': dir
});
var shadowView = createView();
utils.attachComponentView(hostView, 0, shadowView);
utils.attachAndHydrateInPlaceHostView(null, null, hostView, createInjector());
expect(spyActionAccessor1.spy('subscribe')).toHaveBeenCalledWith(hostView, 0, dir);
expect(spyActionAccessor2.spy('subscribe')).toHaveBeenCalledWith(hostView, 1, dir);
});
}); });
describe('attachViewInContainer', () => { describe('attachViewInContainer', () => {

View File

@ -25,7 +25,8 @@ export function main() {
someDirectiveWithHostProperties, someDirectiveWithHostProperties,
someDirectiveWithHostAttributes, someDirectiveWithHostAttributes,
someDirectiveWithEvents, someDirectiveWithEvents,
someDirectiveWithGlobalEvents someDirectiveWithGlobalEvents,
someDirectiveWithHostActions
]; ];
parser = new Parser(new Lexer()); parser = new Parser(new Lexer());
}); });
@ -171,6 +172,14 @@ export function main() {
expect(eventBinding.source.source).toEqual('doItGlobal()'); expect(eventBinding.source.source).toEqual('doItGlobal()');
}); });
it('should bind directive host actions', () => {
var results = process(
el('<div some-decor-host-actions></div>')
);
var directiveBinding = results[0].directives[0];
expect(directiveBinding.hostActions[0].actionName).toEqual('focus');
});
//TODO: assertions should be enabled when running tests: https://github.com/angular/angular/issues/1340 //TODO: assertions should be enabled when running tests: https://github.com/angular/angular/issues/1340
describe('component directives', () => { describe('component directives', () => {
it('should save the component id', () => { it('should save the component id', () => {
@ -276,6 +285,13 @@ var someDirectiveWithEvents = new DirectiveMetadata({
}) })
}); });
var someDirectiveWithHostActions = new DirectiveMetadata({
selector: '[some-decor-host-actions]',
hostActions: MapWrapper.createFromStringMap({
'focus': 'focus()'
})
});
var someDirectiveWithGlobalEvents = new DirectiveMetadata({ var someDirectiveWithGlobalEvents = new DirectiveMetadata({
selector: '[some-decor-globalevents]', selector: '[some-decor-globalevents]',
hostListeners: MapWrapper.createFromStringMap({ hostListeners: MapWrapper.createFromStringMap({

View File

@ -2,7 +2,7 @@ import {MapWrapper} from 'angular2/src/facade/collection';
import {DirectiveMetadata} from 'angular2/src/render/api'; import {DirectiveMetadata} from 'angular2/src/render/api';
import {directiveMetadataFromMap, directiveMetadataToMap} from import {directiveMetadataFromMap, directiveMetadataToMap} from
'angular2/src/render/dom/convert'; 'angular2/src/render/dom/convert';
import {describe, expect, it} from 'angular2/test_lib'; import {ddescribe, describe, expect, it} from 'angular2/test_lib';
export function main() { export function main() {
describe('convert', () => { describe('convert', () => {
@ -12,6 +12,8 @@ export function main() {
hostListeners: MapWrapper.createFromPairs([['listenKey', 'listenVal']]), hostListeners: MapWrapper.createFromPairs([['listenKey', 'listenVal']]),
hostProperties: hostProperties:
MapWrapper.createFromPairs([['hostPropKey', 'hostPropVal']]), MapWrapper.createFromPairs([['hostPropKey', 'hostPropVal']]),
hostActions:
MapWrapper.createFromPairs([['hostActionKey', 'hostActionVal']]),
id: 'someComponent', id: 'someComponent',
properties: MapWrapper.createFromPairs([['propKey', 'propVal']]), properties: MapWrapper.createFromPairs([['propKey', 'propVal']]),
readAttributes: ['read1', 'read2'], readAttributes: ['read1', 'read2'],
@ -24,6 +26,8 @@ export function main() {
MapWrapper.createFromPairs([['listenKey', 'listenVal']])); MapWrapper.createFromPairs([['listenKey', 'listenVal']]));
expect(MapWrapper.get(map, 'hostProperties')).toEqual( expect(MapWrapper.get(map, 'hostProperties')).toEqual(
MapWrapper.createFromPairs([['hostPropKey', 'hostPropVal']])); MapWrapper.createFromPairs([['hostPropKey', 'hostPropVal']]));
expect(MapWrapper.get(map, 'hostActions')).toEqual(
MapWrapper.createFromPairs([['hostActionKey', 'hostActionVal']]));
expect(MapWrapper.get(map, 'id')).toEqual('someComponent'); expect(MapWrapper.get(map, 'id')).toEqual('someComponent');
expect(MapWrapper.get(map, 'properties')).toEqual( expect(MapWrapper.get(map, 'properties')).toEqual(
MapWrapper.createFromPairs([['propKey', 'propVal']])); MapWrapper.createFromPairs([['propKey', 'propVal']]));
@ -39,6 +43,8 @@ export function main() {
['hostListeners', MapWrapper.createFromPairs([['testKey', 'testVal']])], ['hostListeners', MapWrapper.createFromPairs([['testKey', 'testVal']])],
['hostProperties', ['hostProperties',
MapWrapper.createFromPairs([['hostPropKey', 'hostPropVal']])], MapWrapper.createFromPairs([['hostPropKey', 'hostPropVal']])],
['hostActions',
MapWrapper.createFromPairs([['hostActionKey', 'hostActionVal']])],
['id', 'testId'], ['id', 'testId'],
['properties', MapWrapper.createFromPairs([['propKey', 'propVal']])], ['properties', MapWrapper.createFromPairs([['propKey', 'propVal']])],
['readAttributes', ['readTest1', 'readTest2']], ['readAttributes', ['readTest1', 'readTest2']],
@ -51,6 +57,8 @@ export function main() {
MapWrapper.createFromPairs([['testKey', 'testVal']])); MapWrapper.createFromPairs([['testKey', 'testVal']]));
expect(meta.hostProperties).toEqual( expect(meta.hostProperties).toEqual(
MapWrapper.createFromPairs([['hostPropKey', 'hostPropVal']])); MapWrapper.createFromPairs([['hostPropKey', 'hostPropVal']]));
expect(meta.hostActions).toEqual(
MapWrapper.createFromPairs([['hostActionKey', 'hostActionVal']]));
expect(meta.id).toEqual('testId'); expect(meta.id).toEqual('testId');
expect(meta.properties).toEqual( expect(meta.properties).toEqual(
MapWrapper.createFromPairs([['propKey', 'propVal']])); MapWrapper.createFromPairs([['propKey', 'propVal']]));

View File

@ -96,6 +96,25 @@ export function main() {
}); });
})); }));
it('should call actions on the element',
inject([AsyncTestCompleter, DomTestbed], (async, tb) => {
tb.compileAll([someComponent,
new ViewDefinition({
componentId: 'someComponent',
template: '<div with-host-actions></div>',
directives: [directiveWithHostActions]
})
]).then( (protoViewDtos) => {
var views = tb.createRootViews(protoViewDtos);
var componentView = views[1];
tb.renderer.callAction(componentView.viewRef, 0, 'setAttribute("key", "value")', null);
expect(DOM.getOuterHTML(tb.rootEl)).toContain('key="value"');
async.done();
});
}));
it('should add and remove views to and from containers', it('should add and remove views to and from containers',
inject([AsyncTestCompleter, DomTestbed], (async, tb) => { inject([AsyncTestCompleter, DomTestbed], (async, tb) => {
tb.compileAll([someComponent, tb.compileAll([someComponent,
@ -152,3 +171,12 @@ var someComponent = new DirectiveMetadata({
type: DirectiveMetadata.COMPONENT_TYPE, type: DirectiveMetadata.COMPONENT_TYPE,
selector: 'some-comp' selector: 'some-comp'
}); });
var directiveWithHostActions = new DirectiveMetadata({
id: 'withHostActions',
type: DirectiveMetadata.DIRECTIVE_TYPE,
selector: '[with-host-actions]',
hostActions: MapWrapper.createFromStringMap({
'setAttr' : 'setAttribute("key", "value")'
})
});

View File

@ -6,6 +6,7 @@
"hostListeners": {}, "hostListeners": {},
"hostProperties": {}, "hostProperties": {},
"hostAttributes": {}, "hostAttributes": {},
"hostActions": null,
"properties": {}, "properties": {},
"readAttributes": [], "readAttributes": [],
"type": 1, "type": 1,