diff --git a/package.json b/package.json index f25cd6e2f1..50eba9c79f 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "eslint-plugin-import": "^2.24.2", "eslint-plugin-vue": "^7.11.1", "husky": "^7.0.4", + "intersection-observer": "^0.12.2", "lint-staged": "^11.0.0", "npm-run-all": "^4.1.5", "stylelint": "^13.13.1", diff --git a/packages/devui-vue/devui/table/__tests__/table.spec.tsx b/packages/devui-vue/devui/table/__tests__/table.spec.tsx index ad751403bb..24cf9c8fda 100644 --- a/packages/devui-vue/devui/table/__tests__/table.spec.tsx +++ b/packages/devui-vue/devui/table/__tests__/table.spec.tsx @@ -5,6 +5,7 @@ import { Button } from '../../button'; import { useNamespace } from '../../shared/hooks/use-namespace'; import { nextTick, ref } from 'vue'; import { Input } from '../../input'; +import 'intersection-observer'; let data: Array> = []; const ns = useNamespace('table', true); @@ -637,4 +638,40 @@ describe('d-table', () => { expect(inputNew.exists()).toBeFalsy(); expect(cellItem.text()).toBe('Mark1'); }); + + it('table lazy mode work', async () => { + const wrapper = mount({ + setup() { + const handleLoadMore = () => { + + // TODO: add exception to test emit event(Jest don't have IntersectionObserver) + // The 'intersection-observer' polyfill don't work in Jest env? It's just prevent error report in DTable. + data.push({ + firstName: 'loadMore', + lastName: 'loadMore', + gender: 'Female', + date: '1990/01/12', + }); + }; + + return () => ( + + + + + + + ); + }, + }); + + await nextTick(); + await nextTick(); + const table = wrapper.find(ns.b()); + const lazyEle = table.find(ns.e('lazy__flag')); + + // test lazyFlagElement exist + expect(lazyEle.exists()).toBeTruthy(); + wrapper.unmount(); + }); }); diff --git a/packages/devui-vue/devui/table/src/components/body/body-types.ts b/packages/devui-vue/devui/table/src/components/body/body-types.ts index 75aae6fe69..c6858ca663 100644 --- a/packages/devui-vue/devui/table/src/components/body/body-types.ts +++ b/packages/devui-vue/devui/table/src/components/body/body-types.ts @@ -1,4 +1,4 @@ -import type { ComputedRef } from 'vue'; +import type { ComputedRef, Ref } from 'vue'; import { DefaultRow } from '../../table-types'; import { Column } from '../column/column-types'; @@ -21,3 +21,8 @@ export interface UseMergeCell { export interface UseBodyRender { getTableRowClass: (row: DefaultRow) => Record; } + +export interface UseLazyLoad { + lazy: boolean; + lazyFlagRef: Ref; +} diff --git a/packages/devui-vue/devui/table/src/components/body/body.scss b/packages/devui-vue/devui/table/src/components/body/body.scss index 8703cf4f7e..d7082c894b 100644 --- a/packages/devui-vue/devui/table/src/components/body/body.scss +++ b/packages/devui-vue/devui/table/src/components/body/body.scss @@ -77,6 +77,12 @@ $input-height-md: 28px; .is-hidden { display: none; } + + } + + &__lazy__flag { + width: 0; + height: 0; } } diff --git a/packages/devui-vue/devui/table/src/components/body/body.tsx b/packages/devui-vue/devui/table/src/components/body/body.tsx index 18ee573ffd..e828532a39 100644 --- a/packages/devui-vue/devui/table/src/components/body/body.tsx +++ b/packages/devui-vue/devui/table/src/components/body/body.tsx @@ -4,7 +4,7 @@ import { Column } from '../column/column-types'; import { CellClickArg, RowClickArg } from './body-types'; import TD from '../body-td/body-td'; import { useNamespace } from '../../../../shared/hooks/use-namespace'; -import { useMergeCell, useBodyRender } from './use-body'; +import { useMergeCell, useBodyRender, useLazyLoad } from './use-body'; import './body.scss'; export default defineComponent({ @@ -15,6 +15,7 @@ export default defineComponent({ const ns = useNamespace('table'); const { tableSpans, removeCells } = useMergeCell(); const { getTableRowClass } = useBodyRender(); + const { lazy, lazyFlagRef } = useLazyLoad(); const onCellClick = (cellClickArg: CellClickArg) => { table.emit('cell-click', cellClickArg); }; @@ -59,6 +60,7 @@ export default defineComponent({ )} ))} + {lazy && } ); }, diff --git a/packages/devui-vue/devui/table/src/components/body/use-body.ts b/packages/devui-vue/devui/table/src/components/body/use-body.ts index af5da42231..06df48fa18 100644 --- a/packages/devui-vue/devui/table/src/components/body/use-body.ts +++ b/packages/devui-vue/devui/table/src/components/body/use-body.ts @@ -1,7 +1,7 @@ -import { inject, computed, Ref } from 'vue'; +import { inject, computed, Ref, ref, onMounted, onBeforeUnmount } from 'vue'; import { TABLE_TOKEN, ITableInstanceAndDefaultRow } from '../../table-types'; import { useNamespace } from '../../../../shared/hooks/use-namespace'; -import { UseBodyRender, UseMergeCell, CellClickArg } from './body-types'; +import { UseBodyRender, UseMergeCell, CellClickArg, UseLazyLoad } from './body-types'; import { getRowIdentity } from '../../utils'; const ns = useNamespace('table'); @@ -87,3 +87,56 @@ export function useBodyRender(): UseBodyRender { return { getTableRowClass }; } + +export function useLazyLoad(): UseLazyLoad { + const table = inject(TABLE_TOKEN) as ITableInstanceAndDefaultRow; + const { lazy } = table.props; + + // set an empty tag in the bottom of tbody. when the tbody scroll to bottom. The empty tag's intersectionObserver will be triggered. + const lazyFlagRef = ref(); + let lazyObserver: IntersectionObserver; + + onMounted(() => { + + // if lazy mode is turn on. It'll observe an empty tag to determine whether the bottom has been reached. + if (lazy) { + + // when the tbody reached bottom(because the lazyFlagElement is at the bottom of tbody), the observe's callback will be triggered + lazyObserver = new IntersectionObserver( + (entries) => { + + // Not support IE + const lazyFlagOb = entries[0]; + + // isIntersecting is true ==> the lazyFlagElement is in viewport + if (lazyFlagOb.isIntersecting) { + + // exec user props.loadMore to load more data + // loadMore(); + table.emit('load-more'); + } + }, + { + + // only fired in the table scroll event + root: table.tableRef.value, + } + ); + lazyObserver.observe(lazyFlagRef.value); + } + }); + + onBeforeUnmount(() => { + + // unload intersectionObserver + if (lazy) { + lazyObserver.unobserve(lazyFlagRef.value); + lazyObserver.disconnect(); + } + }); + + return { + lazy, + lazyFlagRef, + }; +} diff --git a/packages/devui-vue/devui/table/src/table-types.ts b/packages/devui-vue/devui/table/src/table-types.ts index 34abf5b8da..2acc4d9d3c 100644 --- a/packages/devui-vue/devui/table/src/table-types.ts +++ b/packages/devui-vue/devui/table/src/table-types.ts @@ -101,6 +101,10 @@ export const tableProps = { type: Number, default: 16, }, + lazy: { + type: Boolean, + default: false, + }, }; export type TableProps = ExtractPropTypes; diff --git a/packages/devui-vue/devui/table/src/table.tsx b/packages/devui-vue/devui/table/src/table.tsx index 4c2c555df8..14fd3a0a64 100644 --- a/packages/devui-vue/devui/table/src/table.tsx +++ b/packages/devui-vue/devui/table/src/table.tsx @@ -16,7 +16,7 @@ export default defineComponent({ dLoading: Loading, }, props: tableProps, - emits: ['sort-change', 'cell-click', 'row-click', 'check-change', 'check-all-change', 'expand-change'], + emits: ['sort-change', 'cell-click', 'row-click', 'check-change', 'check-all-change', 'expand-change', 'load-more'], setup(props: TableProps, ctx) { const table = getCurrentInstance() as ITableInstanceAndDefaultRow; const store = createStore(toRef(props, 'data'), table); diff --git a/packages/devui-vue/docs/components/table/index.md b/packages/devui-vue/docs/components/table/index.md index 24ccae5870..11698a42c3 100644 --- a/packages/devui-vue/docs/components/table/index.md +++ b/packages/devui-vue/docs/components/table/index.md @@ -1367,6 +1367,123 @@ export default defineComponent({ ::: +### 懒加载 + +:::demo 使用lazy启用懒加载,当滚动表格底部时到触发loadMore事件实现懒加载。 + +```vue + + + +``` + +::: + ### Table 参数 | 参数名 | 类型 | 默认值 | 说明 | 跳转 Demo | @@ -1389,7 +1506,7 @@ export default defineComponent({ | show-header | `boolean` | true | 可选,配置是否显示表头 | [表格样式](#表格样式) | | row-key | `string \| Function(item, index: number): string` | -- | 可选,行数据的 Key,用来优化 Table 渲染,类型为 string 时,支持多层访问:`item.user.id`,但不支持 `item.user[0].id`,此种情况请使用 Function。 | [表格交互(Function)](#表格交互)
[展开行(string)](#展开行) | | indent | `number` | 16 | 可选,展示树形数据时,树节点的缩进 | [树形表格](#树形表格) | - +| lazy | `boolean` | false | 可选,是否懒加载数据(搭配loadMore使用) | [懒加载](#懒加载) | ### Table 事件 | 事件名 | 回调参数 | 说明 | 跳转 Demo | @@ -1399,6 +1516,7 @@ export default defineComponent({ | check-change | `Function(checked: boolean, row, selection)` | 勾选表格行回调事件,返回该行信息和表格所有选中行数据 | [表格交互](#表格交互) | | check-all-change | `Function(checked: boolean, selection)` | 全选表格行回调事件,返回勾选状态和表格所有选中行数据 | [表格交互](#表格交互) | | row-click | `Function(obj: RowClickArg)` | 某一行被点击时触发该事件,返回该行信息 | [表格交互](#表格交互) | +| load-more | `Function()` | 滚动到表格底部触发懒加载事件(需配合props.lazy开启) | [懒加载](#懒加载) | ### Table 方法 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0502454c65..dc81cfe58b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: 5.4 +lockfileVersion: 5.3 importers: @@ -12,6 +12,7 @@ importers: eslint-plugin-import: ^2.24.2 eslint-plugin-vue: ^7.11.1 husky: ^7.0.4 + intersection-observer: ^0.12.2 lint-staged: ^11.0.0 npm-run-all: ^4.1.5 stylelint: ^13.13.1 @@ -27,10 +28,11 @@ importers: eslint-plugin-import: 2.25.4_eslint@7.32.0 eslint-plugin-vue: 7.20.0_eslint@7.32.0 husky: 7.0.4 + intersection-observer: registry.npmmirror.com/intersection-observer/0.12.2 lint-staged: 11.2.6 npm-run-all: 4.1.5 stylelint: 13.13.1 - stylelint-config-recommended-scss: 4.3.0_2vkgt733dnumio3be4grtjqkwy + stylelint-config-recommended-scss: 4.3.0_d55469ff7b1b68c43b61270d19a60ab6 stylelint-config-standard: 22.0.0_stylelint@13.13.1 stylelint-scss: 3.21.0_stylelint@13.13.1 @@ -2278,7 +2280,7 @@ packages: '@sinonjs/commons': 1.8.3 dev: true - /@stylelint/postcss-css-in-js/0.37.2_j55xdkkcxc32kvnyvx3y7casfm: + /@stylelint/postcss-css-in-js/0.37.2_4f7b71a942b8b7a555b8adf78f88122b: resolution: {integrity: sha512-nEhsFoJurt8oUmieT8qy4nk81WRHmJynmVwn/Vts08PL9fhgIsMhk1GId5yAN643OzqEEb5S/6At2TZW7pqPDA==} peerDependencies: postcss: '>=7.0.0' @@ -2286,12 +2288,12 @@ packages: dependencies: '@babel/core': 7.17.5 postcss: 7.0.39 - postcss-syntax: 0.36.2_kei4jy7wdgbhc236h4oijypxom + postcss-syntax: 0.36.2_postcss@7.0.39 transitivePeerDependencies: - supports-color dev: true - /@stylelint/postcss-markdown/0.36.2_j55xdkkcxc32kvnyvx3y7casfm: + /@stylelint/postcss-markdown/0.36.2_4f7b71a942b8b7a555b8adf78f88122b: resolution: {integrity: sha512-2kGbqUVJUGE8dM+bMzXG/PYUWKkjLIkRLWNh39OaADkiabDRdw8ATFCgbMz5xdIcvwspPAluSL7uY+ZiTWdWmQ==} deprecated: 'Use the original unforked package instead: postcss-markdown' peerDependencies: @@ -2299,7 +2301,7 @@ packages: postcss-syntax: '>=0.36.2' dependencies: postcss: 7.0.39 - postcss-syntax: 0.36.2_kei4jy7wdgbhc236h4oijypxom + postcss-syntax: 0.36.2_postcss@7.0.39 remark: 13.0.0 unist-util-find-all-after: 3.0.2 transitivePeerDependencies: @@ -4089,22 +4091,12 @@ packages: /debug/2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true dependencies: ms: 2.0.0 dev: true /debug/3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true dependencies: ms: 2.1.3 dev: true @@ -4797,44 +4789,21 @@ packages: dependencies: debug: 3.2.7 resolve: 1.22.0 - transitivePeerDependencies: - - supports-color dev: true - /eslint-module-utils/2.7.3_ulu2225r2ychl26a37c6o2rfje: + /eslint-module-utils/2.7.3: resolution: {integrity: sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ==} engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint-import-resolver-node: '*' - eslint-import-resolver-typescript: '*' - eslint-import-resolver-webpack: '*' - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - eslint-import-resolver-node: - optional: true - eslint-import-resolver-typescript: - optional: true - eslint-import-resolver-webpack: - optional: true dependencies: debug: 3.2.7 - eslint-import-resolver-node: 0.3.6 find-up: 2.1.0 - transitivePeerDependencies: - - supports-color dev: true /eslint-plugin-import/2.25.4_eslint@7.32.0: resolution: {integrity: sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA==} engines: {node: '>=4'} peerDependencies: - '@typescript-eslint/parser': '*' eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true dependencies: array-includes: 3.1.4 array.prototype.flat: 1.2.5 @@ -4842,7 +4811,7 @@ packages: doctrine: 2.1.0 eslint: 7.32.0 eslint-import-resolver-node: 0.3.6 - eslint-module-utils: 2.7.3_ulu2225r2ychl26a37c6o2rfje + eslint-module-utils: 2.7.3 has: 1.0.3 is-core-module: 2.8.1 is-glob: 4.0.3 @@ -4850,10 +4819,6 @@ packages: object.values: 1.1.5 resolve: 1.22.0 tsconfig-paths: 3.12.0 - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color dev: true /eslint-plugin-vue/7.20.0_eslint@7.32.0: @@ -6545,7 +6510,7 @@ packages: dependencies: universalify: 2.0.0 optionalDependencies: - graceful-fs: 4.2.9 + graceful-fs: registry.npmmirror.com/graceful-fs/4.2.9 /jsonparse/1.3.1: resolution: {integrity: sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=} @@ -7408,7 +7373,7 @@ packages: trouter: 2.0.1 dev: true - /postcss-html/0.36.0_j55xdkkcxc32kvnyvx3y7casfm: + /postcss-html/0.36.0_4f7b71a942b8b7a555b8adf78f88122b: resolution: {integrity: sha512-HeiOxGcuwID0AFsNAL0ox3mW6MHH5cstWN1Z3Y+n6H+g12ih7LHdYxWwEA/QmrebctLjo79xz9ouK3MroHwOJw==} peerDependencies: postcss: '>=5.0.0' @@ -7416,7 +7381,7 @@ packages: dependencies: htmlparser2: 3.10.1 postcss: 7.0.39 - postcss-syntax: 0.36.2_kei4jy7wdgbhc236h4oijypxom + postcss-syntax: 0.36.2_postcss@7.0.39 dev: true /postcss-less/3.1.4: @@ -7463,31 +7428,12 @@ packages: util-deprecate: 1.0.2 dev: true - /postcss-syntax/0.36.2_kei4jy7wdgbhc236h4oijypxom: + /postcss-syntax/0.36.2_postcss@7.0.39: resolution: {integrity: sha512-nBRg/i7E3SOHWxF3PpF5WnJM/jQ1YpY9000OaVXlAQj6Zp/kIqJxEDWIZ67tAd7NLuk7zqN4yqe9nc0oNAOs1w==} peerDependencies: postcss: '>=5.0.0' - postcss-html: '*' - postcss-jsx: '*' - postcss-less: '*' - postcss-markdown: '*' - postcss-scss: '*' - peerDependenciesMeta: - postcss-html: - optional: true - postcss-jsx: - optional: true - postcss-less: - optional: true - postcss-markdown: - optional: true - postcss-scss: - optional: true dependencies: postcss: 7.0.39 - postcss-html: 0.36.0_j55xdkkcxc32kvnyvx3y7casfm - postcss-less: 3.1.4 - postcss-scss: 2.1.1 dev: true /postcss-value-parser/4.2.0: @@ -8319,7 +8265,7 @@ packages: resolution: {integrity: sha1-eVjHk+R+MuB9K1yv5cC/jhLneQI=} dev: true - /stylelint-config-recommended-scss/4.3.0_2vkgt733dnumio3be4grtjqkwy: + /stylelint-config-recommended-scss/4.3.0_d55469ff7b1b68c43b61270d19a60ab6: resolution: {integrity: sha512-/noGjXlO8pJTr/Z3qGMoaRFK8n1BFfOqmAbX1RjTIcl4Yalr+LUb1zb9iQ7pRx1GsEBXOAm4g2z5/jou/pfMPg==} peerDependencies: stylelint: ^10.1.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 @@ -8366,8 +8312,8 @@ packages: engines: {node: '>=10.13.0'} hasBin: true dependencies: - '@stylelint/postcss-css-in-js': 0.37.2_j55xdkkcxc32kvnyvx3y7casfm - '@stylelint/postcss-markdown': 0.36.2_j55xdkkcxc32kvnyvx3y7casfm + '@stylelint/postcss-css-in-js': 0.37.2_4f7b71a942b8b7a555b8adf78f88122b + '@stylelint/postcss-markdown': 0.36.2_4f7b71a942b8b7a555b8adf78f88122b autoprefixer: 9.8.8 balanced-match: 2.0.0 chalk: 4.1.2 @@ -8393,7 +8339,7 @@ packages: micromatch: 4.0.4 normalize-selector: 0.2.0 postcss: 7.0.39 - postcss-html: 0.36.0_j55xdkkcxc32kvnyvx3y7casfm + postcss-html: 0.36.0_4f7b71a942b8b7a555b8adf78f88122b postcss-less: 3.1.4 postcss-media-query-parser: 0.2.3 postcss-resolve-nested-selector: 0.1.1 @@ -8401,7 +8347,7 @@ packages: postcss-sass: 0.4.4 postcss-scss: 2.1.1 postcss-selector-parser: 6.0.9 - postcss-syntax: 0.36.2_kei4jy7wdgbhc236h4oijypxom + postcss-syntax: 0.36.2_postcss@7.0.39 postcss-value-parser: 4.2.0 resolve-from: 5.0.0 slash: 3.0.0 @@ -8415,8 +8361,6 @@ packages: v8-compile-cache: 2.3.0 write-file-atomic: 3.0.3 transitivePeerDependencies: - - postcss-jsx - - postcss-markdown - supports-color dev: true @@ -9492,3 +9436,16 @@ packages: /zwitch/1.0.5: resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==} dev: true + + registry.npmmirror.com/graceful-fs/4.2.9: + resolution: {integrity: sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.9.tgz} + name: graceful-fs + version: 4.2.9 + requiresBuild: true + optional: true + + registry.npmmirror.com/intersection-observer/0.12.2: + resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/intersection-observer/-/intersection-observer-0.12.2.tgz} + name: intersection-observer + version: 0.12.2 + dev: true