feat(components): [skeleton] throttle supports values ​​as object (#17041)

* fix(components): [skeleton] `throttle` property not working

* fix: lint fix

* feat: add func & doc & test

* feat: remove test modify

* feat: increase document examples, improve document descriptions

* fix(components): [skeleton] `throttle` property not working

* fix: lint fix

* feat: add func & doc & test

* feat: remove test modify

* feat: increase document examples, improve document descriptions

* feat: 重构`useThrottleRender`钩子以提高代码可读性和效率

- 简化了对`throttle`参数的判断逻辑,通过`isNumber`函数判断是否为数字
- 将`leadingDispatch`和`trailingDispatch`函数合并为`dispatcher`函数,根据传入的类型判断执行逻辑
- 优化了`watch`回调函数,使用`dispatcher`函数替代重复的判断逻辑

* feat: 写法优化

* feat: 引入`isObject`函数替代原有的`typeof throttle === 'object'`判断方式

* feat: 优化骨架屏文档结构和示例

* feat: 完善文字描述和修改对应的文件名

* Update docs/en-US/component/skeleton.md

Co-authored-by: btea <2356281422@qq.com>

* Update docs/en-US/component/skeleton.md

Co-authored-by: btea <2356281422@qq.com>

* feat: Optimize code writing

* Update docs/en-US/component/skeleton.md

Co-authored-by: btea <2356281422@qq.com>

* Update docs/en-US/component/skeleton.md

* feat: modify doc

* feat: md

* feat: 补充 useThrottleRender 钩子的测试用例

* feat: 将 SkeletonThrottle 类型移动到hook中, 重命名为 ThrottleType 以提高通用性

---------

Co-authored-by: btea <2356281422@qq.com>
This commit is contained in:
chenweiyi 2024-11-07 21:31:16 +08:00 committed by GitHub
parent 89731b7d1f
commit 3eb734ccc4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 236 additions and 21 deletions

View File

@ -79,23 +79,57 @@ skeleton/rendering-with-data
Sometimes API responds very quickly, when that happens, the skeleton just gets rendered to the DOM then it needs to switch back to real DOM, that causes the sudden flashy. To avoid such thing, you can use the `throttle` attribute.
:::tip
Since ^(2.8.8), the `throttle` attribute supports two values: `number` and `object`. When passing a `number`, it is equivalent to `{leading: xxx}`, controlling the throttling of the skeleton screen display. Of course, you can also control the throttling of the skeleton screen disappearance by passing `{trailing: xxx}`
:::
:::demo
skeleton/avoiding-rendering-bouncing
:::
## Initial rendering loading ^(2.8.8)
When the initial value of loading is true, you can set `throttle: {initVal: true, leading: xxx}` to control the immediate display of the initial skeleton screen without throttling.
:::demo
skeleton/initial-rendering-loading
:::
## Toggle show/hide without rending bouncing ^(2.8.8)
:::tip
You can set `throttle: {initVal: true, leading: xxx, trailing: xxx}` to control the initial display of the skeleton effect and to make the transition of the skeleton more smooth when switching loading states.
:::
Sometimes you want to render the business components more smoothly when loading toggle show or hide. You can use set the `throttle: {leading: xxx, trailing:xxx}` to control the rendering bouncing.
:::demo
skeleton/leading-trailing-without-bouncing
:::
##
## Skeleton API
### Skeleton Attributes
| Name | Description | Type | Default |
| -------- | ---------------------------------------------------------------- | ---------- | ------- |
| animated | whether showing the animation | ^[boolean] | false |
| count | how many fake items to render to the DOM | ^[number] | 1 |
| loading | whether showing the real DOM | ^[boolean] | false |
| rows | numbers of the row, only useful when no template slot were given | ^[number] | 3 |
| throttle | rendering delay in milliseconds | ^[number] | 0 |
| Name | Description | Type | Default |
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ------- |
| animated | whether showing the animation | ^[boolean] | false |
| count | how many fake items to render to the DOM | ^[number] | 1 |
| loading | whether showing the real DOM | ^[boolean] | false |
| rows | numbers of the row, only useful when no template slot were given | ^[number] | 3 |
| throttle | rendering delay in milliseconds. Numbers represent delayed display, and can also be set to delay hide, for example `{ leading: 500, trailing: 500 }`. When needing to control the initial value of loading, you can set `{ initVal: true }` | ^[number] / ^[object]`{ leading?: number, trailing?: number, initVal?: boolean }` | 0 |
### Skeleton Slots

View File

@ -11,7 +11,7 @@
:throttle="500"
>
<template #template>
<el-skeleton-item variant="image" style="width: 240px; height: 240px" />
<el-skeleton-item variant="image" style="width: 240px; height: 265px" />
<div style="padding: 14px">
<el-skeleton-item variant="h3" style="width: 50%" />
<div

View File

@ -0,0 +1,55 @@
<template>
<el-space direction="vertical" alignment="flex-start">
<div>
<label style="margin-right: 16px">Switch Loading</label>
<el-switch v-model="loading" />
</div>
<el-skeleton
style="width: 240px"
:loading="loading"
animated
:throttle="{ leading: 500, initVal: true }"
>
<template #template>
<el-skeleton-item variant="image" style="width: 240px; height: 265px" />
<div style="padding: 14px">
<el-skeleton-item variant="h3" style="width: 50%" />
<div
style="
display: flex;
align-items: center;
justify-items: space-between;
margin-top: 16px;
height: 16px;
"
>
<el-skeleton-item variant="text" style="margin-right: 16px" />
<el-skeleton-item variant="text" style="width: 30%" />
</div>
</div>
</template>
<template #default>
<el-card :body-style="{ padding: '0px', marginBottom: '1px' }">
<img
src="https://shadow.elemecdn.com/app/element/hamburger.9cf7b091-55e9-11e9-a976-7f4d0b07eef6.png"
class="image"
/>
<div style="padding: 14px">
<span>Delicious hamburger</span>
<div class="bottom card-header">
<div class="time">{{ currentDate }}</div>
<el-button text class="button">operation button</el-button>
</div>
</div>
</el-card>
</template>
</el-skeleton>
</el-space>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const loading = ref(true)
const currentDate = new Date().toDateString()
</script>

View File

@ -0,0 +1,55 @@
<template>
<el-space direction="vertical" alignment="flex-start">
<div>
<label style="margin-right: 16px">Switch Loading</label>
<el-switch v-model="loading" />
</div>
<el-skeleton
style="width: 240px"
:loading="loading"
animated
:throttle="{ leading: 500, trailing: 500, initVal: true }"
>
<template #template>
<el-skeleton-item variant="image" style="width: 240px; height: 265px" />
<div style="padding: 14px">
<el-skeleton-item variant="h3" style="width: 50%" />
<div
style="
display: flex;
align-items: center;
justify-items: space-between;
margin-top: 16px;
height: 16px;
"
>
<el-skeleton-item variant="text" style="margin-right: 16px" />
<el-skeleton-item variant="text" style="width: 30%" />
</div>
</div>
</template>
<template #default>
<el-card :body-style="{ padding: '0px', marginBottom: '1px' }">
<img
src="https://shadow.elemecdn.com/app/element/hamburger.9cf7b091-55e9-11e9-a976-7f4d0b07eef6.png"
class="image"
/>
<div style="padding: 14px">
<span>Delicious hamburger</span>
<div class="bottom card-header">
<div class="time">{{ currentDate }}</div>
<el-button text class="button">operation button</el-button>
</div>
</div>
</el-card>
</template>
</el-skeleton>
</el-space>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const loading = ref(false)
const currentDate = new Date().toDateString()
</script>

View File

@ -83,4 +83,22 @@ describe('Skeleton.vue', () => {
expect((wrapper.vm as SkeletonInstance).uiLoading).toBe(true)
})
it('should throttle object rendering', async () => {
const wrapper = mount(
<Skeleton throttle={{ trailing: 500, initVal: true }} loading={true} />
)
expect((wrapper.vm as SkeletonInstance).uiLoading).toBe(true)
await wrapper.setProps({
loading: false,
})
vi.runAllTimers()
await nextTick()
expect((wrapper.vm as SkeletonInstance).uiLoading).toBe(false)
})
})

