diff --git a/modules/angular2/src/core/annotations_impl/annotations.js b/modules/angular2/src/core/annotations_impl/annotations.js index 6a5e4a7798..c87c5c1389 100644 --- a/modules/angular2/src/core/annotations_impl/annotations.js +++ b/modules/angular2/src/core/annotations_impl/annotations.js @@ -620,6 +620,33 @@ export class Directive extends Injectable { */ 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. * @@ -641,6 +668,7 @@ export class Directive extends Injectable { hostListeners, hostProperties, hostAttributes, + hostActions, lifecycle, compileChildren = true, }:{ @@ -650,6 +678,7 @@ export class Directive extends Injectable { hostListeners: any, hostProperties: any, hostAttributes: any, + hostActions: any, lifecycle:List, compileChildren:boolean }={}) @@ -661,6 +690,7 @@ export class Directive extends Injectable { this.hostListeners = hostListeners; this.hostProperties = hostProperties; this.hostAttributes = hostAttributes; + this.hostActions = hostActions; this.lifecycle = lifecycle; this.compileChildren = compileChildren; } @@ -858,6 +888,7 @@ export class Component extends Directive { hostListeners, hostProperties, hostAttributes, + hostActions, injectables, lifecycle, changeDetection = DEFAULT, @@ -870,6 +901,7 @@ export class Component extends Directive { hostListeners:any, hostProperties:any, hostAttributes:any, + hostActions:any, injectables:List, lifecycle:List, changeDetection:string, @@ -884,6 +916,7 @@ export class Component extends Directive { hostListeners: hostListeners, hostProperties: hostProperties, hostAttributes: hostAttributes, + hostActions: hostActions, lifecycle: lifecycle, compileChildren: compileChildren }); diff --git a/modules/angular2/src/core/compiler/compiler.js b/modules/angular2/src/core/compiler/compiler.js index 3aaaaa38f1..790fe2b36b 100644 --- a/modules/angular2/src/core/compiler/compiler.js +++ b/modules/angular2/src/core/compiler/compiler.js @@ -233,6 +233,7 @@ export class Compiler { hostListeners: isPresent(ann.hostListeners) ? MapWrapper.createFromStringMap(ann.hostListeners) : null, hostProperties: isPresent(ann.hostProperties) ? MapWrapper.createFromStringMap(ann.hostProperties) : 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, readAttributes: readAttributes }); diff --git a/modules/angular2/src/core/compiler/element_injector.js b/modules/angular2/src/core/compiler/element_injector.js index 69322d81cf..992998eb44 100644 --- a/modules/angular2/src/core/compiler/element_injector.js +++ b/modules/angular2/src/core/compiler/element_injector.js @@ -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 {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, AbstractBindingError, CyclicDependencyError} from 'angular2/di'; 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 : []; } + get hostActions() { //StringMap + return isPresent(this.annotation) && isPresent(this.annotation.hostActions) ? this.annotation.hostActions : {}; + } + get changeDetection() { if (this.annotation instanceof Component) { 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 @@ -346,6 +366,7 @@ export class ProtoElementInjector { distanceToParent:number; attributes:Map; eventEmitterAccessors:List>; + hostActionAccessors:List>; numberOfDirectives:number; @@ -380,56 +401,67 @@ export class ProtoElementInjector { this.numberOfDirectives = bindings.length; var length = bindings.length; this.eventEmitterAccessors = ListWrapper.createFixedSize(length); + this.hostActionAccessors = ListWrapper.createFixedSize(length); if (length > 0) { this._binding0 = this._createBinding(bindings[0]); this._keyId0 = this._binding0.key.id; this.eventEmitterAccessors[0] = this._createEventEmitterAccessors(this._binding0); + this.hostActionAccessors[0] = this._createHostActionAccessors(this._binding0); } if (length > 1) { this._binding1 = this._createBinding(bindings[1]); this._keyId1 = this._binding1.key.id; this.eventEmitterAccessors[1] = this._createEventEmitterAccessors(this._binding1); + this.hostActionAccessors[1] = this._createHostActionAccessors(this._binding1); } if (length > 2) { this._binding2 = this._createBinding(bindings[2]); this._keyId2 = this._binding2.key.id; this.eventEmitterAccessors[2] = this._createEventEmitterAccessors(this._binding2); + this.hostActionAccessors[2] = this._createHostActionAccessors(this._binding2); } if (length > 3) { this._binding3 = this._createBinding(bindings[3]); this._keyId3 = this._binding3.key.id; this.eventEmitterAccessors[3] = this._createEventEmitterAccessors(this._binding3); + this.hostActionAccessors[3] = this._createHostActionAccessors(this._binding3); } if (length > 4) { this._binding4 = this._createBinding(bindings[4]); this._keyId4 = this._binding4.key.id; this.eventEmitterAccessors[4] = this._createEventEmitterAccessors(this._binding4); + this.hostActionAccessors[4] = this._createHostActionAccessors(this._binding4); } if (length > 5) { this._binding5 = this._createBinding(bindings[5]); this._keyId5 = this._binding5.key.id; this.eventEmitterAccessors[5] = this._createEventEmitterAccessors(this._binding5); + this.hostActionAccessors[5] = this._createHostActionAccessors(this._binding5); } if (length > 6) { this._binding6 = this._createBinding(bindings[6]); this._keyId6 = this._binding6.key.id; this.eventEmitterAccessors[6] = this._createEventEmitterAccessors(this._binding6); + this.hostActionAccessors[6] = this._createHostActionAccessors(this._binding6); } if (length > 7) { this._binding7 = this._createBinding(bindings[7]); this._keyId7 = this._binding7.key.id; this.eventEmitterAccessors[7] = this._createEventEmitterAccessors(this._binding7); + this.hostActionAccessors[7] = this._createHostActionAccessors(this._binding7); } if (length > 8) { this._binding8 = this._createBinding(bindings[8]); this._keyId8 = this._binding8.key.id; this.eventEmitterAccessors[8] = this._createEventEmitterAccessors(this._binding8); + this.hostActionAccessors[8] = this._createHostActionAccessors(this._binding8); } if (length > 9) { this._binding9 = this._createBinding(bindings[9]); this._keyId9 = this._binding9.key.id; this.eventEmitterAccessors[9] = this._createEventEmitterAccessors(this._binding9); + this.hostActionAccessors[9] = this._createHostActionAccessors(this._binding9); } if (length > 10) { 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 { return new ElementInjector(this, parent); } @@ -661,6 +701,10 @@ export class ElementInjector extends TreeNode { return this._proto.eventEmitterAccessors; } + getHostActionAccessors() { + return this._proto.hostActionAccessors; + } + getComponent() { if (this._proto._binding0IsComponent) { return this._obj0; diff --git a/modules/angular2/src/core/compiler/view.js b/modules/angular2/src/core/compiler/view.js index 5551b26055..a1a4c5183c 100644 --- a/modules/angular2/src/core/compiler/view.js +++ b/modules/angular2/src/core/compiler/view.js @@ -126,6 +126,10 @@ export class AppView { 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 // returns false if preventDefault must be applied to the DOM event dispatchEvent(elementIndex:number, eventName:string, locals:Map): boolean { diff --git a/modules/angular2/src/core/compiler/view_manager_utils.js b/modules/angular2/src/core/compiler/view_manager_utils.js index e0a2408547..2d7e175f0f 100644 --- a/modules/angular2/src/core/compiler/view_manager_utils.js +++ b/modules/angular2/src/core/compiler/view_manager_utils.js @@ -192,6 +192,7 @@ export class AppViewManagerUtils { if (isPresent(elementInjector)) { elementInjector.instantiateDirectives(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 @@ -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) { var binders = view.proto.elementBinders; for (var i = 0; i < binders.length; ++i) { diff --git a/modules/angular2/src/facade/collection.dart b/modules/angular2/src/facade/collection.dart index f32c8bc593..76df62499d 100644 --- a/modules/angular2/src/facade/collection.dart +++ b/modules/angular2/src/facade/collection.dart @@ -82,6 +82,9 @@ class StringMapWrapper { } return m; } + static List keys(Map a) { + return a.keys.toList(); + } static bool isEmpty(Map m) => m.isEmpty; } diff --git a/modules/angular2/src/facade/collection.ts b/modules/angular2/src/facade/collection.ts index 5609b4ad12..ac684b8bdb 100644 --- a/modules/angular2/src/facade/collection.ts +++ b/modules/angular2/src/facade/collection.ts @@ -53,6 +53,7 @@ export class StringMapWrapper { static contains(map, key) { return map.hasOwnProperty(key); } static get(map, key) { return map.hasOwnProperty(key) ? map[key] : undefined; } static set(map, key, value) { map[key] = value; } + static keys(map) { return Object.keys(map); } static isEmpty(map) { for (var prop in map) { return false; diff --git a/modules/angular2/src/render/api.js b/modules/angular2/src/render/api.js index 9e41578cfd..44570e573f 100644 --- a/modules/angular2/src/render/api.js +++ b/modules/angular2/src/render/api.js @@ -117,16 +117,18 @@ export class DirectiveMetadata { hostListeners:Map; hostProperties:Map; hostAttributes:Map; + hostActions:Map; properties:Map; readAttributes:List; 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.selector = selector; this.compileChildren = isPresent(compileChildren) ? compileChildren : true; this.hostListeners = hostListeners; this.hostProperties = hostProperties; this.hostAttributes = hostAttributes; + this.hostActions = hostActions; this.properties = properties; this.readAttributes = readAttributes; 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. */ - 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 * in the ProtoView */ 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. */ setText(viewRef:RenderViewRef, textNodeIndex:number, text:string):void { diff --git a/modules/angular2/src/render/dom/compiler/directive_parser.js b/modules/angular2/src/render/dom/compiler/directive_parser.js index 9b0b32208b..7114dcacae 100644 --- a/modules/angular2/src/render/dom/compiler/directive_parser.js +++ b/modules/angular2/src/render/dom/compiler/directive_parser.js @@ -73,6 +73,11 @@ export class DirectiveParser extends CompileStep { 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)) { MapWrapper.forEach(directive.hostProperties, (hostPropertyName, directivePropertyName) => { this._bindHostProperty(hostPropertyName, directivePropertyName, current, directiveBinderBuilder); @@ -136,7 +141,11 @@ export class DirectiveParser extends CompileStep { } else { 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) { diff --git a/modules/angular2/src/render/dom/convert.js b/modules/angular2/src/render/dom/convert.js index 9b57c08554..7c290936dc 100644 --- a/modules/angular2/src/render/dom/convert.js +++ b/modules/angular2/src/render/dom/convert.js @@ -14,6 +14,7 @@ export function directiveMetadataToMap(meta: DirectiveMetadata): Map { ['hostListeners', _cloneIfPresent(meta.hostListeners)], ['hostProperties', _cloneIfPresent(meta.hostProperties)], ['hostAttributes', _cloneIfPresent(meta.hostAttributes)], + ['hostActions', _cloneIfPresent(meta.hostActions)], ['properties', _cloneIfPresent(meta.properties)], ['readAttributes', _cloneIfPresent(meta.readAttributes)], ['type', meta.type], @@ -33,6 +34,7 @@ export function directiveMetadataFromMap(map: Map): DirectiveMetadata { compileChildren: MapWrapper.get(map, 'compileChildren'), hostListeners: _cloneIfPresent(MapWrapper.get(map, 'hostListeners')), hostProperties: _cloneIfPresent(MapWrapper.get(map, 'hostProperties')), + hostActions: _cloneIfPresent(MapWrapper.get(map, 'hostActions')), hostAttributes: _cloneIfPresent(MapWrapper.get(map, 'hostAttributes')), properties: _cloneIfPresent(MapWrapper.get(map, 'properties')), readAttributes: _cloneIfPresent(MapWrapper.get(map, 'readAttributes')), diff --git a/modules/angular2/src/render/dom/dom_renderer.js b/modules/angular2/src/render/dom/dom_renderer.js index 921b88a142..983d0b3e14 100644 --- a/modules/angular2/src/render/dom/dom_renderer.js +++ b/modules/angular2/src/render/dom/dom_renderer.js @@ -192,6 +192,11 @@ export class DomRenderer extends Renderer { 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 { var view = resolveInternalDomView(viewRef); DOM.setText(view.boundTextNodes[textNodeIndex], text); diff --git a/modules/angular2/src/render/dom/util.js b/modules/angular2/src/render/dom/util.js index 0ca98b1327..096d954faa 100644 --- a/modules/angular2/src/render/dom/util.js +++ b/modules/angular2/src/render/dom/util.js @@ -18,4 +18,4 @@ export function dashCaseToCamelCase(input:string) { return StringWrapper.replaceAllMapped(input, DASH_CASE_REGEXP, (m) => { return m[1].toUpperCase(); }); -} +} \ No newline at end of file diff --git a/modules/angular2/src/render/dom/view/element_binder.js b/modules/angular2/src/render/dom/view/element_binder.js index d07a5b0c13..48fd170f83 100644 --- a/modules/angular2/src/render/dom/view/element_binder.js +++ b/modules/angular2/src/render/dom/view/element_binder.js @@ -14,6 +14,7 @@ export class ElementBinder { parentIndex:number; distanceToParent:number; propertySetters: Map; + hostActions: Map; constructor({ textNodeIndices, @@ -23,6 +24,7 @@ export class ElementBinder { eventLocals, localEvents, globalEvents, + hostActions, parentIndex, distanceToParent, propertySetters @@ -34,6 +36,7 @@ export class ElementBinder { this.eventLocals = eventLocals; this.localEvents = localEvents; this.globalEvents = globalEvents; + this.hostActions = hostActions; this.parentIndex = parentIndex; this.distanceToParent = distanceToParent; this.propertySetters = propertySetters; @@ -51,3 +54,15 @@ export class Event { 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; + } +} diff --git a/modules/angular2/src/render/dom/view/proto_view_builder.js b/modules/angular2/src/render/dom/view/proto_view_builder.js index d944dd91c3..de76f4530a 100644 --- a/modules/angular2/src/render/dom/view/proto_view_builder.js +++ b/modules/angular2/src/render/dom/view/proto_view_builder.js @@ -7,7 +7,7 @@ import { } from 'angular2/change_detection'; 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 * as api from '../../api'; @@ -48,6 +48,7 @@ export class ProtoViewBuilder { var apiElementBinders = []; ListWrapper.forEach(this.elements, (ebb) => { var propertySetters = MapWrapper.create(); + var hostActions = MapWrapper.create(); var apiDirectiveBinders = ListWrapper.map(ebb.directives, (dbb) => { ebb.eventBuilder.merge(dbb.eventBuilder); @@ -56,6 +57,10 @@ export class ProtoViewBuilder { MapWrapper.set(propertySetters, hostPropertyName, setterFactory(hostPropertyName)); }); + ListWrapper.forEach(dbb.hostActions, (hostAction) => { + MapWrapper.set(hostActions, hostAction.actionExpression, hostAction.expression); + }); + return new api.DirectiveBinder({ directiveIndex: dbb.directiveIndex, propertyBindings: dbb.propertyBindings, @@ -90,6 +95,7 @@ export class ProtoViewBuilder { eventLocals: new LiteralArray(ebb.eventBuilder.buildEventLocals()), localEvents: ebb.eventBuilder.buildLocalEvents(), globalEvents: ebb.eventBuilder.buildGlobalEvents(), + hostActions: hostActions, propertySetters: propertySetters })); }); @@ -213,6 +219,7 @@ export class DirectiveBuilder { directiveIndex:number; propertyBindings: Map; hostPropertyBindings: Map; + hostActions: List; eventBindings: List; eventBuilder: EventBuilder; @@ -220,6 +227,7 @@ export class DirectiveBuilder { this.directiveIndex = directiveIndex; this.propertyBindings = MapWrapper.create(); this.hostPropertyBindings = MapWrapper.create(); + this.hostActions = ListWrapper.create(); this.eventBindings = ListWrapper.create(); this.eventBuilder = new EventBuilder(); } @@ -232,6 +240,10 @@ export class DirectiveBuilder { 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) { ListWrapper.push(this.eventBindings, this.eventBuilder.add(name, expression, target)); } diff --git a/modules/angular2/src/render/dom/view/view.js b/modules/angular2/src/render/dom/view/view.js index bba576cd3e..8ba157dbd6 100644 --- a/modules/angular2/src/render/dom/view/view.js +++ b/modules/angular2/src/render/dom/view/view.js @@ -1,5 +1,6 @@ import {DOM} from 'angular2/src/dom/dom_adapter'; 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 {DomViewContainer} from './view_container'; @@ -80,6 +81,18 @@ export class DomView { 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) { DOM.setText(this.boundTextNodes[textIndex], value); } diff --git a/modules/angular2/test/core/compiler/element_injector_spec.js b/modules/angular2/test/core/compiler/element_injector_spec.js index 8c4a373948..218b215ae6 100644 --- a/modules/angular2/test/core/compiler/element_injector_spec.js +++ b/modules/angular2/test/core/compiler/element_injector_spec.js @@ -17,7 +17,7 @@ import {ViewRef, Renderer} from 'angular2/src/render/api'; import {QueryList} from 'angular2/src/core/compiler/query_list'; class DummyDirective extends Directive { - constructor({lifecycle, events} = {}) { super({lifecycle: lifecycle, events: events}); } + constructor({lifecycle, events, hostActions} = {}) { super({lifecycle: lifecycle, events: events, hostActions:hostActions}); } } @proxy @@ -97,6 +97,13 @@ class HasEventEmitter { } } +class HasHostAction { + hostActionName; + constructor() { + this.hostActionName = "hostAction"; + } +} + class NeedsAttribute { typeAttribute; titleAttribute; @@ -381,7 +388,7 @@ export function main() { }); describe('event emitters', () => { - it('should return a list of event emitter accessors', () => { + it('should return a list of event accessors', () => { var binding = DirectiveBinding.createFromType( HasEventEmitter, new DummyDirective({events: ['emitter']})); @@ -392,6 +399,18 @@ export function main() { expect(accessor.eventName).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'); + }); }); }); diff --git a/modules/angular2/test/core/compiler/integration_spec.js b/modules/angular2/test/core/compiler/integration_spec.js index 585b4a83a7..c683ab33d4 100644 --- a/modules/angular2/test/core/compiler/integration_spec.js +++ b/modules/angular2/test/core/compiler/integration_spec.js @@ -581,15 +581,37 @@ export function main() { expect(listener.msg).toEqual(''); - emitter.fireEvent('fired !'); - - PromiseWrapper.setTimeout(() => { + ObservableWrapper.subscribe(emitter.event, (_) => { expect(listener.msg).toEqual('fired !'); 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: '
', + 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('
'); + async.done(); + }); + + updateHost.triggerSetAttr('value'); + }); + })); + } + it('should support render events', inject([TestBed, AsyncTestCompleter], (tb, async) => { tb.overrideView(MyComp, new View({ template: '
', @@ -671,6 +693,7 @@ export function main() { }); })); + if (DOM.supportsDOMEvents()) { it('should support preventing default on render events', inject([TestBed, AsyncTestCompleter], (tb, async) => { 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({ selector: '[listener]', hostListeners: {'event': 'onEvent($event)'} diff --git a/modules/angular2/test/core/compiler/view_manager_utils_spec.js b/modules/angular2/test/core/compiler/view_manager_utils_spec.js index eec658b6d7..31b852906e 100644 --- a/modules/angular2/test/core/compiler/view_manager_utils_spec.js +++ b/modules/angular2/test/core/compiler/view_manager_utils_spec.js @@ -31,6 +31,7 @@ import {AppViewManagerUtils} from 'angular2/src/core/compiler/view_manager_utils export function main() { // TODO(tbosch): add more tests here! + describe('AppViewManagerUtils', () => { var metadataReader; @@ -71,6 +72,7 @@ export function main() { 'isExportingComponent' : false, 'isExportingElement' : false, 'getEventEmitterAccessors' : [], + 'getHostActionAccessors' : [], 'getComponent' : null, 'getDynamicallyLoadedComponent': null, 'getHost': host @@ -154,11 +156,13 @@ export function main() { var hostView = createView(hostPv); var spyEventAccessor1 = SpyObject.stub({"subscribe" : null}); SpyObject.stub(hostView.elementInjectors[0], { + 'getHostActionAccessors': [], 'getEventEmitterAccessors': [[spyEventAccessor1]], 'getDirectiveAtIndex': dir }); var spyEventAccessor2 = SpyObject.stub({"subscribe" : null}); SpyObject.stub(hostView.elementInjectors[1], { + 'getHostActionAccessors': [], 'getEventEmitterAccessors': [[spyEventAccessor2]], 'getDirectiveAtIndex': dir }); @@ -172,6 +176,36 @@ export function main() { 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', () => { diff --git a/modules/angular2/test/render/dom/compiler/directive_parser_spec.js b/modules/angular2/test/render/dom/compiler/directive_parser_spec.js index 2c40cb342b..b751977da6 100644 --- a/modules/angular2/test/render/dom/compiler/directive_parser_spec.js +++ b/modules/angular2/test/render/dom/compiler/directive_parser_spec.js @@ -25,7 +25,8 @@ export function main() { someDirectiveWithHostProperties, someDirectiveWithHostAttributes, someDirectiveWithEvents, - someDirectiveWithGlobalEvents + someDirectiveWithGlobalEvents, + someDirectiveWithHostActions ]; parser = new Parser(new Lexer()); }); @@ -171,6 +172,14 @@ export function main() { expect(eventBinding.source.source).toEqual('doItGlobal()'); }); + it('should bind directive host actions', () => { + var results = process( + el('
') + ); + 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 describe('component directives', () => { 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({ selector: '[some-decor-globalevents]', hostListeners: MapWrapper.createFromStringMap({ @@ -287,4 +303,4 @@ var componentWithNonElementSelector = new DirectiveMetadata({ id: 'componentWithNonElementSelector', selector: '[attr]', type: DirectiveMetadata.COMPONENT_TYPE -}); +}); \ No newline at end of file diff --git a/modules/angular2/test/render/dom/convert_spec.js b/modules/angular2/test/render/dom/convert_spec.js index b380efc6fc..989ec21463 100644 --- a/modules/angular2/test/render/dom/convert_spec.js +++ b/modules/angular2/test/render/dom/convert_spec.js @@ -2,7 +2,7 @@ import {MapWrapper} from 'angular2/src/facade/collection'; import {DirectiveMetadata} from 'angular2/src/render/api'; import {directiveMetadataFromMap, directiveMetadataToMap} from '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() { describe('convert', () => { @@ -12,6 +12,8 @@ export function main() { hostListeners: MapWrapper.createFromPairs([['listenKey', 'listenVal']]), hostProperties: MapWrapper.createFromPairs([['hostPropKey', 'hostPropVal']]), + hostActions: + MapWrapper.createFromPairs([['hostActionKey', 'hostActionVal']]), id: 'someComponent', properties: MapWrapper.createFromPairs([['propKey', 'propVal']]), readAttributes: ['read1', 'read2'], @@ -24,6 +26,8 @@ export function main() { MapWrapper.createFromPairs([['listenKey', 'listenVal']])); expect(MapWrapper.get(map, 'hostProperties')).toEqual( 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, 'properties')).toEqual( MapWrapper.createFromPairs([['propKey', 'propVal']])); @@ -39,6 +43,8 @@ export function main() { ['hostListeners', MapWrapper.createFromPairs([['testKey', 'testVal']])], ['hostProperties', MapWrapper.createFromPairs([['hostPropKey', 'hostPropVal']])], + ['hostActions', + MapWrapper.createFromPairs([['hostActionKey', 'hostActionVal']])], ['id', 'testId'], ['properties', MapWrapper.createFromPairs([['propKey', 'propVal']])], ['readAttributes', ['readTest1', 'readTest2']], @@ -51,6 +57,8 @@ export function main() { MapWrapper.createFromPairs([['testKey', 'testVal']])); expect(meta.hostProperties).toEqual( MapWrapper.createFromPairs([['hostPropKey', 'hostPropVal']])); + expect(meta.hostActions).toEqual( + MapWrapper.createFromPairs([['hostActionKey', 'hostActionVal']])); expect(meta.id).toEqual('testId'); expect(meta.properties).toEqual( MapWrapper.createFromPairs([['propKey', 'propVal']])); diff --git a/modules/angular2/test/render/dom/dom_renderer_integration_spec.js b/modules/angular2/test/render/dom/dom_renderer_integration_spec.js index 667469955f..5b0d6bfd4c 100644 --- a/modules/angular2/test/render/dom/dom_renderer_integration_spec.js +++ b/modules/angular2/test/render/dom/dom_renderer_integration_spec.js @@ -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: '
', + 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', inject([AsyncTestCompleter, DomTestbed], (async, tb) => { tb.compileAll([someComponent, @@ -152,3 +171,12 @@ var someComponent = new DirectiveMetadata({ type: DirectiveMetadata.COMPONENT_TYPE, selector: 'some-comp' }); + +var directiveWithHostActions = new DirectiveMetadata({ + id: 'withHostActions', + type: DirectiveMetadata.DIRECTIVE_TYPE, + selector: '[with-host-actions]', + hostActions: MapWrapper.createFromStringMap({ + 'setAttr' : 'setAttribute("key", "value")' + }) +}); diff --git a/modules/angular2/test/transform/integration/simple_annotation_files/expected/bar.ng_meta.json b/modules/angular2/test/transform/integration/simple_annotation_files/expected/bar.ng_meta.json index 5abbe1c03b..676376544f 100644 --- a/modules/angular2/test/transform/integration/simple_annotation_files/expected/bar.ng_meta.json +++ b/modules/angular2/test/transform/integration/simple_annotation_files/expected/bar.ng_meta.json @@ -6,6 +6,7 @@ "hostListeners": {}, "hostProperties": {}, "hostAttributes": {}, + "hostActions": null, "properties": {}, "readAttributes": [], "type": 1,