diff --git a/apps/demos/testing/common.test.js b/apps/demos/testing/common.test.js index d71bf8f3cc76..d7c0b22b13eb 100644 --- a/apps/demos/testing/common.test.js +++ b/apps/demos/testing/common.test.js @@ -133,9 +133,6 @@ const SKIPPED_TESTS = { ], }, React: { - Common: [ - { demo: 'ActionAndListsOverview', themes: [THEME.generic, THEME.material] }, - ], Charts: [ { demo: 'PiesWithEqualSize', themes: [THEME.material] }, { demo: 'CustomAnnotations', themes: [THEME.material] }, @@ -182,9 +179,6 @@ const SKIPPED_TESTS = { ], }, Vue: { - Common: [ - { demo: 'ActionAndListsOverview', themes: [THEME.generic, THEME.material] }, - ], Charts: [ { demo: 'TilingAlgorithms', themes: [THEME.material] }, { demo: 'ExportAndPrintingAPI', themes: [THEME.material] }, diff --git a/packages/devextreme/js/__internal/ui/collection/base.ts b/packages/devextreme/js/__internal/ui/collection/base.ts index 9928a0d14007..ee4c3b547cc8 100644 --- a/packages/devextreme/js/__internal/ui/collection/base.ts +++ b/packages/devextreme/js/__internal/ui/collection/base.ts @@ -47,6 +47,7 @@ declare class Base< container: dxElementWrapper; contentClass: string; defaultTemplateName: string; + uniqueKey?: string; }): dxElementWrapper; _renderContent(): void; _postprocessRenderItem(args: unknown): void; diff --git a/packages/devextreme/js/__internal/ui/collection/m_collection_widget.async.ts b/packages/devextreme/js/__internal/ui/collection/m_collection_widget.async.ts index 7660b180bc8d..e4c3dc9f1365 100644 --- a/packages/devextreme/js/__internal/ui/collection/m_collection_widget.async.ts +++ b/packages/devextreme/js/__internal/ui/collection/m_collection_widget.async.ts @@ -1,11 +1,13 @@ +import Guid from '@js/core/guid'; import { noop } from '@js/core/utils/common'; +import type { DeferredObj } from '@js/core/utils/deferred'; import { Deferred, when } from '@js/core/utils/deferred'; import CollectionWidgetEdit from './m_collection_widget.edit'; const AsyncCollectionWidget = CollectionWidgetEdit.inherit({ _initMarkup() { - this._asyncTemplateItems = []; + this._asyncTemplateItemsMap = {}; this.callBase(); }, @@ -17,9 +19,10 @@ const AsyncCollectionWidget = CollectionWidgetEdit.inherit({ _renderItemContent(args) { const renderContentDeferred = Deferred(); const itemDeferred = Deferred(); + const uniqueKey = `dx${new Guid()}`; - this._asyncTemplateItems[args.index] = itemDeferred; - const $itemContent = this.callBase(args); + this._asyncTemplateItemsMap[uniqueKey] = itemDeferred; + const $itemContent = this.callBase({ ...args, uniqueKey }); itemDeferred.done(() => { renderContentDeferred.resolve($itemContent); @@ -30,7 +33,7 @@ const AsyncCollectionWidget = CollectionWidgetEdit.inherit({ _onItemTemplateRendered(itemTemplate, renderArgs) { return () => { - this._asyncTemplateItems[renderArgs.index]?.resolve(); + this._asyncTemplateItemsMap[renderArgs.uniqueKey]?.resolve(); }; }, @@ -38,19 +41,29 @@ const AsyncCollectionWidget = CollectionWidgetEdit.inherit({ _planPostRenderActions(...args: unknown[]) { const d = Deferred(); - when.apply(this, this._asyncTemplateItems).done(() => { + const asyncTemplateItems = Object.values>(this._asyncTemplateItemsMap); + + when.apply(this, asyncTemplateItems).done(() => { this._postProcessRenderItems(...args); - d.resolve(); + + d.resolve().done(() => { + this._asyncTemplateItemsMap = {}; + }); }); + return d.promise(); }, _clean() { this.callBase(); - this._asyncTemplateItems.forEach((item) => { + + const asyncTemplateItems = Object.values>(this._asyncTemplateItemsMap); + + asyncTemplateItems.forEach((item) => { item.reject(); }); - this._asyncTemplateItems = []; + + this._asyncTemplateItemsMap = {}; }, }); diff --git a/packages/devextreme/js/__internal/ui/tree_view/m_tree_view.base.ts b/packages/devextreme/js/__internal/ui/tree_view/m_tree_view.base.ts index bab507e26c34..7bbcc586858a 100644 --- a/packages/devextreme/js/__internal/ui/tree_view/m_tree_view.base.ts +++ b/packages/devextreme/js/__internal/ui/tree_view/m_tree_view.base.ts @@ -1441,7 +1441,7 @@ const TreeViewBase = (HierarchicalCollectionWidget as any).inherit({ if (this._showCheckboxes()) { const parentValue = parentNode.internalFields.selected; - this._getCheckBoxInstance($parentNode).option('value', parentValue); + this._getCheckBoxInstance($parentNode)?.option('value', parentValue); this._toggleSelectedClass($parentNode, parentValue); } diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/listParts/commonTests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/listParts/commonTests.js index 73d02c4d7201..072dc0b5b7cc 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/listParts/commonTests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/listParts/commonTests.js @@ -3683,6 +3683,54 @@ QUnit.module('regressions', moduleSetup, () => { assert.equal(count, 1); }); + + QUnit.test('Selection: item selected correctly on async render with tree structure (T1269855)', function(assert) { + this.clock.restore(); + const done = assert.async(); + + const data = [ + { id: 1, name: 'Item 1_1', group: 'group_1' }, + { id: 2, name: 'Item 1_2', group: 'group_1' }, + { id: 3, name: 'Item 1_3', group: 'group_1' }, + { id: 4, name: 'Item 2_1', group: 'group_2' }, + ]; + + const dataSource = new DataSource({ + store: new ArrayStore({ data, key: 'id' }), + group: 'group', + }); + + const instance = new List($('#list'), { + dataSource, + grouped: true, + templatesRenderAsynchronously: true, + integrationOptions: { + templates: { + 'item': { + render: function({ model, container, onRendered }) { + setTimeout(function() { + const $item = $(`
${model.name}
`); + $item.appendTo(container); + + onRendered(); + }, 100); + } + }, + } + }, + selectionMode: 'single', + selectedItemKeys: [data[0].id], + }); + + instance.option('_onItemsRendered', () => { + const listElement = instance.element(); + const $firstGroup = $(listElement).find(`.${LIST_GROUP_CLASS}`).eq(0); + const $firstItemInFirstGroup = $firstGroup.find(`.${LIST_ITEM_CLASS}`).eq(0); + + assert.ok($firstItemInFirstGroup.hasClass(LIST_ITEM_SELECTED_CLASS), 'First item in first group should be selected'); + done(); + }); + }); }); QUnit.module('widget sizing render', {}, () => { diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/treeView.async.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/treeView.async.tests.js new file mode 100644 index 000000000000..9cba4f0d050b --- /dev/null +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/treeView.async.tests.js @@ -0,0 +1,122 @@ +import $ from 'jquery'; +import TreeView from 'ui/tree_view'; + +import 'generic_light.css!'; + +const { testStart } = QUnit; + +testStart(function() { + const markup = '
'; + + $('#qunit-fixture').html(markup); +}); + +const asyncTemplateRenderTimeout = 50; + +const CHECKBOX_CLASS = 'dx-checkbox'; +const CHECKBOX_CHECKED_CLASS = 'dx-checkbox-checked'; +const CHECKBOX_INDETERMINATE_CLASS = 'dx-checkbox-indeterminate'; +const TREEVIEW_ROOT_NODE_CLASS = 'dx-treeview-root-node'; + +QUnit.module('Async render', () => { + ['normal', 'selectAll'].forEach((showCheckBoxesMode) => { + QUnit.test(`TreeView checkboxed should be correctly rendered in async mode. checkboxMode: ${showCheckBoxesMode} (T1269855)`, function(assert) { + const done = assert.async(); + + const data = [ + { + id: 1, + text: 'Item 1', + expanded: true, + selected: true, + items: [ + { + id: 12, text: 'Nested Item 2', expanded: true, items: [ + { id: 121, text: 'Third level item 1' }, + { id: 122, text: 'Third level item 2' } + ] + } + ] + }, + { + id: 2, + text: 'Item 2', + expanded: true, + items: [ + { + id: 22, text: 'Nested Item 2', expanded: true, items: [ + { id: 221, text: 'Third level item 1' }, + { id: 222, text: 'Third level item 2', selected: true } + ] + } + ] + }, + { + id: 3, + text: 'Item 3', + expanded: true, + items: [ + { + id: 33, text: 'Nested Item 3', expanded: true, items: [ + { id: 331, text: 'Third level item 1' }, + { id: 332, text: 'Third level item 2' } + ] + } + ] + } + ]; + + const instance = new TreeView($('#treeView'), { + items: data, + showCheckBoxesMode, + templatesRenderAsynchronously: true, + itemTemplate: 'myTemplate', + integrationOptions: { + templates: { + myTemplate: { + render({ model, container, onRendered }) { + setTimeout(() => { + const $item = $(`
${model.text}
`); + $item.appendTo(container); + + onRendered(); + }); + } + } + } + }, + }); + + setTimeout(() => { + const element = instance.itemsContainer(); + const $treeRootNodes = $(element).find(`.${TREEVIEW_ROOT_NODE_CLASS}`); + + const $firstRootNode = $treeRootNodes.eq(0); + const $firstGroupCheckboxes = $firstRootNode.find(`.${CHECKBOX_CLASS}`); + + assert.ok($firstGroupCheckboxes.eq(0).hasClass(CHECKBOX_CHECKED_CLASS), 'First group root checkbox has selected class'); + assert.ok($firstGroupCheckboxes.eq(1).hasClass(CHECKBOX_CHECKED_CLASS), 'First group nested node checkbox has selected class'); + assert.ok($firstGroupCheckboxes.eq(2).hasClass(CHECKBOX_CHECKED_CLASS), 'First group leaf node 1 checkbox has selected class'); + assert.ok($firstGroupCheckboxes.eq(3).hasClass(CHECKBOX_CHECKED_CLASS), 'First group leaf node 2 checkbox has selected class'); + + const $secondRootNode = $treeRootNodes.eq(1); + const $secondGroupCheckboxes = $secondRootNode.find(`.${CHECKBOX_CLASS}`); + + assert.ok($secondGroupCheckboxes.eq(0).hasClass(CHECKBOX_INDETERMINATE_CLASS), 'Second group root checkbox has indeterminate class'); + assert.ok($secondGroupCheckboxes.eq(1).hasClass(CHECKBOX_INDETERMINATE_CLASS), 'Second group nested node checkbox has indeterminate class'); + assert.notOk($secondGroupCheckboxes.eq(2).hasClass(CHECKBOX_CHECKED_CLASS), 'Second group leaf node 1 checkbox has not selected class'); + assert.ok($secondGroupCheckboxes.eq(3).hasClass(CHECKBOX_CHECKED_CLASS), 'Second group leaf node 2 checkbox has selected class'); + + const $thirdRootNode = $treeRootNodes.eq(2); + const $thirdGroupCheckboxes = $thirdRootNode.find(`.${CHECKBOX_CLASS}`); + + assert.notOk($thirdGroupCheckboxes.eq(0).hasClass(CHECKBOX_CHECKED_CLASS), 'Third group root checkbox has not selected class'); + assert.notOk($thirdGroupCheckboxes.eq(1).hasClass(CHECKBOX_CHECKED_CLASS), 'Third group nested node checkbox has not selected class'); + assert.notOk($thirdGroupCheckboxes.eq(2).hasClass(CHECKBOX_CHECKED_CLASS), 'Third group leaf node 1 checkbox has not selected class'); + assert.notOk($thirdGroupCheckboxes.eq(3).hasClass(CHECKBOX_CHECKED_CLASS), 'Third group leaf node 2 checkbox has not selected class'); + + done(); + }, asyncTemplateRenderTimeout); + }); + }); +});