From 2c997810d9185aba6e8089d95fec022097ca977b Mon Sep 17 00:00:00 2001 From: bykov Date: Wed, 20 Sep 2017 19:35:17 +0300 Subject: [PATCH 1/7] Introduce templated content in dxi components (T532675) --- README.md | 22 ++++++++++++++++++++ src/core/component.ts | 4 ++-- src/core/template-host.ts | 9 ++++++--- src/core/template.ts | 2 +- templates/nested-component.tst | 20 ++++++++++++------ tests/src/ui/list.spec.ts | 37 ++++++++++++++++++++++++++++++++++ 6 files changed, 82 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 8e2993ec6..ca98eeff6 100644 --- a/README.md +++ b/README.md @@ -457,6 +457,28 @@ export class AppComponent { } ``` +Note, in some scenarios an item content can be rerendered by it's host component. To keep the item child components alive they should be declared as a single template via the `dxTemplate` structural directive. There are several alternatives depending on the item content: + +```html + + + + + +
+ + +
+
+ + + +
+``` + Angular has a built-in `template` directive. To define the `template` property of the configuration component (for example, `dxo-master-detail`), use the following code: ```html diff --git a/src/core/component.ts b/src/core/component.ts index 02dab62df..95824a64b 100644 --- a/src/core/component.ts +++ b/src/core/component.ts @@ -7,7 +7,7 @@ import { } from '@angular/core'; import { DxTemplateDirective } from './template'; -import { DxTemplateHost } from './template-host'; +import { IDxTemplateHost, DxTemplateHost } from './template-host'; import { EmitterHelper } from './events-strategy'; import { WatcherHelper } from './watcher-helper'; import { @@ -17,7 +17,7 @@ import { CollectionNestedOptionContainerImpl } from './nested-option'; -export abstract class DxComponent implements AfterViewInit, AfterContentChecked, INestedOptionContainer, ICollectionNestedOptionContainer { +export abstract class DxComponent implements AfterViewInit, AfterContentChecked, INestedOptionContainer, ICollectionNestedOptionContainer, IDxTemplateHost { private _optionToUpdate: any = {}; private _collectionContainerImpl: ICollectionNestedOptionContainer; eventHelper: EmitterHelper; diff --git a/src/core/template-host.ts b/src/core/template-host.ts index 0b8858917..cef89c6b8 100644 --- a/src/core/template-host.ts +++ b/src/core/template-host.ts @@ -1,9 +1,12 @@ -import { DxComponent } from './component'; import { DxTemplateDirective } from './template'; +export interface IDxTemplateHost { + setTemplate(DxTemplateDirective); +}; + export class DxTemplateHost { - host: DxComponent; - setHost(host: DxComponent) { + host: IDxTemplateHost; + setHost(host: IDxTemplateHost) { this.host = host; } setTemplate(template: DxTemplateDirective) { diff --git a/src/core/template.ts b/src/core/template.ts index 51581aaf0..880d5257a 100644 --- a/src/core/template.ts +++ b/src/core/template.ts @@ -23,7 +23,7 @@ export class RenderData { } @Directive({ - selector: '[dxTemplate][dxTemplateOf]' + selector: '[dxTemplate]' }) export class DxTemplateDirective { @Input() diff --git a/templates/nested-component.tst b/templates/nested-component.tst index 2afaa7056..82ac113a8 100644 --- a/templates/nested-component.tst +++ b/templates/nested-component.tst @@ -13,7 +13,9 @@ import { QueryList<#?#> } from '@angular/core'; -import { NestedOptionHost<#? it.hasTemplate #>, extractTemplate<#?#> } from '../../core/nested-option'; +import { NestedOptionHost<#? it.hasTemplate #>, extractTemplate<#?#> } from '../../core/nested-option';<#? it.hasTemplate #> +import { DxTemplateDirective } from '../../core/template'; +import { IDxTemplateHost, DxTemplateHost } from '../../core/template-host';<#?#> import { <#= it.baseClass #> } from '<#= it.basePath #>'; <#~ it.collectionNestedComponents :component:i #><#? component.className !== it.className #>import { <#= component.className #>Component } from './<#= component.path #>'; <#?#><#~#> @@ -22,12 +24,12 @@ import { <#= it.baseClass #> } from '<#= it.basePath #>'; selector: '<#= it.selector #>', template: '<#? it.hasTemplate #><#?#>', styles: ['<#? it.hasTemplate #>:host { display: block; }<#?#>'], - providers: [NestedOptionHost]<#? it.inputs #>, + providers: [NestedOptionHost<#? it.hasTemplate #>, DxTemplateHost<#?#>]<#? it.inputs #>, inputs: [<#~ it.inputs :input:i #> '<#= input.name #>'<#? i < it.inputs.length-1 #>,<#?#><#~#> ]<#?#> }) -export class <#= it.className #>Component extends <#= it.baseClass #><#? it.hasTemplate #> implements AfterViewInit<#?#> {<#~ it.properties :prop:i #> +export class <#= it.className #>Component extends <#= it.baseClass #><#? it.hasTemplate #> implements AfterViewInit, IDxTemplateHost<#?#> {<#~ it.properties :prop:i #> @Input() get <#= prop.name #>() { return this._getOption('<#= prop.name #>'); @@ -49,13 +51,19 @@ export class <#= it.className #>Component extends <#= it.baseClass #><#? it.hasT this.setChildren('<#= component.propertyName #>', value); } <#~#> - - constructor(@SkipSelf() @Host() parentOptionHost: NestedOptionHost, @Host() optionHost: NestedOptionHost<#? it.hasTemplate #>, private element: ElementRef<#?#>) { + constructor(@SkipSelf() @Host() parentOptionHost: NestedOptionHost, + @Host() optionHost: NestedOptionHost<#? it.hasTemplate #>, + @Host() templateHost: DxTemplateHost, + private element: ElementRef<#?#>) { super(); parentOptionHost.setNestedOption(this); - optionHost.setHost(this, this._fullOptionPath.bind(this)); + optionHost.setHost(this, this._fullOptionPath.bind(this));<#? it.hasTemplate #> + templateHost.setHost(this);<#?#> } <#? it.hasTemplate #> + setTemplate(template: DxTemplateDirective) { + this.template = template; + } ngAfterViewInit() { extractTemplate(this, this.element); } diff --git a/tests/src/ui/list.spec.ts b/tests/src/ui/list.spec.ts index 1b99ce6cb..fb2a30abe 100644 --- a/tests/src/ui/list.spec.ts +++ b/tests/src/ui/list.spec.ts @@ -515,4 +515,41 @@ describe('DxList', () => { expect(fixture.componentInstance.buttonDestroyed).toBe(true); }); + + it('should use item template to render/rerender an item with a template (T532675)', async(() => { + TestBed.configureTestingModule({ + declarations: [TestContainerComponent], + imports: [DxButtonModule, DxListModule] + }); + + TestBed.overrideComponent(TestContainerComponent, { + set: { + template: ` + + + + + + + + + ` + } + }); + + let fixture = TestBed.createComponent(TestContainerComponent); + fixture.detectChanges(); + + let instance = getWidget(fixture); + expect(instance.element().find('.dx-button').eq(0).dxButton('instance')).not.toBeUndefined(); + expect(instance.element().find('.dx-button').eq(1).dxButton('instance')).not.toBeUndefined(); + + instance.repaint(); + fixture.detectChanges(); + expect(instance.element().find('.dx-button').eq(0).dxButton('instance')).not.toBeUndefined(); + expect(instance.element().find('.dx-button').eq(1).dxButton('instance')).not.toBeUndefined(); + })); + }); From 8834b3632f3cd905d3525f0dc514c1981248aa09 Mon Sep 17 00:00:00 2001 From: bykov Date: Wed, 20 Sep 2017 20:22:26 +0300 Subject: [PATCH 2/7] Update to ng 4 syntax --- README.md | 4 ++-- tests/src/ui/list.spec.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ca98eeff6..2d360eace 100644 --- a/README.md +++ b/README.md @@ -471,10 +471,10 @@ Note, in some scenarios an item content can be rerendered by it's host component - + ``` diff --git a/tests/src/ui/list.spec.ts b/tests/src/ui/list.spec.ts index fb2a30abe..7e71e651b 100644 --- a/tests/src/ui/list.spec.ts +++ b/tests/src/ui/list.spec.ts @@ -530,9 +530,9 @@ describe('DxList', () => { - + ` From 0a6beafe4f3db6f49c320b6fdc4b131f3ed56694 Mon Sep 17 00:00:00 2001 From: bykov Date: Wed, 20 Sep 2017 20:27:02 +0300 Subject: [PATCH 3/7] Fix linter issue --- src/core/component.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/component.ts b/src/core/component.ts index 95824a64b..0c32becdf 100644 --- a/src/core/component.ts +++ b/src/core/component.ts @@ -17,7 +17,8 @@ import { CollectionNestedOptionContainerImpl } from './nested-option'; -export abstract class DxComponent implements AfterViewInit, AfterContentChecked, INestedOptionContainer, ICollectionNestedOptionContainer, IDxTemplateHost { +export abstract class DxComponent implements AfterViewInit, AfterContentChecked, + INestedOptionContainer, ICollectionNestedOptionContainer, IDxTemplateHost { private _optionToUpdate: any = {}; private _collectionContainerImpl: ICollectionNestedOptionContainer; eventHelper: EmitterHelper; From 4ccf060f2fe00a5cc3742e90392e30e3b3d8df45 Mon Sep 17 00:00:00 2001 From: bykov Date: Wed, 20 Sep 2017 20:42:19 +0300 Subject: [PATCH 4/7] Fix unit test for NG 2 --- tests/src/ui/list.spec.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/src/ui/list.spec.ts b/tests/src/ui/list.spec.ts index 7e71e651b..9a26df46f 100644 --- a/tests/src/ui/list.spec.ts +++ b/tests/src/ui/list.spec.ts @@ -1,6 +1,7 @@ /* tslint:disable:component-selector */ import { + VERSION, Component, ViewChildren, QueryList, @@ -517,6 +518,8 @@ describe('DxList', () => { }); it('should use item template to render/rerender an item with a template (T532675)', async(() => { + const ngTemplateName = Number(VERSION.major) >= 4 ? 'ng-template' : 'template'; + TestBed.configureTestingModule({ declarations: [TestContainerComponent], imports: [DxButtonModule, DxListModule] @@ -530,9 +533,9 @@ describe('DxList', () => { - + <${ngTemplateName} dxTemplate> - + ` From 9fce9d145c1a15c962088f0e83c436a5de4552f5 Mon Sep 17 00:00:00 2001 From: bykov Date: Wed, 20 Sep 2017 21:44:07 +0300 Subject: [PATCH 5/7] Add missed argument name --- src/core/template-host.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/template-host.ts b/src/core/template-host.ts index cef89c6b8..67cdad076 100644 --- a/src/core/template-host.ts +++ b/src/core/template-host.ts @@ -1,7 +1,7 @@ import { DxTemplateDirective } from './template'; export interface IDxTemplateHost { - setTemplate(DxTemplateDirective); + setTemplate(template: DxTemplateDirective); }; export class DxTemplateHost { From 6c3dd3a0e23b35eca430a761987d67eda72a66eb Mon Sep 17 00:00:00 2001 From: bykov Date: Mon, 25 Sep 2017 15:31:13 +0300 Subject: [PATCH 6/7] Improve README --- README.md | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 2d360eace..179c9f227 100644 --- a/README.md +++ b/README.md @@ -457,25 +457,15 @@ export class AppComponent { } ``` -Note, in some scenarios an item content can be rerendered by it's host component. To keep the item child components alive they should be declared as a single template via the `dxTemplate` structural directive. There are several alternatives depending on the item content: +If your item template contains some *nested* components, declare it using the parameterless `dxTemplate` structural directive as follows: ```html - - -
- - +
- - - - - -
``` From 26860e37ccc06b9564e654e3d463ce4a618a0e5e Mon Sep 17 00:00:00 2001 From: bykov Date: Mon, 25 Sep 2017 15:31:58 +0300 Subject: [PATCH 7/7] Fix interface naming inconsistency --- src/core/events-strategy.ts | 10 +++++----- src/core/nested-option.ts | 16 ++++++++-------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/core/events-strategy.ts b/src/core/events-strategy.ts index 8aa2e4c90..e557be18e 100644 --- a/src/core/events-strategy.ts +++ b/src/core/events-strategy.ts @@ -3,13 +3,13 @@ import { DxComponent } from './component'; const dxToNgEventNames = {}; -interface EventSubscription { +interface IEventSubscription { handler: any; unsubscribe: () => void; } export class NgEventsStrategy { - private subscriptions: { [key: string]: EventSubscription[] } = {}; + private subscriptions: { [key: string]: IEventSubscription[] } = {}; constructor(private component: DxComponent, private ngZone: NgZone) { } @@ -62,13 +62,13 @@ export class NgEventsStrategy { } } -interface RememberedEvent { +interface IRememberedEvent { name: string; context: EmitterHelper; } -let events: RememberedEvent[] = []; -let onStableSubscription: EventSubscription = null; +let events: IRememberedEvent[] = []; +let onStableSubscription: IEventSubscription = null; let createOnStableSubscription = function(ngZone: NgZone, fireNgEvent: Function) { if (onStableSubscription) { diff --git a/src/core/nested-option.ts b/src/core/nested-option.ts index b22bbc7b4..7fe7d7a6c 100644 --- a/src/core/nested-option.ts +++ b/src/core/nested-option.ts @@ -11,11 +11,11 @@ export interface INestedOptionContainer { instance: any; } -export interface OptionPathGetter { (): string; } +export interface IOptionPathGetter { (): string; } export abstract class BaseNestedOption implements INestedOptionContainer, ICollectionNestedOptionContainer { protected _host: INestedOptionContainer; - protected _hostOptionPath: OptionPathGetter; + protected _hostOptionPath: IOptionPathGetter; private _collectionContainerImpl: ICollectionNestedOptionContainer; protected _initialOptions = {}; @@ -42,7 +42,7 @@ export abstract class BaseNestedOption implements INestedOptionContainer, IColle } } - setHost(host: INestedOptionContainer, optionPath: OptionPathGetter) { + setHost(host: INestedOptionContainer, optionPath: IOptionPathGetter) { this._host = host; this._hostOptionPath = optionPath; } @@ -91,7 +91,7 @@ export class CollectionNestedOptionContainerImpl implements ICollectionNestedOpt } export abstract class NestedOption extends BaseNestedOption { - setHost(host: INestedOptionContainer, optionPath: OptionPathGetter) { + setHost(host: INestedOptionContainer, optionPath: IOptionPathGetter) { super.setHost(host, optionPath); this._host[this._optionPath] = this._initialOptions; @@ -123,10 +123,10 @@ export abstract class CollectionNestedOption extends BaseNestedOption implements } } -export interface OptionWithTemplate extends BaseNestedOption { +export interface IOptionWithTemplate extends BaseNestedOption { template: any; } -export function extractTemplate(option: OptionWithTemplate, element: ElementRef) { +export function extractTemplate(option: IOptionWithTemplate, element: ElementRef) { if (!option.template === undefined || !element.nativeElement.hasChildNodes()) { return; } @@ -184,9 +184,9 @@ export function extractTemplate(option: OptionWithTemplate, element: ElementRef) export class NestedOptionHost { private _host: INestedOptionContainer; - private _optionPath: OptionPathGetter; + private _optionPath: IOptionPathGetter; - setHost(host: INestedOptionContainer, optionPath?: OptionPathGetter) { + setHost(host: INestedOptionContainer, optionPath?: IOptionPathGetter) { this._host = host; this._optionPath = optionPath || (() => ''); }