Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create event emitters for nested components #728

Merged
merged 1 commit into from
Mar 29, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
443 changes: 382 additions & 61 deletions metadata/NGMetaData.json

Large diffs are not rendered by default.

4 changes: 1 addition & 3 deletions src/core/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,7 @@ export abstract class DxComponent implements OnChanges, OnInit, DoCheck, AfterCo
}

protected _createEventEmitters(events) {
events.forEach(event => {
this.eventHelper.createEmitter(event.emit, event.subscribe);
});
this.eventHelper.createEventEmitters(events);
}
_shouldOptionChange(name: string, value: any) {
if (this.changedOptions.hasOwnProperty(name)) {
Expand Down
10 changes: 8 additions & 2 deletions src/core/events-strategy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { EventEmitter, NgZone, Injectable } from '@angular/core';
import { DxComponent } from './component';
import * as eventsEngine from 'devextreme/events/core/events_engine';
import { BaseNestedOption } from './nested-option';

const dxToNgEventNames = {};

Expand All @@ -12,7 +13,7 @@ interface IEventSubscription {
export class NgEventsStrategy {
private subscriptions: { [key: string]: IEventSubscription[] } = {};

constructor(private component: DxComponent, private ngZone: NgZone) { }
constructor(private component: DxComponent | BaseNestedOption, private ngZone: NgZone) { }

hasEvent(name: string) {
return this.ngZone.run(() => {
Expand Down Expand Up @@ -69,7 +70,7 @@ export class EmitterHelper {
strategy: NgEventsStrategy;
lockedValueChangeEvent = false;

constructor(ngZone: NgZone, public component: DxComponent) {
constructor(ngZone: NgZone, public component: DxComponent | BaseNestedOption) {
this.strategy = new NgEventsStrategy(component, ngZone);
}
fireNgEvent(eventName: string, eventArgs: any) {
Expand All @@ -87,6 +88,11 @@ export class EmitterHelper {
dxToNgEventNames[dxEventName] = ngEventName;
}
}
createEventEmitters(events: Array<any>) {
events.forEach(event => {
this.createEmitter(event.emit, event.subscribe);
});
}
}

@Injectable()
Expand Down
53 changes: 50 additions & 3 deletions src/core/nested-option.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { QueryList, ElementRef, Renderer2 } from '@angular/core';
import { QueryList, ElementRef, Renderer2, NgZone, AfterContentInit } from '@angular/core';
import { ɵgetDOM as getDOM } from '@angular/platform-browser';

import { DX_TEMPLATE_WRAPPER_CLASS } from './template';
import { getElement } from './utils';
import { EmitterHelper } from './events-strategy';

import * as events from 'devextreme/events';

Expand All @@ -15,17 +16,27 @@ export interface INestedOptionContainer {

export interface IOptionPathGetter { (): string; }

export abstract class BaseNestedOption implements INestedOptionContainer, ICollectionNestedOptionContainer {
export abstract class BaseNestedOption implements INestedOptionContainer, ICollectionNestedOptionContainer, AfterContentInit {
protected _host: INestedOptionContainer;
protected _hostOptionPath: IOptionPathGetter;
private _collectionContainerImpl: ICollectionNestedOptionContainer;
protected _initialOptions = {};
eventHelper: EmitterHelper;

protected abstract get _optionPath(): string;
protected abstract _fullOptionPath(): string;
protected abstract _initEvents(): void;

constructor() {
constructor(ngZone: NgZone) {
this._collectionContainerImpl = new CollectionNestedOptionContainerImpl(this._setOption.bind(this), this._filterItems.bind(this));
this.eventHelper = new EmitterHelper(ngZone, this);
}

ngAfterContentInit() {
this._initEvents();
}
protected _createEventEmitters(events) {
this.eventHelper.createEventEmitters(events);
}

protected _getOption(name: string): any {
Expand Down Expand Up @@ -102,6 +113,19 @@ export abstract class NestedOption extends BaseNestedOption {
protected _fullOptionPath() {
return this._hostOptionPath() + this._optionPath + '.';
}

protected _initEvents() {
this.instance.on('optionChanged', (e) => {
let nameParts = e.fullName.split('.');

for (let i = 0; i <= nameParts.length - 2; i++ ) {
if (nameParts[i] === this._optionPath) {
this.eventHelper.fireNgEvent(nameParts[i + 1] + 'Change', [e.value]);
return;
}
};
});
}
}

export interface ICollectionNestedOption {
Expand All @@ -123,6 +147,29 @@ export abstract class CollectionNestedOption extends BaseNestedOption implements
get isLinked() {
return this._index !== undefined && !!this.instance;
}

protected _initEvents() {
if (!this.instance) {
return;
}

this.instance.on('optionChanged', (e) => {
let nameParts = e.fullName.split('.');

for (let i = 0; i <= nameParts.length - 2; i++ ) {
if (nameParts[i].indexOf('[') !== -1) {
let parts = nameParts[i].split('['),
name = parts[0],
index = parts[1].split(']')[0];

if (name === this._optionPath && +index === this._index) {
this.eventHelper.fireNgEvent(nameParts[i + 1] + 'Change', [e.value]);
return;
}
}
};
});
}
}

export interface IOptionWithTemplate extends BaseNestedOption {
Expand Down
21 changes: 18 additions & 3 deletions templates/nested-component.tst
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@
import {
Component,
NgModule,
NgZone,
Host,<#? it.hasTemplate #>
ElementRef,
Renderer2,
Inject,
AfterViewInit,<#?#>
SkipSelf<#? it.properties #>,
Input<#?#><#? it.collectionNestedComponents.length #>,
Input<#?#><#? it.events #>,
Output,
EventEmitter<#?#><#? it.collectionNestedComponents.length #>,
ContentChildren,
forwardRef,
QueryList<#?#>
Expand Down Expand Up @@ -47,6 +50,12 @@ export class <#= it.className #>Component extends <#= it.baseClass #><#? it.hasT
this._setOption('<#= prop.name #>', value);
}
<#~#>
<#~ it.events :event:i #><#? event.description #>
/**
* <#= event.description #>
*/<#?#>
@Output() <#= event.emit #>: <#= event.type #>;<#? i < it.events.length-1 #>
<#?#><#~#>
protected get _optionPath() {
return '<#= it.optionName #>';
}
Expand All @@ -60,13 +69,19 @@ export class <#= it.className #>Component extends <#= it.baseClass #><#? it.hasT
this.setChildren('<#= component.propertyName #>', value);
}
<#~#>
constructor(@SkipSelf() @Host() parentOptionHost: NestedOptionHost,
constructor(ngZone: NgZone, @SkipSelf() @Host() parentOptionHost: NestedOptionHost,
@Host() optionHost: NestedOptionHost<#? it.hasTemplate #>,
private renderer: Renderer2,
@Inject(DOCUMENT) private document: any,
@Host() templateHost: DxTemplateHost,
private element: ElementRef<#?#>) {
super();
super(ngZone);<#? it.events #>

this._createEventEmitters([
<#~ it.events :event:i #>{ emit: '<#= event.emit #>' }<#? i < it.events.length-1 #>,
<#?#><#~#>
]);
<#?#>
parentOptionHost.setNestedOption(this);
optionHost.setHost(this, this._fullOptionPath.bind(this));<#? it.hasTemplate #>
templateHost.setHost(this);<#?#><#? it.optionName === 'dataSource' #>
Expand Down
77 changes: 71 additions & 6 deletions tests/src/core/nested-option.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,18 @@ export class DxoTestOptionComponent extends NestedOption {
this._setOption('testNestedOption', value);
}

@Output() testNestedOptionChange: EventEmitter<any>;

protected get _optionPath() {
return 'testOption';
}

constructor(@SkipSelf() @Host() private _pnoh: NestedOptionHost, @Host() private _noh: NestedOptionHost) {
super();
constructor(ngZone: NgZone, @SkipSelf() @Host() private _pnoh: NestedOptionHost, @Host() private _noh: NestedOptionHost) {
super(ngZone);

this._createEventEmitters([
{ emit: 'testNestedOptionChange' }
]);

this._pnoh.setNestedOption(this);
this._noh.setHost(this);
Expand All @@ -93,12 +99,18 @@ export class DxiTestCollectionOptionComponent extends CollectionNestedOption {
this._setOption('testOption', value);
}

@Output() testOptionChange: EventEmitter<any>;

protected get _optionPath() {
return 'testCollectionOption';
}

constructor(@SkipSelf() @Host() private _pnoh: NestedOptionHost, @Host() private _noh: NestedOptionHost) {
super();
constructor(ngZone: NgZone, @SkipSelf() @Host() private _pnoh: NestedOptionHost, @Host() private _noh: NestedOptionHost) {
super(ngZone);

this._createEventEmitters([
{ emit: 'testOptionChange' }
]);

this._pnoh.setNestedOption(this);
this._noh.setHost(this, this._fullOptionPath.bind(this));
Expand All @@ -124,12 +136,13 @@ export class DxiTestCollectionOptionWithTemplateComponent extends CollectionNest

shownEventFired = false;

constructor(@SkipSelf() @Host() private _pnoh: NestedOptionHost,
constructor(ngZone: NgZone,
@SkipSelf() @Host() private _pnoh: NestedOptionHost,
@Host() private _noh: NestedOptionHost,
private element: ElementRef,
private renderer: Renderer2,
@Inject(DOCUMENT) private document: any) {
super();
super(ngZone);

this._pnoh.setNestedOption(this);
this._noh.setHost(this, this._fullOptionPath.bind(this));
Expand Down Expand Up @@ -230,6 +243,8 @@ export class DxTestWidgetComponent extends DxComponent {
export class TestContainerComponent {
testOption: string;
@ViewChildren(DxTestWidgetComponent) innerWidgets: QueryList<DxTestWidgetComponent>;

testMethod() {}
}


Expand Down Expand Up @@ -314,6 +329,56 @@ describe('DevExtreme Angular widget', () => {
expect(instance.option('testCollectionOption')[0].testOption).toEqual({ testNestedOption: 'text' });
}));

it('nested option should emit change event', async(() => {
TestBed.overrideComponent(TestContainerComponent, {
set: {
template: `
<dx-test-widget>
<dxo-test-option
[(testNestedOption)]='testOption'
(testNestedOptionChange)='testMethod()'>
</dxo-test-option>
</dx-test-widget>
`
}
});
let fixture = TestBed.createComponent(TestContainerComponent);
fixture.detectChanges();

let testComponent = fixture.componentInstance,
instance = getWidget(fixture),
testSpy = spyOn(testComponent, 'testMethod');

instance.option('testOption.testNestedOption', 'new value');
fixture.detectChanges();
expect(testSpy).toHaveBeenCalledTimes(1);
}));

it('collection nested option should emit change event', async(() => {
TestBed.overrideComponent(TestContainerComponent, {
set: {
template: `
<dx-test-widget>
<dxi-test-collection-option
[(testOption)]='testOption'
(testOptionChange)='testMethod()'>
</dxi-test-collection-option>
</dx-test-widget>
`
}
});
let fixture = TestBed.createComponent(TestContainerComponent);
fixture.detectChanges();

let testComponent = fixture.componentInstance,
instance = getWidget(fixture),
testSpy = spyOn(testComponent, 'testMethod');

instance.option('testCollectionOption[0].testOption', 'new value');
fixture.detectChanges();
expect(testSpy).toHaveBeenCalledTimes(1);
}));

it('method template.render of nested option should trigger shownEvent after rendering', async(() => {
TestBed.overrideComponent(TestContainerComponent, {
set: {
Expand Down
Loading