Skip to content

Commit 824cc60

Browse files
Fix dynamic collection components (#364)
* Fix dynamic collection components * Add tests and refactoring * Small fix
1 parent 763b174 commit 824cc60

File tree

6 files changed

+208
-39
lines changed

6 files changed

+208
-39
lines changed

src/core/component.test.ts

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -841,15 +841,85 @@ describe("nested option", () => {
841841
});
842842

843843
it("remove nested component by condition", (done) => {
844+
const nestedCollectionItem = buildTestConfigCtor();
845+
(nestedCollectionItem as any as IConfigurationComponent).$_optionName = "nestedOption";
846+
(nestedCollectionItem as any as IConfigurationComponent).$_isCollectionItem = true;
844847
const vm = new Vue({
845848
template:
846849
`<test-component>` +
847-
` <nested v-if="showNest" :prop1="123" />` +
848-
` <nested :prop1="321" />` +
850+
` <nested-collection-item v-if="showNest" :prop1="123" />` +
851+
` <nested-collection-item :prop1="321" />` +
849852
`</test-component>`,
850853
components: {
851854
TestComponent,
852-
Nested
855+
nestedCollectionItem
856+
},
857+
data: {
858+
showNest: true
859+
}
860+
}).$mount();
861+
862+
vm.$data.showNest = false;
863+
864+
Vue.nextTick(() => {
865+
expect(Widget.option).toHaveBeenCalledWith("nestedOption", [{ prop1: 321 }]);
866+
done();
867+
});
868+
});
869+
870+
it("should update only part of collection components", (done) => {
871+
const nestedCollectionItem = buildTestConfigCtor();
872+
(nestedCollectionItem as any as IConfigurationComponent).$_optionName = "nestedOption";
873+
(nestedCollectionItem as any as IConfigurationComponent).$_isCollectionItem = true;
874+
const vm = new Vue({
875+
template:
876+
`<test-component>` +
877+
` <nested-collection-item>` +
878+
` <nested-collection-item>` +
879+
` <nested-collection-item v-if="showNest" :prop1="123">` +
880+
` </nested-collection-item>` +
881+
` <nested-collection-item :prop1="321">` +
882+
` </nested-collection-item>` +
883+
` </nested-collection-item>` +
884+
` </nested-collection-item>` +
885+
`</test-component>`,
886+
components: {
887+
TestComponent,
888+
nestedCollectionItem
889+
},
890+
data: {
891+
showNest: true
892+
}
893+
}).$mount();
894+
895+
vm.$data.showNest = false;
896+
897+
Vue.nextTick(() => {
898+
expect(Widget.option)
899+
.toHaveBeenCalledWith("nestedOption[0].nestedOption[0].nestedOption", [{ prop1: 321 }]);
900+
done();
901+
});
902+
});
903+
904+
it("should update only part of collection components (remove all subnested)", (done) => {
905+
const nestedCollectionItem = buildTestConfigCtor();
906+
(nestedCollectionItem as any as IConfigurationComponent).$_optionName = "nestedOption";
907+
(nestedCollectionItem as any as IConfigurationComponent).$_isCollectionItem = true;
908+
const vm = new Vue({
909+
template:
910+
`<test-component>` +
911+
` <nested-collection-item>` +
912+
` <nested-collection-item>` +
913+
` <nested-collection-item v-if="showNest" :prop1="123">` +
914+
` </nested-collection-item>` +
915+
` <nested-collection-item v-if="showNest" :prop1="321">` +
916+
` </nested-collection-item>` +
917+
` </nested-collection-item>` +
918+
` </nested-collection-item>` +
919+
`</test-component>`,
920+
components: {
921+
TestComponent,
922+
nestedCollectionItem
853923
},
854924
data: {
855925
showNest: true
@@ -859,7 +929,7 @@ describe("nested option", () => {
859929
vm.$data.showNest = false;
860930

861931
Vue.nextTick(() => {
862-
expect(Widget.option).toHaveBeenCalledWith("nestedOption", { prop1: 321 });
932+
expect(Widget.option).toHaveBeenCalledWith("nestedOption[0].nestedOption[0].nestedOption", undefined);
863933
done();
864934
});
865935
});

src/core/component.ts

Lines changed: 16 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import Configuration, { bindOptionWatchers, setEmitOptionChangedFunc } from "./c
88
import { getConfig, getInnerChanges, IConfigurable, initOptionChangedFunc } from "./configuration-component";
99
import { DX_REMOVE_EVENT } from "./constants";
1010
import { IExtension, IExtensionComponentNode } from "./extension-component";
11-
import { camelize, forEachChildNode, toComparable } from "./helpers";
11+
import { camelize, forEachChildNode, getOptionValue, toComparable } from "./helpers";
1212
import {
1313
IEventBusHolder
1414
} from "./templates-discovering";
@@ -23,6 +23,7 @@ interface IWidgetComponent extends IConfigurable {
2323

2424
interface IBaseComponent extends IVue, IWidgetComponent, IEventBusHolder {
2525
$_isExtension: boolean;
26+
$_applyConfigurationChanges: () => void;
2627
$_createWidget: (element: any) => void;
2728
$_getIntegrationOptions: () => void;
2829
$_getExtraIntegrationOptions: () => void;
@@ -95,31 +96,8 @@ const BaseComponent: VueConstructor<IBaseComponent> = Vue.extend({
9596
}
9697
(this as IBaseComponent).$_pendingOptions = {};
9798

98-
if (this.$_config.componentsCountChanged) {
99-
const options = this.$_config.getNestedOptionValues();
100-
const prevOptions = this.$_config.prevNestedOptions;
101-
const optionsList = Object.keys(options);
102-
const prevOptionsList = Object.keys(prevOptions);
103-
if (optionsList.length < prevOptionsList.length) {
104-
prevOptionsList.forEach((prevName) => {
105-
const hasOption = optionsList.some((name) => {
106-
return prevName === name;
107-
});
108-
109-
if (!hasOption) {
110-
this.$_instance.resetOption(prevName);
111-
}
112-
});
113-
}
114-
115-
for (const name in options) {
116-
if (options.hasOwnProperty(name)) {
117-
this.$_instance.option(name, options[name]);
118-
}
119-
}
99+
this.$_applyConfigurationChanges();
120100

121-
this.$_config.componentsCountChanged = false;
122-
}
123101
this.$_instance.endUpdate();
124102
this.eventBus.$emit("updated");
125103
},
@@ -145,6 +123,19 @@ const BaseComponent: VueConstructor<IBaseComponent> = Vue.extend({
145123
},
146124

147125
methods: {
126+
$_applyConfigurationChanges(): void {
127+
this.$_config.componentsCountChanged.forEach(({ optionPath, isCollection, removed }) => {
128+
const options = this.$_config.getNestedOptionValues();
129+
130+
if (!isCollection && removed) {
131+
this.$_instance.resetOption(optionPath);
132+
} else {
133+
this.$_instance.option(optionPath, getOptionValue(options, optionPath));
134+
}
135+
});
136+
137+
this.$_config.cleanComponentsCountChanged();
138+
},
148139
$_createWidget(element: any): void {
149140
const thisComponent = this as IBaseComponent;
150141

src/core/configuration-component.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ interface IConfigurable extends IConfigurationOwner {
2020
$_innerChanges: any;
2121
}
2222

23+
interface IComponentInfo {
24+
optionPath: string;
25+
isCollection: boolean;
26+
removed?: boolean;
27+
}
28+
2329
function getConfig(vueInstance: Pick<IVue, "$vnode">): Configuration | undefined {
2430
if (!vueInstance.$vnode) {
2531
return;
@@ -49,6 +55,17 @@ function initOptionChangedFunc(config, vueInstance: Pick<IVue, "$vnode" | "$prop
4955
setEmitOptionChangedFunc(config, vueInstance, innerChanges);
5056
}
5157

58+
function getComponentInfo({name, isCollectionItem, ownerConfig }: Configuration, removed?: boolean): IComponentInfo {
59+
const parentPath = ownerConfig && ownerConfig.fullPath;
60+
const optionPath = name && parentPath ? `${parentPath}.${name}` : name || "";
61+
62+
return {
63+
optionPath,
64+
isCollection: isCollectionItem,
65+
removed
66+
};
67+
}
68+
5269
const DxConfiguration: VueConstructor = Vue.extend({
5370
beforeMount() {
5471
const config = getConfig(this) as Configuration;
@@ -59,17 +76,27 @@ const DxConfiguration: VueConstructor = Vue.extend({
5976

6077
mounted() {
6178
if ((this.$parent as any).$_instance) {
62-
(this.$parent as any).$_config.componentsCountChanged = true;
79+
(this.$parent as any).$_config.componentsCountChanged
80+
.push(getComponentInfo(getConfig(this) as Configuration));
6381
}
6482
},
6583

6684
beforeDestroy() {
67-
(this.$parent as any).$_config.componentsCountChanged = true;
85+
(this.$parent as any).$_config.componentsCountChanged
86+
.push(getComponentInfo(getConfig(this) as Configuration, true));
6887
},
6988

7089
render(createElement: (...args) => VNode): VNode {
7190
return createElement();
7291
}
7392
});
7493

75-
export { DxConfiguration, IConfigurable, IConfigurationComponent, initOptionChangedFunc, getConfig, getInnerChanges };
94+
export {
95+
DxConfiguration,
96+
IComponentInfo,
97+
IConfigurable,
98+
IConfigurationComponent,
99+
initOptionChangedFunc,
100+
getConfig,
101+
getInnerChanges
102+
};

src/core/configuration.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Vue } from "vue/types/vue";
2+
import { IComponentInfo } from "./configuration-component";
23
import { getOptionInfo, isEqual } from "./helpers";
34

45
type UpdateFunc = (name: string, value: any) => void;
@@ -28,7 +29,7 @@ class Configuration {
2829
private _nestedConfigurations: Configuration[];
2930
private _prevNestedConfigOptions: any;
3031
private _emitOptionChanged: EmitOptionChangedFunc;
31-
private _componentsCountChanged: boolean;
32+
private _componentChanges: IComponentInfo[];
3233

3334
private _options: string[];
3435

@@ -49,7 +50,7 @@ class Configuration {
4950
this._collectionItemIndex = collectionItemIndex;
5051
this._expectedChildren = expectedChildren || {};
5152
this._ownerConfig = ownerConfig;
52-
this._componentsCountChanged = false;
53+
this._componentChanges = [];
5354

5455
this.updateValue = this.updateValue.bind(this);
5556
}
@@ -64,12 +65,12 @@ class Configuration {
6465
: this._name;
6566
}
6667

67-
public get hasOptionsToUpdate(): boolean {
68-
return this._componentsCountChanged;
68+
public get componentsCountChanged(): IComponentInfo[] {
69+
return this._componentChanges;
6970
}
7071

71-
public set hasOptionsToUpdate(value: boolean) {
72-
this._componentsCountChanged = value;
72+
public cleanComponentsCountChanged() {
73+
this._componentChanges = [];
7374
}
7475

7576
public get fullPath(): string | null {
@@ -78,6 +79,10 @@ class Configuration {
7879
: this.fullName;
7980
}
8081

82+
public get ownerConfig(): Pick<Configuration, "fullPath"> | undefined {
83+
return this._ownerConfig;
84+
}
85+
8186
public get options(): string[] {
8287
return this._options;
8388
}

src/core/helpers.test.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { allKeysAreEqual, getOptionInfo, isEqual, toComparable } from "./helpers";
1+
import { allKeysAreEqual, getOptionInfo, getOptionValue, isEqual, toComparable } from "./helpers";
22

33
describe("toComparable", () => {
44

@@ -70,6 +70,69 @@ describe("allKeysAreEqual", () => {
7070
});
7171
});
7272

73+
describe("getOptionValue", () => {
74+
it("returns for simple option", () => {
75+
const optionValue = getOptionValue({ test: "text" }, "test");
76+
77+
expect(optionValue).toEqual("text");
78+
});
79+
80+
it("returns for complex option", () => {
81+
const optionValue = getOptionValue({ test: { value: "text" } }, "test");
82+
const optionValue1 = getOptionValue({ test: { value: "text" } }, "test.value");
83+
84+
expect(optionValue).toEqual({ value: "text" });
85+
expect(optionValue1).toEqual("text");
86+
});
87+
88+
it("returns for complex option", () => {
89+
const optionValue = getOptionValue({ test: { value: "text" } }, "test");
90+
91+
expect(optionValue).toEqual({ value: "text" });
92+
});
93+
94+
it("returns for collection option", () => {
95+
const value = [
96+
{ text: "value1"},
97+
{ text: "value2"},
98+
{ text: "value3",
99+
test: [{
100+
option: {
101+
text: "value1"
102+
}
103+
}, {
104+
text: "value2"
105+
}] }
106+
];
107+
108+
const optionValue1 = getOptionValue({ test: value }, "test[1]");
109+
const optionValue2 = getOptionValue({ test: value }, "test");
110+
const optionValue3 = getOptionValue({ test: value }, "test[2].test[1]");
111+
const optionValue4 = getOptionValue({ test: value }, "test[2].test");
112+
const optionValue5 = getOptionValue({ test: value }, "test[2].test[0].option");
113+
const optionValue6 = getOptionValue({ test: value }, "test[2].test[0].option.text");
114+
115+
expect(optionValue1).toEqual({ text: "value2" });
116+
expect(optionValue2).toEqual(value);
117+
expect(optionValue3).toEqual({ text: "value2" });
118+
expect(optionValue4).toEqual([{
119+
option: { text: "value1" }
120+
}, {
121+
text: "value2"
122+
}]);
123+
expect(optionValue5).toEqual({ text: "value1" });
124+
expect(optionValue6).toEqual("value1");
125+
});
126+
127+
it("returns for empty", () => {
128+
const optionValue = getOptionValue({}, "test");
129+
const optionValue2 = getOptionValue({ test: [{ text: "value1" }] }, "test[1]");
130+
131+
expect(optionValue).toEqual(undefined);
132+
expect(optionValue2).toEqual(undefined);
133+
});
134+
});
135+
73136
describe("getOptionInfo", () => {
74137
it("returns for simple option", () => {
75138
const optionInfo = getOptionInfo("test");

src/core/helpers.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,19 @@ export function allKeysAreEqual(obj1: object, obj2: object) {
4949
return true;
5050
}
5151

52+
export function getOptionValue(options, optionPath) {
53+
let value = options;
54+
55+
optionPath.split(".").forEach((p) => {
56+
const optionInfo = getOptionInfo(p);
57+
value = optionInfo.isCollection ?
58+
value[optionInfo.name] && value[optionInfo.name][optionInfo.index] :
59+
value[optionInfo.name];
60+
});
61+
62+
return value;
63+
}
64+
5265
export function getOptionInfo(name: string): IOptionInfo | ICollectionOptionInfo {
5366
const parts = name.split("[");
5467

0 commit comments

Comments
 (0)