Skip to content

Commit

Permalink
[#2785] Throttling composable, integration to VaSelect and VaDataTable (
Browse files Browse the repository at this point in the history
  • Loading branch information
aluarius authored Jan 31, 2023
1 parent 9dc0c8e commit c8d0a30
Show file tree
Hide file tree
Showing 13 changed files with 158 additions and 11 deletions.
1 change: 1 addition & 0 deletions packages/docs/src/locales/en/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@
"fallbackText": "Shows an alternative text if image failed to load or src doesn't specified.",
"fallbackIcon": "Shows an icon if image failed to load or src doesn't specified.",
"fallbackRender": "Allows to use render function to render custom contents if image failed to load or src doesn't specified",
"delay": "Sets throttling delay (ms) for the components any data change (useful for huge data).",
"ratio": "Aspect ratio of the component's wrapper."
}
},
Expand Down
1 change: 1 addition & 0 deletions packages/docs/src/locales/ru/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@
"fallbackText": "Показывает альтернативный текст, если изображение не указано или не удалось загрузить.",
"fallbackIcon": "Показывает альтернативную иконку, если изображение не указано или не удалось загрузить.",
"fallbackRender": "Позволяет задать функцию-рендерер с настраиваемым содержимым, если изображение не указано или не удалось загрузить.",
"delay": "Устанавливает задержку (тротлинг в ms) изменения данных в компоненте (при рендеринге больших объемов данных).",
"ratio": "Соотношение сторон обертки компонента."
}
},
Expand Down
4 changes: 3 additions & 1 deletion packages/docs/src/locales/zh-cn/zh-cn.json
Original file line number Diff line number Diff line change
Expand Up @@ -179,10 +179,12 @@
"plain": "Applies `plain` styling.",
"round": "Adds rounded corners (or make a button fully rounded if only icon is passed).",
"iconRight": "The icon to be displayed to the right of a title.",
"keyboardNavigation": "Enables keyboard navigation for the component.",
"fallbackSrc": "Shows an alternative image if original image failed to load or src doesn't specified.",
"fallbackText": "Shows an alternative text if image failed to load or src doesn't specified.",
"fallbackIcon": "Shows an icon if image failed to load or src doesn't specified.",
"fallbackRender": "Allows to use render function to render custom contents if image failed to load or src doesn't specified"
"fallbackRender": "Allows to use render function to render custom contents if image failed to load or src doesn't specified",
"delay": "Sets throttling delay (ms) for the components any data change (useful for huge data)."
}
},
"VaBadge": {
Expand Down
13 changes: 13 additions & 0 deletions packages/ui/src/components/va-data-table/VaDataTable.new.demo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,19 @@
</va-data-table>
</VbCard>

<VbCard title="Throttling (in the example below - for columns sorting)">
<va-data-table
v-model:sort-by="sortBy"
v-model:sorting-order="sortingOrder"
:items="items"
:columns="columns"
:delay="1000"
>
<template #header(street)="{ label }">{{ label }}</template>
<template #header(companyName)>Company Name</template>
</va-data-table>
</VbCard>

<VbCard title="Use `columns` prop, enable sorting and use custom sorting function (always returns -1) for the `id` column">
<label for="sortingBy">Sort by</label>
<select id="sortingBy" v-model="sortBy">
Expand Down
3 changes: 2 additions & 1 deletion packages/ui/src/components/va-data-table/VaDataTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ import { useFilterable, useFilterableProps } from './hooks/useFilterable'
import { useSortable, useSortableProps } from './hooks/useSortable'
import { useTableScroll, useTableScrollProps, useTableScrollEmits } from './hooks/useTableScroll'
import { useComponentPresetProp, useTranslation } from '../../composables'
import { useComponentPresetProp, useTranslation, useThrottleProps } from '../../composables'
import { extractComponentProps, filterComponentProps } from '../../utils/component-options'
Expand Down Expand Up @@ -240,6 +240,7 @@ export default defineComponent({
...usePaginatedRowsProps,
...useRowsProps,
...useSelectableProps,
...useThrottleProps,
hoverable: { type: Boolean, default: false },
clickable: { type: Boolean, default: false },
loading: { type: Boolean, default: false },
Expand Down
13 changes: 9 additions & 4 deletions packages/ui/src/components/va-data-table/hooks/useFilterable.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { Ref, watch, computed, PropType, ExtractPropTypes } from 'vue'

import { useThrottleValue, useThrottleProps } from '../../../composables'

import type { DataTableRow, DataTableFilterMethod, DataTableItem } from '../types'

export const useFilterableProps = {
...useThrottleProps,
filter: { type: String, default: '' },
filterMethod: { type: Function as PropType<DataTableFilterMethod | undefined> },
}
Expand Down Expand Up @@ -31,14 +34,16 @@ export const useFilterable = (
}))
})