View File

@ -1,6 +1,7 @@
import { buildProps } from '@element-plus/utils'
import { buildProps, definePropType } from '@element-plus/utils'
import type Skeleton from './skeleton.vue'
import type { ExtractPropTypes } from 'vue'
import type { ThrottleType } from '@element-plus/hooks'
export const skeletonProps = buildProps({
/**
@ -35,7 +36,7 @@ export const skeletonProps = buildProps({
* @description rendering delay in milliseconds
*/
throttle: {
type: Number,
type: definePropType<ThrottleType>([Number, Object]),
},
} as const)
export type SkeletonProps = ExtractPropTypes<typeof skeletonProps>

View File

@ -2,7 +2,7 @@
<template v-if="uiLoading">
<div :class="[ns.b(), ns.is('animated', animated)]" v-bind="$attrs">
<template v-for="i in count" :key="i">
<slot v-if="loading" :key="i" name="template">
<slot v-if="uiLoading" :key="i" name="template">
<el-skeleton-item :class="ns.is('first')" variant="p" />
<el-skeleton-item
v-for="item in rows"

View File

@ -47,4 +47,29 @@ describe.concurrent('useThrottleRender', () => {
loading.value = true
expect(throttled.value).toBe(false) // should still be false after throttle time
})
it('should use `initVal` as initial value when pass `{ initVal: true/false }`', async () => {
const loading = ref(false)
const throttled = useThrottleRender(loading, { initVal: true })
expect(throttled.value).toBe(true)
const throttled2 = useThrottleRender(loading, { initVal: false })
expect(throttled2.value).toBe(false)
})
it('should throttle on display and disappear when pass `{ leading: xxx, trailing: xxx }`', async () => {
const loading = ref(false)
const throttled = useThrottleRender(loading, {
leading: 200,
trailing: 200,
})
expect(throttled.value).toBe(false) // initially false when not pass initVal
loading.value = true
expect(throttled.value).toBe(false) // should remain false until throttle time
await sleep(250) // Here, the delay time cannot be set to 200, setTimeout is not so precise, you can set it a little larger.
expect(throttled.value).toBe(true) // should be true after throttle time
loading.value = false
expect(throttled.value).toBe(true) // should remain true until throttle time
await sleep(250) // Here, the delay time cannot be set to 200, setTimeout is not so precise, you can set it a little larger.
expect(throttled.value).toBe(false) // should be false after throttle time
})
})

