Skip to content

Commit bfc25fb

Browse files
authored
TagBox: tags should be actualized after data source change (T1253312)
1 parent e2bc8d8 commit bfc25fb

File tree

3 files changed

+192
-14
lines changed

3 files changed

+192
-14
lines changed

packages/devextreme/js/__internal/ui/m_tag_box.ts

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -787,7 +787,15 @@ const TagBox = (SelectBox as any).inherit({
787787
const d = Deferred();
788788
const dataController = this._dataController;
789789

790-
if ((!this._isDataSourceChanged || isListItemsLoaded) && selectedItemsAlreadyLoaded) {
790+
if (!this._dataSource) {
791+
return d.resolve([]).promise();
792+
}
793+
794+
if (
795+
(!this._isDataSourceChanged || isListItemsLoaded)
796+
&& selectedItemsAlreadyLoaded
797+
&& !this._isDataSourceOptionChanged
798+
) {
791799
return d.resolve(filteredItems).promise();
792800
}
793801
const { customQueryParams, expand, select } = dataController.loadOptions();
@@ -799,6 +807,7 @@ const TagBox = (SelectBox as any).inherit({
799807
})
800808
.done((data, extra) => {
801809
this._isDataSourceChanged = false;
810+
this._isDataSourceOptionChanged = false;
802811
if (this._disposed) {
803812
d.reject();
804813
return;
@@ -818,6 +827,7 @@ const TagBox = (SelectBox as any).inherit({
818827
const items = [];
819828
const cache = {};
820829
const isValueExprSpecified = this._valueGetterExpr() === 'this';
830+
const { acceptCustomValue } = this.option();
821831
const filteredValues = {};
822832

823833
filteredItems.forEach((filteredItem) => {
@@ -832,13 +842,30 @@ const TagBox = (SelectBox as any).inherit({
832842
const currentItem = filteredValues[isValueExprSpecified ? JSON.stringify(value) : value];
833843

834844
if (isValueExprSpecified && !isDefined(currentItem)) {
835-
loadItemPromises.push(this._loadItem(value, cache).always((item) => {
836-
const newItem = this._createTagData(item, value);
837-
items.splice(index, 0, newItem as never);
838-
}) as never);
845+
if (!this._dataSource) {
846+
return;
847+
}
848+
849+
loadItemPromises.push(
850+
// @ts-expect-error
851+
this._loadItem(value, cache)
852+
.done((item) => {
853+
const newItem = this._createTagData(item, value);
854+
// @ts-expect-error
855+
items.splice(index, 0, newItem);
856+
})
857+
.fail(() => {
858+
if (acceptCustomValue) {
859+
const newItem = this._createTagData(undefined, value);
860+
// @ts-expect-error
861+
items.splice(index, 0, newItem);
862+
}
863+
}),
864+
);
839865
} else {
840866
const newItem = this._createTagData(currentItem, value);
841-
items.splice(index, 0, newItem as never);
867+
// @ts-expect-error
868+
items.splice(index, 0, newItem);
842869
}
843870
});
844871

@@ -870,7 +897,8 @@ const TagBox = (SelectBox as any).inherit({
870897
values.forEach((value) => {
871898
const item = this._getItemFromPlain(value);
872899
if (isDefined(item)) {
873-
resultItems.push(item as never);
900+
// @ts-expect-error
901+
resultItems.push(item);
874902
}
875903
});
876904
return resultItems;
@@ -1007,6 +1035,12 @@ const TagBox = (SelectBox as any).inherit({
10071035
return selectedItems;
10081036
},
10091037

1038+
_processDataSourceChanging() {
1039+
this._isDataSourceOptionChanged = true;
1040+
1041+
this.callBase();
1042+
},
1043+
10101044
_integrateInput() {
10111045
this._isInputReady.resolve();
10121046
this.callBase();
@@ -1060,13 +1094,15 @@ const TagBox = (SelectBox as any).inherit({
10601094
this._tagElements().remove();
10611095
} else {
10621096
const $tags = this._tagElements();
1063-
const values = this._getValue();
1097+
1098+
const selectedItems = this.option('selectedItems') ?? [];
1099+
const values = selectedItems.map((item) => this._valueGetter(item));
10641100

10651101
each($tags, (_, tag) => {
10661102
const $tag = $(tag);
10671103
const tagData = $tag.data(TAGBOX_TAG_DATA_KEY);
10681104

1069-
if (!values?.includes(tagData)) {
1105+
if (!values.includes(tagData)) {
10701106
$tag.remove();
10711107
}
10721108
});
@@ -1413,8 +1449,8 @@ const TagBox = (SelectBox as any).inherit({
14131449

14141450
_dataSourceFilterExpr() {
14151451
const filter = [];
1416-
1417-
this._getValue().forEach((value) => filter.push(['!', [this._valueGetterExpr(), value]] as never));
1452+
// @ts-expect-error
1453+
this._getValue().forEach((value) => filter.push(['!', [this._valueGetterExpr(), value]]));
14181454

14191455
return filter;
14201456
},

packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/tagBox.markup.tests.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ QUnit.module('base markup', moduleSetup, () => {
6767
QUnit.test('tagbox should render custom values in tags', function(assert) {
6868
const $element = $('#tagBox')
6969
.dxTagBox({
70-
value: [1, 2]
70+
value: [1, 2],
71+
acceptCustomValue: true,
7172
});
7273

7374
const tags = $element.find('.' + TAGBOX_TAG_CONTENT_CLASS);
@@ -77,6 +78,7 @@ QUnit.module('base markup', moduleSetup, () => {
7778
QUnit.test('tagElement arguments of tagTemplate for custom tags is correct', function(assert) {
7879
$('#tagBox').dxTagBox({
7980
value: [1, 2],
81+
acceptCustomValue: true,
8082
tagTemplate: function(tagData, tagElement) {
8183
assert.equal(isRenderer(tagElement), !!config().useJQuery, 'tagElement is correct');
8284
}

packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/tagBox.tests.js

Lines changed: 142 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,146 @@ QUnit.module('tags', moduleSetup, () => {
738738
done();
739739
}, TIME_TO_WAIT);
740740
});
741+
742+
['items', 'dataSource'].forEach((optionName) => {
743+
QUnit.test('TagBox should not have unexpected selected tags when value includes item that doesn\'t exist in items', function(assert) {
744+
const options = { value: [1, 11] };
745+
options[optionName] = [1, 2, 3];
746+
const $tagBox = $('#tagBox').dxTagBox(options);
747+
const tagBox = $tagBox.dxTagBox('instance');
748+
749+
const { selectedItems } = tagBox.option();
750+
const $tags = $tagBox.find(`.${TAGBOX_TAG_CLASS}`);
751+
752+
assert.deepEqual(selectedItems, [1], 'selectedItems have no unexpected items');
753+
assert.strictEqual($tags.length, 1, 'there is no unexpected tags');
754+
});
755+
});
756+
757+
[false, true].forEach((deferRendering) => {
758+
[
759+
{
760+
initialOptions: {
761+
deferRendering,
762+
items: [1, 2, 3],
763+
value: [1, 3],
764+
},
765+
optionsToUpdate: {
766+
items: [1, 2],
767+
},
768+
expectedSelectedItems: [1],
769+
optionName: 'items',
770+
},
771+
{
772+
initialOptions: {
773+
deferRendering,
774+
dataSource: [1, 2, 3],
775+
value: [1, 3],
776+
},
777+
optionsToUpdate: {
778+
dataSource: [1, 2],
779+
},
780+
expectedSelectedItems: [1],
781+
optionName: 'dataSource',
782+
},
783+
{
784+
initialOptions: {
785+
deferRendering,
786+
items: [1],
787+
value: [1],
788+
},
789+
optionsToUpdate: {
790+
items: null,
791+
},
792+
expectedSelectedItems: [],
793+
optionName: 'items',
794+
},
795+
{
796+
initialOptions: {
797+
deferRendering,
798+
dataSource: [1],
799+
value: [1],
800+
},
801+
optionsToUpdate: {
802+
dataSource: null,
803+
},
804+
expectedSelectedItems: [],
805+
optionName: 'dataSource',
806+
},
807+
{
808+
initialOptions: {
809+
deferRendering,
810+
dataSource: [{ id: 1, text: 'one' }, { id: 2, text: 'two' }, { id: 3, text: 'three' }],
811+
value: [1, 3],
812+
valueExpr: 'id',
813+
},
814+
optionsToUpdate: {
815+
dataSource: [{ id: 1, text: 'one' }, { id: 2, text: 'two' }],
816+
},
817+
expectedSelectedItems: [{ id: 1, text: 'one' }],
818+
optionName: 'dataSource',
819+
},
820+
{
821+
initialOptions: {
822+
deferRendering,
823+
dataSource: new DataSource({ store: [{ id: 1, text: 'one' }, { id: 2, text: 'two' }, { id: 3, text: 'three' }] }),
824+
value: [1, 3],
825+
valueExpr: 'id',
826+
},
827+
optionsToUpdate: {
828+
dataSource: new DataSource({ store: [{ id: 1, text: 'one' }, { id: 2, text: 'two' }] }),
829+
},
830+
expectedSelectedItems: [{ id: 1, text: 'one' }],
831+
optionName: 'dataSource',
832+
},
833+
{
834+
initialOptions: {
835+
deferRendering,
836+
items: null,
837+
value: [1],
838+
},
839+
optionsToUpdate: {
840+
items: [1, 2],
841+
},
842+
expectedSelectedItems: [1],
843+
optionName: 'items',
844+
},
845+
{
846+
initialOptions: {
847+
deferRendering,
848+
dataSource: [{ id: 1, text: 'one' }, { id: 2, text: 'two' }],
849+
value: [1, 3],
850+
valueExpr: 'id',
851+
},
852+
optionsToUpdate: {
853+
dataSource: [{ id: 1, text: 'one' }, { id: 2, text: 'two' }, { id: 3, text: 'three' }],
854+
},
855+
expectedSelectedItems: [{ id: 1, text: 'one' }, { id: 3, text: 'three' }],
856+
optionName: 'dataSource',
857+
},
858+
].forEach(({ initialOptions, optionsToUpdate, expectedSelectedItems, optionName }) => {
859+
const source = initialOptions.dataSource instanceof DataSource ? 'DataSource' : JSON.stringify(initialOptions[optionName]);
860+
861+
QUnit.test(`SelectedItems should be updated correctly on runtime ${optionName} change (deferRendering=${deferRendering}, source=${source}) (T1253312)`, function(assert) {
862+
const tagBox = $('#tagBox').dxTagBox(initialOptions).dxTagBox('instance');
863+
864+
tagBox.option(optionsToUpdate);
865+
866+
assert.deepEqual(tagBox.option('selectedItems'), expectedSelectedItems, 'selectedItems are updated');
867+
});
868+
869+
QUnit.test(`Tags should be updated correctly on runtime ${optionName} change (deferRendering=${deferRendering}, source=${source}) (T1253312)`, function(assert) {
870+
const $tagBox = $('#tagBox').dxTagBox(initialOptions);
871+
const tagBox = $tagBox.dxTagBox('instance');
872+
873+
tagBox.option(optionsToUpdate);
874+
875+
const $tags = $tagBox.find(`.${TAGBOX_TAG_CLASS}`);
876+
877+
assert.strictEqual($tags.length, expectedSelectedItems.length, 'tags are updated');
878+
});
879+
});
880+
});
741881
});
742882

743883
QUnit.module('multi tag support', {
@@ -7038,7 +7178,7 @@ QUnit.module('performance', () => {
70387178
this.resetGetterCallCount();
70397179
$(`.${SELECT_ALL_CHECKBOX_CLASS}`).trigger('dxclick');
70407180

7041-
assert.strictEqual(this.getValueGetterCallCount(), 6254, 'key getter call count');
7181+
assert.strictEqual(this.getValueGetterCallCount(), 6354, 'key getter call count');
70427182
assert.strictEqual(isValueEqualsSpy.callCount, 5050, '_isValueEquals call count');
70437183
});
70447184

@@ -7049,7 +7189,7 @@ QUnit.module('performance', () => {
70497189
const checkboxes = $(`.${LIST_CHECKBOX_CLASS}`);
70507190
checkboxes.eq(checkboxes.length - 1).trigger('dxclick');
70517191

7052-
assert.strictEqual(this.getValueGetterCallCount(), 6052, 'key getter call count');
7192+
assert.strictEqual(this.getValueGetterCallCount(), 6151, 'key getter call count');
70537193
});
70547194
});
70557195

0 commit comments

Comments
 (0)