Skip to content
This repository was archived by the owner on Mar 2, 2026. It is now read-only.

Commit 7910992

Browse files
dxvladislavvolkovdxvladislavvolkov
andauthored
Fix reset option for collections options (#1042)
* Fix reset option for collections options * Rework fix * Refactoring * Rename method Co-authored-by: dxvladislavvolkov <dxvladislavvolkov@devexpress.com>
1 parent 3bb0e72 commit 7910992

4 files changed

Lines changed: 104 additions & 17 deletions

File tree

src/core/component.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ export abstract class DxComponent implements OnChanges, OnInit, DoCheck, AfterCo
5151
instance: any;
5252
isLinked = true;
5353
changedOptions = {};
54-
removedOptions = [];
54+
removedNestedComponents = [];
55+
recreatedNestedComponents: any[];
5556
widgetUpdateLocked = false;
5657

5758
private _initTemplates() {
@@ -171,7 +172,7 @@ export abstract class DxComponent implements OnChanges, OnInit, DoCheck, AfterCo
171172
}
172173

173174
protected _destroyWidget() {
174-
this.removedOptions = [];
175+
this.removedNestedComponents = [];
175176
if (this.instance) {
176177
let element = this.instance.element();
177178
events.triggerHandler(element, 'dxremove', { _angularIntegration: true });
@@ -218,6 +219,7 @@ export abstract class DxComponent implements OnChanges, OnInit, DoCheck, AfterCo
218219
ngAfterViewInit() {
219220
this._initTemplates();
220221
this.instance.endUpdate();
222+
this.recreatedNestedComponents = [];
221223
}
222224

223225
applyOptions() {
@@ -231,13 +233,21 @@ export abstract class DxComponent implements OnChanges, OnInit, DoCheck, AfterCo
231233

232234
resetOptions() {
233235
if (this.instance) {
234-
this.removedOptions.forEach(option => {
235-
this.instance.resetOption(option);
236+
this.removedNestedComponents.forEach(option => {
237+
if (option && !this.isRecreated(option)) {
238+
this.instance.resetOption(option);
239+
}
236240
});
237-
this.removedOptions = [];
241+
this.removedNestedComponents = [];
242+
this.recreatedNestedComponents = [];
238243
}
239244
}
240245

246+
isRecreated(name: string): boolean {
247+
return this.recreatedNestedComponents &&
248+
this.recreatedNestedComponents.some(nestedComponent => nestedComponent.getOptionPath() === name);
249+
}
250+
241251
setTemplate(template: DxTemplateDirective) {
242252
this.templates.push(template);
243253
}

src/core/nested-option.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ const VISIBILITY_CHANGE_SELECTOR = 'dx-visibility-change-handler';
1111
export interface INestedOptionContainer {
1212
instance: any;
1313
isLinked: boolean;
14-
removedOptions: string[];
14+
removedNestedComponents: string[];
1515
optionChangedHandlers: EventEmitter<any>;
16+
recreatedNestedComponents: any[];
1617
}
1718

1819
export interface IOptionPathGetter { (): string; }
@@ -59,18 +60,29 @@ export abstract class BaseNestedOption implements INestedOptionContainer, IColle
5960

6061
protected _setOption(name: string, value: any) {
6162
if (this.isLinked) {
62-
this.instance.option(this._fullOptionPath() + name, value);
63+
const fullPath = this._fullOptionPath() + name;
64+
this.instance.option(fullPath, value);
6365
} else {
6466
this._initialOptions[name] = value;
6567
}
6668
}
6769

6870
protected _addRemovedOption(name: string) {
69-
if (this.instance && this.removedOptions) {
70-
this.removedOptions.push(name);
71+
if (this.instance && this.removedNestedComponents) {
72+
this.removedNestedComponents.push(name);
7173
}
7274
}
7375

76+
protected _addRecreatedComponent() {
77+
if (this.instance && this.recreatedNestedComponents) {
78+
this.recreatedNestedComponents.push({ getOptionPath: () => this._getOptionPath() });
79+
}
80+
}
81+
82+
protected _getOptionPath() {
83+
return this._hostOptionPath() + this._optionPath;
84+
}
85+
7486
setHost(host: INestedOptionContainer, optionPath: IOptionPathGetter) {
7587
this._host = host;
7688
this._hostOptionPath = optionPath;
@@ -89,8 +101,12 @@ export abstract class BaseNestedOption implements INestedOptionContainer, IColle
89101
return this._host && this._host.instance;
90102
}
91103

92-
get removedOptions() {
93-
return this._host && this._host.removedOptions;
104+
get removedNestedComponents() {
105+
return this._host && this._host.removedNestedComponents;
106+
}
107+
108+
get recreatedNestedComponents() {
109+
return this._host && this._host.recreatedNestedComponents;
94110
}
95111

96112
get isLinked() {
@@ -136,7 +152,7 @@ export abstract class NestedOption extends BaseNestedOption {
136152
}
137153

138154
protected _fullOptionPath() {
139-
return this._hostOptionPath() + this._optionPath + '.';
155+
return this._getOptionPath() + '.';
140156
}
141157
}
142158

@@ -149,7 +165,7 @@ export abstract class CollectionNestedOption extends BaseNestedOption implements
149165
_index: number;
150166

151167
protected _fullOptionPath() {
152-
return this._hostOptionPath() + this._optionPath + '[' + this._index + ']' + '.';
168+
return `${this._getOptionPath()}[${this._index}].`;
153169
}
154170

155171
get _value() {

templates/nested-component.tst

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<#?#>
55
import {
66
Component,<#? !it.isCollection #>
7+
OnInit,
78
OnDestroy,<#?#>
89
NgModule,
910
Host,<#? it.hasTemplate #>
@@ -45,8 +46,8 @@ import { <#= it.baseClass #> } from '<#= it.basePath #>';
4546
'<#= input.name #>'<#? i < it.inputs.length-1 #>,<#?#><#~#>
4647
]<#?#>
4748
})
48-
export class <#= it.className #>Component extends <#= it.baseClass #><#? it.hasTemplate #> implements AfterViewInit,<#? !it.isCollection #> OnDestroy,<#?#>
49-
IDxTemplateHost<#?#><#? !it.isCollection && !it.hasTemplate #> implements OnDestroy <#?#> {<#~ it.properties :prop:i #>
49+
export class <#= it.className #>Component extends <#= it.baseClass #><#? it.hasTemplate #> implements AfterViewInit,<#? !it.isCollection #> OnDestroy, OnInit,<#?#>
50+
IDxTemplateHost<#?#><#? !it.isCollection && !it.hasTemplate #> implements OnDestroy, OnInit <#?#> {<#~ it.properties :prop:i #>
5051
@Input()
5152
get <#= prop.name #>(): <#= prop.type #> {
5253
return this._getOption('<#= prop.name #>');
@@ -106,8 +107,12 @@ export class <#= it.className #>Component extends <#= it.baseClass #><#? it.hasT
106107
}
107108
<#?#>
108109
<#? !it.isCollection #>
110+
ngOnInit() {
111+
this._addRecreatedComponent();
112+
}
113+
109114
ngOnDestroy() {
110-
this._addRemovedOption(this._fullOptionPath().slice(0, -1));
115+
this._addRemovedOption(this._getOptionPath());
111116
}
112117
<#?#>
113118
}

tests/src/ui/form.spec.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ class TestContainerComponent {
2525
name: 'Unknown',
2626
date: new Date()
2727
};
28+
labelValues = [{ name: 'label1' }, { name: 'label2' }];
29+
formData1 = { labels: this.labelValues };
2830
@ViewChild(DxFormComponent) formComponent: DxFormComponent;
2931

3032
validateForm() {
@@ -174,7 +176,7 @@ describe('DxForm', () => {
174176
<dxi-validation-rule type="custom" [validationCallback]="validateForm">
175177
</dxi-validation-rule>
176178
</dxi-item>
177-
</dxi-item>
179+
</dxi-item>
178180
</dx-form>
179181
`
180182
}
@@ -187,6 +189,60 @@ describe('DxForm', () => {
187189
expect(formInstance.validate()).toBeDefined();
188190
});
189191

192+
it('should not call resetOption for rerendered nested collection components', () => {
193+
TestBed.overrideComponent(TestContainerComponent, {
194+
set: {
195+
template: `
196+
<dx-form [formData]="formData1">
197+
<dxi-item itemType="group"
198+
name="label-container">
199+
<dxi-item [dataField]="'labels[' + i + ']'" *ngFor="let labelValue of labelValues; let i = index">
200+
<dxo-label [text]="labelValue.name"></dxo-label>
201+
</dxi-item>
202+
</dxi-item>
203+
</dx-form>
204+
`
205+
}
206+
});
207+
208+
let fixture = TestBed.createComponent(TestContainerComponent);
209+
fixture.detectChanges();
210+
let formInstance = getWidget(fixture);
211+
let spy = spyOn(formInstance, 'resetOption');
212+
213+
fixture.componentInstance.labelValues = [{ name: 'label1' }, { name: 'label2' }, { name: 'label3' }];
214+
fixture.detectChanges();
215+
216+
expect(spy.calls.count()).toBe(0);
217+
expect(formInstance.option('items[0].items[0].label.text')).toBe('label1');
218+
});
219+
220+
it('should not call resetOption for rerendered nested components', () => {
221+
TestBed.overrideComponent(TestContainerComponent, {
222+
set: {
223+
template: `
224+
<dx-form [formData]="formData1">
225+
<dxi-item [dataField]="'labels[' + i + ']'" *ngFor="let labelValue of labelValues; let i = index">
226+
<dxo-label [text]="labelValue.name"></dxo-label>
227+
</dxi-item>
228+
</dx-form>
229+
`
230+
}
231+
});
232+
233+
let fixture = TestBed.createComponent(TestContainerComponent);
234+
fixture.detectChanges();
235+
236+
let formInstance = getWidget(fixture);
237+
let spy = spyOn(formInstance, 'resetOption');
238+
239+
fixture.componentInstance.labelValues = [{ name: 'label1' }, { name: 'label2' }, { name: 'label3' }];
240+
fixture.detectChanges();
241+
242+
expect(spy.calls.count()).toBe(0);
243+
expect(formInstance.option('items[0].label.text')).toBe('label1');
244+
});
245+
190246
it('should change the value of dxDateBox', () => {
191247
TestBed.overrideComponent(TestContainerComponent, {
192248
set: {

0 commit comments

Comments
 (0)