View File

@ -1,31 +1,58 @@
import { onMounted, ref, watch } from 'vue'
import { isNumber, isObject, isUndefined } from '@element-plus/utils'
import type { Ref } from 'vue'
export const useThrottleRender = (loading: Ref<boolean>, throttle = 0) => {
export type ThrottleType =
| { leading?: number; trailing?: number; initVal?: boolean }
| number
export const useThrottleRender = (
loading: Ref<boolean>,
throttle: ThrottleType = 0
) => {
if (throttle === 0) return loading
const throttled = ref(false)
const initVal = isObject(throttle) && Boolean(throttle.initVal)
const throttled = ref(initVal)
let timeoutHandle: ReturnType<typeof setTimeout> | null = null
const dispatchThrottling = () => {
const dispatchThrottling = (timer: number | undefined) => {
if (isUndefined(timer)) {
throttled.value = loading.value
return
}
if (timeoutHandle) {
clearTimeout(timeoutHandle)
}
timeoutHandle = setTimeout(() => {
throttled.value = loading.value
}, throttle)
}, timer)
}
onMounted(dispatchThrottling)
const dispatcher = (type: 'leading' | 'trailing') => {
if (type === 'leading') {
if (isNumber(throttle)) {
dispatchThrottling(throttle)
} else {
dispatchThrottling(throttle.leading)
}
} else {
if (isObject(throttle)) {
dispatchThrottling(throttle.trailing)
} else {
throttled.value = false
}
}
}
onMounted(() => dispatcher('leading'))
watch(
() => loading.value,
(val) => {
if (val) {
dispatchThrottling()
} else {
throttled.value = val
}
dispatcher(val ? 'leading' : 'trailing')
}
)
return throttled
}