From 35a575609f509d6365adf57953acbb4cbea845a9 Mon Sep 17 00:00:00 2001 From: Andrey Ovchinnikov Date: Fri, 30 Dec 2016 15:21:26 +0300 Subject: [PATCH 1/2] Implement a native Angular events strategy for DevExtreme EventsMixin --- src/core/component.ts | 45 +++++++-------------- src/core/events-strategy.ts | 69 ++++++++++++++++++++++++++++++++ templates/component.tst | 7 ++-- tests/src/core/component.spec.ts | 67 ++++++++++++++++++++++++++++++- tests/src/core/template.spec.ts | 4 +- 5 files changed, 153 insertions(+), 39 deletions(-) create mode 100644 src/core/events-strategy.ts diff --git a/src/core/component.ts b/src/core/component.ts index 9b8c8dbde..2a942681c 100644 --- a/src/core/component.ts +++ b/src/core/component.ts @@ -6,6 +6,7 @@ import { import { DxTemplateDirective } from './template'; import { DxTemplateHost } from './template-host'; +import { EmitterHelper } from './events-strategy'; import { WatcherHelper } from './watcher-helper'; import { INestedOptionContainer, @@ -14,11 +15,10 @@ import { CollectionNestedOptionContainerImpl } from './nested-option'; -const startupEvents = ['onInitialized', 'onContentReady', 'onToolbarPreparing']; - export abstract class DxComponent implements INestedOptionContainer, ICollectionNestedOptionContainer { private _initialOptions: any; private _collectionContainerImpl: ICollectionNestedOptionContainer; + eventHelper: EmitterHelper; templates: DxTemplateDirective[]; instance: any; @@ -34,34 +34,18 @@ export abstract class DxComponent implements INestedOptionContainer, ICollection } } private _initOptions() { - startupEvents.forEach(eventName => { - this._initialOptions[eventName] = (e) => { - let emitter = this[eventName]; - return emitter && emitter.emit(e); - }; - }); - + this._initialOptions.eventsStrategy = this.eventHelper.strategy; this._initialOptions.integrationOptions.watchMethod = this.watcherHelper.getWatchMethod(); } + protected _createEventEmitters(events) { + events.forEach(event => { + this.eventHelper.createEmitter(event.emit, event.subscribe); + }); + } private _initEvents() { - this._events.forEach(event => { - if (event.subscribe) { - this.instance.on(event.subscribe, e => { - if (event.subscribe === 'optionChanged') { - let changeEventName = e.name + 'Change'; - if (this[changeEventName]) { - this[changeEventName].emit(e.value); - } - this[event.emit].emit(e); - } else { - if (this[event.emit]) { - this.ngZone.run(() => { - this[event.emit].emit(e); - }); - } - } - }); - } + this.instance.on('optionChanged', e => { + let changeEventName = e.name + 'Change'; + this.eventHelper.fireNgEvent(changeEventName, [e.value]); }); } protected _getOption(name: string) { @@ -80,7 +64,6 @@ export abstract class DxComponent implements INestedOptionContainer, ICollection } protected abstract _createInstance(element, options) protected _createWidget(element: any) { - this._initialOptions.integrationOptions = {}; this._initTemplates(); this._initOptions(); this.instance = this._createInstance(element, this._initialOptions); @@ -91,11 +74,12 @@ export abstract class DxComponent implements INestedOptionContainer, ICollection this.instance._dispose(); } } - constructor(protected element: ElementRef, private ngZone: NgZone, templateHost: DxTemplateHost, private watcherHelper: WatcherHelper) { - this._initialOptions = {}; + constructor(protected element: ElementRef, ngZone: NgZone, templateHost: DxTemplateHost, private watcherHelper: WatcherHelper) { + this._initialOptions = { integrationOptions: {} }; this.templates = []; templateHost.setHost(this); this._collectionContainerImpl = new CollectionNestedOptionContainerImpl(this._setOption.bind(this)); + this.eventHelper = new EmitterHelper(ngZone, this); } setTemplate(template: DxTemplateDirective) { this.templates.push(template); @@ -112,4 +96,3 @@ export abstract class DxComponentExtension extends DxComponent { } - diff --git a/src/core/events-strategy.ts b/src/core/events-strategy.ts new file mode 100644 index 000000000..cfcd81add --- /dev/null +++ b/src/core/events-strategy.ts @@ -0,0 +1,69 @@ +import { EventEmitter, NgZone } from '@angular/core'; +import { DxComponent } from './component'; + +const dxToNgEventNames = {}; +const nullEmitter = new EventEmitter(); + +interface EventSubscriber { + handler: any; + unsubscribe: () => void; +} + +export class NgEventsStrategy { + private subscribers: { [key: string]: EventSubscriber[] } = {}; + + constructor(private ngZone: NgZone, private component: DxComponent) { } + + hasEvent(name: string) { + let emitter = this.getEmitter(name); + return emitter !== nullEmitter && emitter.observers.length; + } + + fireEvent(name, args) { + this.ngZone.run(() => { + this.getEmitter(name).next(args && args[0]); + }); + } + + on(name, handler) { + let eventSubscribers = this.subscribers[name] || [], + subsriber = this.getEmitter(name).subscribe(handler), + unsubscribe = subsriber.unsubscribe.bind(subsriber); + + eventSubscribers.push({ handler, unsubscribe }); + this.subscribers[name] = eventSubscribers; + } + + off(name, handler) { + let eventSubscribers = this.subscribers[name] || []; + eventSubscribers + .filter(i => !handler || i.handler === handler) + .forEach(i => i.unsubscribe()); + } + + dispose() {} + + private getEmitter(eventName: string): EventEmitter { + return this.component[dxToNgEventNames[eventName]] || nullEmitter; + } +} + +export class EmitterHelper { + strategy: NgEventsStrategy; + + constructor(ngZone: NgZone, private component: DxComponent) { + this.strategy = new NgEventsStrategy(ngZone, component); + } + fireNgEvent(eventName: string, eventArgs: any) { + let emitter = this.component[eventName]; + if (emitter) { + emitter.next(eventArgs && eventArgs[0]); + } + } + createEmitter(ngEventName: string, dxEventName: string) { + this.component[ngEventName] = new EventEmitter(); + if (dxEventName) { + dxToNgEventNames[dxEventName] = ngEventName; + } + } +} diff --git a/templates/component.tst b/templates/component.tst index 797fc1714..08a6bdc44 100644 --- a/templates/component.tst +++ b/templates/component.tst @@ -11,7 +11,6 @@ import { Component, NgModule, ElementRef, - EventEmitter, NgZone, Input, Output, @@ -76,7 +75,7 @@ export class <#= it.className #>Component extends <#= baseClass #> <#? implement <#?#><#~#> - <#~ it.events :event:i #>@Output() <#= event.emit #> = new EventEmitter();<#? i < it.events.length-1 #> + <#~ it.events :event:i #>@Output() <#= event.emit #>;<#? i < it.events.length-1 #> <#?#><#~#> <#~ collectionNestedComponents :component:i #> @@ -95,10 +94,10 @@ export class <#= it.className #>Component extends <#= baseClass #> <#? implement super(elementRef, ngZone, templateHost, _watcherHelper); - this._events = [ + this._createEventEmitters([ <#~ it.events :event:i #>{ <#? event.subscribe #>subscribe: '<#= event.subscribe #>', <#?#>emit: '<#= event.emit #>' }<#? i < it.events.length-1 #>, <#?#><#~#> - ];<#? collectionProperties.length #> + ]);<#? collectionProperties.length #> this._idh.setHost(this);<#?#> optionHost.setHost(this); diff --git a/tests/src/core/component.spec.ts b/tests/src/core/component.spec.ts index 789b3f948..0ffe5bfa5 100644 --- a/tests/src/core/component.spec.ts +++ b/tests/src/core/component.spec.ts @@ -56,13 +56,13 @@ export class DxTestWidgetComponent extends DxComponent implements AfterViewInit, constructor(elementRef: ElementRef, ngZone: NgZone, templateHost: DxTemplateHost, _watcherHelper: WatcherHelper) { super(elementRef, ngZone, templateHost, _watcherHelper); - this._events = [ + this._createEventEmitters([ { subscribe: 'optionChanged', emit: 'onOptionChanged' }, { subscribe: 'initialized', emit: 'onInitialized' }, { subscribe: 'disposing', emit: 'onDisposing' }, { subscribe: 'contentReady', emit: 'onContentReady' }, { emit: 'testOptionChange' } - ]; + ]); } protected _createInstance(element, options) { @@ -156,6 +156,24 @@ describe('DevExtreme Angular 2 widget', () => { })); + it('should emit testOptionChange event', async(() => { + TestBed.overrideComponent(TestContainerComponent, { + set: { + template: '' + } + }); + let fixture = TestBed.createComponent(TestContainerComponent); + fixture.detectChanges(); + + let component = fixture.componentInstance, + instance = getWidget(fixture), + testSpy = spyOn(component, 'testMethod'); + + instance.option('testOption', 'new value'); + fixture.detectChanges(); + expect(testSpy).toHaveBeenCalledTimes(1); + })); + it('should change component option value', async(() => { let fixture = TestBed.createComponent(DxTestWidgetComponent); fixture.detectChanges(); @@ -251,4 +269,49 @@ describe('DevExtreme Angular 2 widget', () => { })); + it('should unsubscribe events', async(() => { + TestBed.overrideComponent(TestContainerComponent, { + set: { + template: '' + } + }); + + let fixture = TestBed.createComponent(TestContainerComponent); + fixture.detectChanges(); + + let instance = getWidget(fixture), + spy = jasmine.createSpy('spy'); + + instance.on('optionChanged', spy); + instance.off('optionChanged', spy); + + instance.option('testOption', 'new value'); + fixture.detectChanges(); + + expect(spy).toHaveBeenCalledTimes(0); + })); + + it('should unsubscribe all events', async(() => { + TestBed.overrideComponent(TestContainerComponent, { + set: { + template: '' + } + }); + + let fixture = TestBed.createComponent(TestContainerComponent); + fixture.detectChanges(); + + let instance = getWidget(fixture), + spy = jasmine.createSpy('spy'); + + instance.on('optionChanged', spy); + instance.off('optionChanged'); + + instance.option('testOption', 'new value'); + fixture.detectChanges(); + + expect(spy).toHaveBeenCalledTimes(0); + })); + + }); diff --git a/tests/src/core/template.spec.ts b/tests/src/core/template.spec.ts index 14e990229..16f96283c 100644 --- a/tests/src/core/template.spec.ts +++ b/tests/src/core/template.spec.ts @@ -54,10 +54,10 @@ export class DxTestWidgetComponent extends DxComponent implements AfterViewInit constructor(elementRef: ElementRef, ngZone: NgZone, templateHost: DxTemplateHost, _watcherHelper: WatcherHelper) { super(elementRef, ngZone, templateHost, _watcherHelper); - this._events = [ + this._createEventEmitters([ { subscribe: 'optionChanged', emit: 'onOptionChanged' }, { subscribe: 'initialized', emit: 'onInitialized' } - ]; + ]); } protected _createInstance(element, options) { From a45a07fc2b5b50761bd464c8d2a030af78b249be Mon Sep 17 00:00:00 2001 From: Andrey Ovchinnikov Date: Fri, 13 Jan 2017 10:12:15 +0300 Subject: [PATCH 2/2] Correct context in events --- src/core/events-strategy.ts | 2 +- tests/src/core/component.spec.ts | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/core/events-strategy.ts b/src/core/events-strategy.ts index cfcd81add..8d78ecb2e 100644 --- a/src/core/events-strategy.ts +++ b/src/core/events-strategy.ts @@ -27,7 +27,7 @@ export class NgEventsStrategy { on(name, handler) { let eventSubscribers = this.subscribers[name] || [], - subsriber = this.getEmitter(name).subscribe(handler), + subsriber = this.getEmitter(name).subscribe(handler.bind(this.component.instance)), unsubscribe = subsriber.unsubscribe.bind(subsriber); eventSubscribers.push({ handler, unsubscribe }); diff --git a/tests/src/core/component.spec.ts b/tests/src/core/component.spec.ts index 0ffe5bfa5..947b393b2 100644 --- a/tests/src/core/component.spec.ts +++ b/tests/src/core/component.spec.ts @@ -313,5 +313,22 @@ describe('DevExtreme Angular 2 widget', () => { expect(spy).toHaveBeenCalledTimes(0); })); + it('should have correct context in events', async(() => { + TestBed.overrideComponent(TestContainerComponent, { + set: { + template: '' + } + }); + + let fixture = TestBed.createComponent(TestContainerComponent); + fixture.detectChanges(); + + let instance = getWidget(fixture); + + instance.on('optionChanged', function() { + expect(this).toBe(instance); + }); + instance.option('testOption', 'new value'); + })); });