watch(filteredRows, () => {
const filteredRowsThrottled = useThrottleValue(filteredRows, props)

watch(filteredRowsThrottled, () => {
emit('filtered', {
items: filteredRows.value.map(row => row.source),
itemsIndexes: filteredRows.value.map(row => row.initialIndex),
items: filteredRowsThrottled.value.map(row => row.source),
itemsIndexes: filteredRowsThrottled.value.map(row => row.initialIndex),
})
})

return {
filteredRows,
filteredRows: filteredRowsThrottled,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import { Ref, computed, PropType, ExtractPropTypes } from 'vue'

import { useCurrentPageProp } from './useCommonProps'

import { useThrottleValue, useThrottleProps } from '../../../composables'

import type { DataTableRow } from '../types'

export const usePaginatedRowsProps = {
...useThrottleProps,
...useCurrentPageProp,
perPage: { type: Number as PropType<number | undefined> },
}
Expand All @@ -26,7 +29,9 @@ export const usePaginatedRows = (
return sortedRows.value.slice(pageStartIndex, pageStartIndex + props.perPage)
})

const paginatedRowsThrottled = useThrottleValue(paginatedRows, props)

return {
paginatedRows,
paginatedRows: paginatedRowsThrottled,
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { PropType, computed, ref, Ref, watch, ExtractPropTypes, ComputedRef } from 'vue'

import { useThrottleFunction, useThrottleProps } from '../../../composables'

import type {
DataTableColumnInternal,
DataTableRow,
Expand All @@ -9,6 +11,7 @@ import type {
} from '../types'

export const useSortableProps = {
...useThrottleProps,
sortBy: { type: String as PropType<string | undefined> },
sortingOrder: { type: String as PropType<DataTableSortingOrder | undefined> },
}
Expand Down Expand Up @@ -134,6 +137,8 @@ export const useSortable = (
}
}

const toggleSortingThrottled = useThrottleFunction(toggleSorting, props)

const sortingOrderIconName = computed(() => {
return sortingOrderSync.value === 'asc'
? 'va-arrow-up'
Expand All @@ -145,7 +150,7 @@ export const useSortable = (
return {
sortBySync,
sortingOrderSync,
toggleSorting,
toggleSorting: toggleSortingThrottled,
sortedRows,
sortingOrderIconName,
}
Expand Down
7 changes: 7 additions & 0 deletions packages/ui/src/components/va-select/VaSelect.demo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,13 @@
searchable
searchtext="test"
/>
<va-select
v-model="defaultSingleSelect.value"
label="Search with throttling"
:options="defaultSingleSelect.options"
:delay="1000"
searchable
/>
</VbCard>
<VbCard
title="Allow create new"
Expand Down
4 changes: 3 additions & 1 deletion packages/ui/src/components/va-select/VaSelect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ import {
useFocusDeep,
useTranslation,
useBem,
useThrottleProps,
} from '../../composables'
import { extractComponentProps, filterComponentProps } from '../../utils/component-options'
Expand Down Expand Up @@ -175,6 +176,7 @@ export default defineComponent({
...useFormProps,
...useMaxVisibleOptionsProps,
...useToggleIconProps,
...useThrottleProps,
modelValue: {
type: [String, Number, Array, Object] as PropType<SelectOption | SelectOption[]>,
Expand Down Expand Up @@ -553,7 +555,7 @@ export default defineComponent({
}))
const optionsListPropsComputed = computed(() => ({
...pick(props, ['textBy', 'trackBy', 'groupBy', 'disabledBy', 'color', 'virtualScroller']),
...pick(props, ['textBy', 'trackBy', 'groupBy', 'disabledBy', 'color', 'virtualScroller', 'delay']),
search: searchInput.value,
tabindex: tabIndexComputed.value,
selectedValue: valueComputed.value,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ import {
useObjectRefs,
useSlotPassed,
useSelectableList, useSelectableListProps,
useThrottleValue, useThrottleProps,
} from '../../../../composables'
import { scrollToElement } from '../../../../utils/scroll-to-element'
Expand All @@ -102,6 +103,7 @@ export default defineComponent({
...useColorProps,
...useComponentPresetProp,
...useSelectableListProps,
...useThrottleProps,
noOptionsText: { type: String, default: 'Items not found' },
getSelectedState: { type: Function as PropType<(option: SelectOption) => boolean>, required: true },
multiple: { type: Boolean, default: false },
Expand Down Expand Up @@ -162,6 +164,7 @@ export default defineComponent({
return groups
}, { _noGroup: [] }))
const optionGroupsThrottled = useThrottleValue(optionGroups, props)
const isValueExists = (value: SelectOption | null | undefined) => !!value || value === 0
Expand All @@ -174,7 +177,7 @@ export default defineComponent({
const selectOption = (option: SelectOption) => !getDisabled(option) && emit('select-option', option)
const groupedOptions = computed(() => Object.values(optionGroups.value).flat())
const groupedOptions = computed(() => Object.values(optionGroupsThrottled.value).flat())
const currentOptions = computed(() =>
filteredOptions.value.some((el) => getGroupBy(el)) ? groupedOptions.value : filteredOptions.value)
Expand Down Expand Up @@ -259,7 +262,7 @@ export default defineComponent({
virtualScrollerRef,
rootHeight,
optionGroups,
optionGroups: optionGroupsThrottled,
filteredOptions,
selectOptionProps,
isSlotContentPassed,
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/composables/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export * from './useTimer'
export * from './useTrackBy'
export * from './useTranslation'
export * from './useTrapFocus'
export * from './useThrottle'
export * from './useValidation'
export * from './useWindow'
export * from './useWindowSize'
Expand Down
101 changes: 101 additions & 0 deletions packages/ui/src/composables/useThrottle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* @description returns throttled function or value
* the last one always returns last-call value in the end if no more new calls were provided
* @example
* import { useThrottleFunction, useThrottleValue } from '../../composables'
* ...
* const localThrottledFunction = useThrottleFunction(functionToThrottle, props)
* const localThrottledValue = useThrottleValue(reactiveValueToThrottle, props)
*/

import {
ref, toRef, unref,
watch,
Ref, ExtractPropTypes, ComponentInternalInstance,
} from 'vue'

type ThrottledFunctionArgs = any[]
type ThrottledFunction<Output> = (...args: ThrottledFunctionArgs) => Output

export const useThrottleProps = {
delay: {
type: Number,
default: 0,
validator: (value: number) => value >= 0,
},
}

type UseThrottleProps = ExtractPropTypes<typeof useThrottleProps>

/**
* @param fn passed function to throttle
* @param props { delay } call delay in ms
*/
export function useThrottleFunction<Output> (fn: ThrottledFunction<Output>, props: UseThrottleProps) {
const delay = toRef(props, 'delay') ?? 0

const isThrottled = ref(true)

/**
* No way this will be returned without reassignment, so we don't want typescript
* to always keep undefined as possible return type. If function returns undefined itself
* it will be still presented by typescript as undefined (expected behaviour).
*/
let lastCallResult = undefined as any as Output

return function (this: any, ...args: ThrottledFunctionArgs) {
const invoke = () => fn.apply(this, args)

if (!unref(delay)) { return invoke() }

if (isThrottled.value) {
isThrottled.value = false

setTimeout(() => (isThrottled.value = true), unref(delay))

lastCallResult = invoke()
}

return lastCallResult
}
}

/**
* @param value passed reactive value to throttle
* @param props { delay } call delay in ms
*/
export function useThrottleValue<T> (value: Ref<T>, props: UseThrottleProps): Ref<T> {
const delay = toRef(props, 'delay') ?? 0

if (!unref(delay)) { return value }

const isThrottled = ref(true)

const previousCallValue = ref<T>() as Ref<T>
const previousReturnedValue = ref<T>() as Ref<T>
const currentCallValue = ref<T>() as Ref<T>

watch(value, () => {
// we save and return last call value at the end if no more calls were provided
previousCallValue.value = value.value
const lastCallValue = setTimeout(() => {
currentCallValue.value = previousCallValue.value
}, unref(delay))

if (isThrottled.value) {
isThrottled.value = false

currentCallValue.value = value.value
previousReturnedValue.value = value.value

// no need to return previously saved value anymore
clearTimeout(lastCallValue)

setTimeout(() => (isThrottled.value = true), unref(delay))
} else {
currentCallValue.value = previousReturnedValue.value
}
}, { immediate: true })

return currentCallValue
}

0 comments on commit c8d0a30

Please sign in to comment.