Skip to content

Commit 3a74faf

Browse files
authored
Implement a native Angular events strategy for DevExtreme EventsMixin (#332)
* Implement a native Angular events strategy for DevExtreme EventsMixin * Correct context in events
1 parent 278202a commit 3a74faf

File tree

5 files changed

+170
-39
lines changed

5 files changed

+170
-39
lines changed

src/core/component.ts

Lines changed: 14 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66

77
import { DxTemplateDirective } from './template';
88
import { DxTemplateHost } from './template-host';
9+
import { EmitterHelper } from './events-strategy';
910
import { WatcherHelper } from './watcher-helper';
1011
import {
1112
INestedOptionContainer,
@@ -14,11 +15,10 @@ import {
1415
CollectionNestedOptionContainerImpl
1516
} from './nested-option';
1617

17-
const startupEvents = ['onInitialized', 'onContentReady', 'onToolbarPreparing'];
18-
1918
export abstract class DxComponent implements INestedOptionContainer, ICollectionNestedOptionContainer {
2019
private _initialOptions: any;
2120
private _collectionContainerImpl: ICollectionNestedOptionContainer;
21+
eventHelper: EmitterHelper;
2222
templates: DxTemplateDirective[];
2323
instance: any;
2424

@@ -34,34 +34,18 @@ export abstract class DxComponent implements INestedOptionContainer, ICollection
3434
}
3535
}
3636
private _initOptions() {
37-
startupEvents.forEach(eventName => {
38-
this._initialOptions[eventName] = (e) => {
39-
let emitter = this[eventName];
40-
return emitter && emitter.emit(e);
41-
};
42-
});
43-
37+
this._initialOptions.eventsStrategy = this.eventHelper.strategy;
4438
this._initialOptions.integrationOptions.watchMethod = this.watcherHelper.getWatchMethod();
4539
}
40+
protected _createEventEmitters(events) {
41+
events.forEach(event => {
42+
this.eventHelper.createEmitter(event.emit, event.subscribe);
43+
});
44+
}
4645
private _initEvents() {
47-
this._events.forEach(event => {
48-
if (event.subscribe) {
49-
this.instance.on(event.subscribe, e => {
50-
if (event.subscribe === 'optionChanged') {
51-
let changeEventName = e.name + 'Change';
52-
if (this[changeEventName]) {
53-
this[changeEventName].emit(e.value);
54-
}
55-
this[event.emit].emit(e);
56-
} else {
57-
if (this[event.emit]) {
58-
this.ngZone.run(() => {
59-
this[event.emit].emit(e);
60-
});
61-
}
62-
}
63-
});
64-
}
46+
this.instance.on('optionChanged', e => {
47+
let changeEventName = e.name + 'Change';
48+
this.eventHelper.fireNgEvent(changeEventName, [e.value]);
6549
});
6650
}
6751
protected _getOption(name: string) {
@@ -80,7 +64,6 @@ export abstract class DxComponent implements INestedOptionContainer, ICollection
8064
}
8165
protected abstract _createInstance(element, options)
8266
protected _createWidget(element: any) {
83-
this._initialOptions.integrationOptions = {};
8467
this._initTemplates();
8568
this._initOptions();
8669
this.instance = this._createInstance(element, this._initialOptions);
@@ -91,11 +74,12 @@ export abstract class DxComponent implements INestedOptionContainer, ICollection
9174
this.instance.element().triggerHandler({ type: 'dxremove', _angularIntegration: true });
9275
}
9376
}
94-
constructor(protected element: ElementRef, private ngZone: NgZone, templateHost: DxTemplateHost, private watcherHelper: WatcherHelper) {
95-
this._initialOptions = {};
77+
constructor(protected element: ElementRef, ngZone: NgZone, templateHost: DxTemplateHost, private watcherHelper: WatcherHelper) {
78+
this._initialOptions = { integrationOptions: {} };
9679
this.templates = [];
9780
templateHost.setHost(this);
9881
this._collectionContainerImpl = new CollectionNestedOptionContainerImpl(this._setOption.bind(this));
82+
this.eventHelper = new EmitterHelper(ngZone, this);
9983
}
10084
setTemplate(template: DxTemplateDirective) {
10185
this.templates.push(template);
@@ -112,4 +96,3 @@ export abstract class DxComponentExtension extends DxComponent {
11296
}
11397

11498

115-

src/core/events-strategy.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { EventEmitter, NgZone } from '@angular/core';
2+
import { DxComponent } from './component';
3+
4+
const dxToNgEventNames = {};
5+
const nullEmitter = new EventEmitter<any>();
6+
7+
interface EventSubscriber {
8+
handler: any;
9+
unsubscribe: () => void;
10+
}
11+
12+
export class NgEventsStrategy {
13+
private subscribers: { [key: string]: EventSubscriber[] } = {};
14+
15+
constructor(private ngZone: NgZone, private component: DxComponent) { }
16+
17+
hasEvent(name: string) {
18+
let emitter = this.getEmitter(name);
19+
return emitter !== nullEmitter && emitter.observers.length;
20+
}
21+
22+
fireEvent(name, args) {
23+
this.ngZone.run(() => {
24+
this.getEmitter(name).next(args && args[0]);
25+
});
26+
}
27+
28+
on(name, handler) {
29+
let eventSubscribers = this.subscribers[name] || [],
30+
subsriber = this.getEmitter(name).subscribe(handler.bind(this.component.instance)),
31+
unsubscribe = subsriber.unsubscribe.bind(subsriber);
32+
33+
eventSubscribers.push({ handler, unsubscribe });
34+
this.subscribers[name] = eventSubscribers;
35+
}
36+
37+
off(name, handler) {
38+
let eventSubscribers = this.subscribers[name] || [];
39+
eventSubscribers
40+
.filter(i => !handler || i.handler === handler)
41+
.forEach(i => i.unsubscribe());
42+
}
43+
44+
dispose() {}
45+
46+
private getEmitter(eventName: string): EventEmitter<any> {
47+
return this.component[dxToNgEventNames[eventName]] || nullEmitter;
48+
}
49+
}
50+
51+
export class EmitterHelper {
52+
strategy: NgEventsStrategy;
53+
54+
constructor(ngZone: NgZone, private component: DxComponent) {
55+
this.strategy = new NgEventsStrategy(ngZone, component);
56+
}
57+
fireNgEvent(eventName: string, eventArgs: any) {
58+
let emitter = this.component[eventName];
59+
if (emitter) {
60+
emitter.next(eventArgs && eventArgs[0]);
61+
}
62+
}
63+
createEmitter(ngEventName: string, dxEventName: string) {
64+
this.component[ngEventName] = new EventEmitter();
65+
if (dxEventName) {
66+
dxToNgEventNames[dxEventName] = ngEventName;
67+
}
68+
}
69+
}

templates/component.tst

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
Component,
1212
NgModule,
1313
ElementRef,
14-
EventEmitter,
1514
NgZone,
1615
Input,
1716
Output,
@@ -76,7 +75,7 @@ export class <#= it.className #>Component extends <#= baseClass #> <#? implement
7675

7776
<#?#><#~#>
7877

79-
<#~ it.events :event:i #>@Output() <#= event.emit #> = new EventEmitter<any>();<#? i < it.events.length-1 #>
78+
<#~ it.events :event:i #>@Output() <#= event.emit #>;<#? i < it.events.length-1 #>
8079
<#?#><#~#>
8180

8281
<#~ collectionNestedComponents :component:i #>
@@ -95,10 +94,10 @@ export class <#= it.className #>Component extends <#= baseClass #> <#? implement
9594

9695
super(elementRef, ngZone, templateHost, _watcherHelper);
9796

98-
this._events = [
97+
this._createEventEmitters([
9998
<#~ it.events :event:i #>{ <#? event.subscribe #>subscribe: '<#= event.subscribe #>', <#?#>emit: '<#= event.emit #>' }<#? i < it.events.length-1 #>,
10099
<#?#><#~#>
101-
];<#? collectionProperties.length #>
100+
]);<#? collectionProperties.length #>
102101

103102
this._idh.setHost(this);<#?#>
104103
optionHost.setHost(this);

tests/src/core/component.spec.ts

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,13 @@ export class DxTestWidgetComponent extends DxComponent implements AfterViewInit,
5656
constructor(elementRef: ElementRef, ngZone: NgZone, templateHost: DxTemplateHost, _watcherHelper: WatcherHelper) {
5757
super(elementRef, ngZone, templateHost, _watcherHelper);
5858

59-
this._events = [
59+
this._createEventEmitters([
6060
{ subscribe: 'optionChanged', emit: 'onOptionChanged' },
6161
{ subscribe: 'initialized', emit: 'onInitialized' },
6262
{ subscribe: 'disposing', emit: 'onDisposing' },
6363
{ subscribe: 'contentReady', emit: 'onContentReady' },
6464
{ emit: 'testOptionChange' }
65-
];
65+
]);
6666
}
6767

6868
protected _createInstance(element, options) {
@@ -156,6 +156,24 @@ describe('DevExtreme Angular 2 widget', () => {
156156

157157
}));
158158

159+
it('should emit testOptionChange event', async(() => {
160+
TestBed.overrideComponent(TestContainerComponent, {
161+
set: {
162+
template: '<dx-test-widget [testOption]="\'Test Value\'" (testOptionChange)="testMethod()"></dx-test-widget>'
163+
}
164+
});
165+
let fixture = TestBed.createComponent(TestContainerComponent);
166+
fixture.detectChanges();
167+
168+
let component = fixture.componentInstance,
169+
instance = getWidget(fixture),
170+
testSpy = spyOn(component, 'testMethod');
171+
172+
instance.option('testOption', 'new value');
173+
fixture.detectChanges();
174+
expect(testSpy).toHaveBeenCalledTimes(1);
175+
}));
176+
159177
it('should change component option value', async(() => {
160178
let fixture = TestBed.createComponent(DxTestWidgetComponent);
161179
fixture.detectChanges();
@@ -251,4 +269,66 @@ describe('DevExtreme Angular 2 widget', () => {
251269

252270
}));
253271

272+
it('should unsubscribe events', async(() => {
273+
TestBed.overrideComponent(TestContainerComponent, {
274+
set: {
275+
template: '<dx-test-widget></dx-test-widget>'
276+
}
277+
});
278+
279+
let fixture = TestBed.createComponent(TestContainerComponent);
280+
fixture.detectChanges();
281+
282+
let instance = getWidget(fixture),
283+
spy = jasmine.createSpy('spy');
284+
285+
instance.on('optionChanged', spy);
286+
instance.off('optionChanged', spy);
287+
288+
instance.option('testOption', 'new value');
289+
fixture.detectChanges();
290+
291+
expect(spy).toHaveBeenCalledTimes(0);
292+
}));
293+
294+
it('should unsubscribe all events', async(() => {
295+
TestBed.overrideComponent(TestContainerComponent, {
296+
set: {
297+
template: '<dx-test-widget></dx-test-widget>'
298+
}
299+
});
300+
301+
let fixture = TestBed.createComponent(TestContainerComponent);
302+
fixture.detectChanges();
303+
304+
let instance = getWidget(fixture),
305+
spy = jasmine.createSpy('spy');
306+
307+
instance.on('optionChanged', spy);
308+
instance.off('optionChanged');
309+
310+
instance.option('testOption', 'new value');
311+
fixture.detectChanges();
312+
313+
expect(spy).toHaveBeenCalledTimes(0);
314+
}));
315+
316+
it('should have correct context in events', async(() => {
317+
TestBed.overrideComponent(TestContainerComponent, {
318+
set: {
319+
template: '<dx-test-widget></dx-test-widget>'
320+
}
321+
});
322+
323+
let fixture = TestBed.createComponent(TestContainerComponent);
324+
fixture.detectChanges();
325+
326+
let instance = getWidget(fixture);
327+
328+
instance.on('optionChanged', function() {
329+
expect(this).toBe(instance);
330+
});
331+
instance.option('testOption', 'new value');
332+
}));
333+
254334
});

tests/src/core/template.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,10 @@ export class DxTestWidgetComponent extends DxComponent implements AfterViewInit
5454
constructor(elementRef: ElementRef, ngZone: NgZone, templateHost: DxTemplateHost, _watcherHelper: WatcherHelper) {
5555
super(elementRef, ngZone, templateHost, _watcherHelper);
5656

57-
this._events = [
57+
this._createEventEmitters([
5858
{ subscribe: 'optionChanged', emit: 'onOptionChanged' },
5959
{ subscribe: 'initialized', emit: 'onInitialized' }
60-
];
60+
]);
6161
}
6262

6363
protected _createInstance(element, options) {

0 commit comments

Comments
 (0)