Skip to content

Fix dxi component template rerendering issues #560

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

Merged
merged 8 commits into from
Sep 28, 2017
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
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,18 @@ export class AppComponent {
}
```

If your item template contains some *nested* components, declare it using the parameterless `dxTemplate` structural directive as follows:

```html
<dx-list>
<dxi-item>
<div *dxTemplate>
<dx-button text="I'm a nested child component"></dx-button>
</div>
</dxi-item>
</dx-list>
```

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
<dxo-master-detail [template]="'masterDetail'"></dxo-master-detail>
Expand Down
5 changes: 3 additions & 2 deletions src/core/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -17,7 +17,8 @@ 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;
Expand Down
10 changes: 5 additions & 5 deletions src/core/events-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) { }

Expand Down Expand Up @@ -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) {
Expand Down
16 changes: 8 additions & 8 deletions src/core/nested-option.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {};

Expand All @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 || (() => '');
}
Expand Down
9 changes: 6 additions & 3 deletions src/core/template-host.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { DxComponent } from './component';
import { DxTemplateDirective } from './template';

export interface IDxTemplateHost {
Copy link
Contributor

@kvet kvet Sep 21, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't use I prefix for interfaces

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 of 7 existing interfaces have I prefix and just 2 don't. I would add it to the rest two.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are good points why we should use the I prefix :)

setTemplate(template: DxTemplateDirective);
};

export class DxTemplateHost {
host: DxComponent;
setHost(host: DxComponent) {
host: IDxTemplateHost;
setHost(host: IDxTemplateHost) {
this.host = host;
}
setTemplate(template: DxTemplateDirective) {
Expand Down
2 changes: 1 addition & 1 deletion src/core/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export class RenderData {
}

@Directive({
selector: '[dxTemplate][dxTemplateOf]'
selector: '[dxTemplate]'
})
export class DxTemplateDirective {
@Input()
Expand Down
20 changes: 14 additions & 6 deletions templates/nested-component.tst
Original file line number Diff line number Diff line change
Expand Up @@ -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 #>';
<#?#><#~#>
Expand All @@ -22,12 +24,12 @@ import { <#= it.baseClass #> } from '<#= it.basePath #>';
selector: '<#= it.selector #>',
template: '<#? it.hasTemplate #><ng-content></ng-content><#?#>',
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 #>');
Expand All @@ -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);
}
Expand Down
40 changes: 40 additions & 0 deletions tests/src/ui/list.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* tslint:disable:component-selector */

import {
VERSION,
Component,
ViewChildren,
QueryList,
Expand Down Expand Up @@ -515,4 +516,43 @@ describe('DxList', () => {

expect(fixture.componentInstance.buttonDestroyed).toBe(true);
});

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]
});

TestBed.overrideComponent(TestContainerComponent, {
set: {
template: `
<dx-list>
<dxi-item>
<dx-button *dxTemplate></dx-button>
</dxi-item>
<dxi-item>
<${ngTemplateName} dxTemplate>
<dx-button></dx-button>
</${ngTemplateName}>
</dxi-item>
</dx-list>
`
}
});

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();
}));

});