Skip to content

Commit

Permalink
fix(*): update useDebounce to fix delays in KTable after clearing the…
Browse files Browse the repository at this point in the history
… search (#1220)

* fix(*): update useDebounce to fix delays in KTable after clearing the search

* chore: update useUtilities.ts

* refactor(use-debounce): return an object instead of an array

* docs(table): add comments

* test(table): add tests for debounced functions
  • Loading branch information
sumimakito committed Mar 22, 2023
1 parent 36e97bf commit a550695
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 43 deletions.
6 changes: 4 additions & 2 deletions src/components/KCatalog/KCatalog.vue
Expand Up @@ -486,7 +486,9 @@ export default defineComponent({
hasInitialized.value = true
}
const { query, search } = useDebounce('', 350)
const query = ref('')
const { debouncedFn: debouncedSearch } = useDebounce((q: string) => { query.value = q })
const { revalidate } = useRequest(
() => catalogFetcherCacheKey.value,
() => fetchData(),
Expand All @@ -506,7 +508,7 @@ export default defineComponent({
}
watch(() => props.searchInput, (newValue: string) => {
search(newValue)
debouncedSearch(newValue)
}, { immediate: true })
watch(() => [query.value, page.value, pageSize.value], () => {
Expand Down
44 changes: 44 additions & 0 deletions src/components/KTable/KTable.cy.ts
Expand Up @@ -459,4 +459,48 @@ describe('KTable', () => {
cy.getTestId('k-table-pagination').should('be.visible')
})
})

describe('misc', () => {
it('triggers the internal search and revalidate after clearing the search input', () => {
const fns = {
fetcher: ({ query }: { query: string }) => {
return { data: [{ query }] }
},
}

cy.spy(fns, 'fetcher').as('fetcher')

mount(KTable, {
propsData: {
testMode: 'true',
fetcher: fns.fetcher,
isLoading: false,
initialFetcherParams: { offset: 'abc' },
headers: options.headers,
pageSize: 15,
paginationPageSizes: [10, 15, 20],
hidePaginationWhenOptional: true,
paginationType: 'offset',
searchInput: '',
},
})

// wait for initial fetcher call
cy.get('@fetcher', { timeout: 1000 })
.should('have.callCount', 1) // fetcher's 1st call
.should('returned', { data: [{ query: '' }] })
.then(() => cy.wrap(Cypress.vueWrapper.setProps({ searchInput: 'some-keyword' })))

// fetcher call should be delayed (> 350ms for search func + 500ms for revalidate func)
cy.get('@fetcher', { timeout: 1000 }) // fetcher's 2nd call
.should('have.callCount', 2) // fetcher should be called once
.should('returned', { data: [{ query: 'some-keyword' }] })
.then(() => cy.wrap(Cypress.vueWrapper.setProps({ searchInput: '' })))

// fetcher should be called immediately (< 350ms for search func)
cy.get('@fetcher', { timeout: 350 })
.should('have.callCount', 3) // fetcher's 3rd call
.should('returned', { data: [{ query: '' }] })
})
})
})
42 changes: 35 additions & 7 deletions src/components/KTable/KTable.vue
Expand Up @@ -736,12 +736,20 @@ export default defineComponent({
return `k-table_${Math.floor(Math.random() * 1000)}_${props.fetcherCacheKey}` as string
})
const { query, search } = useDebounce('', 350)
const { revalidate } = useRequest(
const query = ref('')
const { debouncedFn: debouncedSearch, generateDebouncedFn: generateDebouncedSearch } = useDebounce((q: string) => { query.value = q }, 350)
const search = generateDebouncedSearch(0) // generate a debounced function with zero delay (immediate)
const { revalidate: _revalidate } = useRequest(
() => tableFetcherCacheKey.value,
() => fetchData(),
{ revalidateOnFocus: false },
{ revalidateOnFocus: false, revalidateDebounce: 0 },
)
const { debouncedFn: debouncedRevalidate, generateDebouncedFn: generateDebouncedRevalidate } = useDebounce(_revalidate, 500)
const revalidate = generateDebouncedRevalidate(0) // generate a debounced function with zero delay (immediate)
const sortClickHandler = (header: TableHeader) => {
const { key, useSortHandlerFn } = header
const prevKey = sortColumnKey.value + '' // avoid pass by ref
Expand Down Expand Up @@ -779,7 +787,7 @@ export default defineComponent({
defaultSorter(key, prevKey, sortColumnOrder.value, data.value)
}
} else if (props.paginationType !== 'offset') {
revalidate()
debouncedRevalidate()
}
// Emit an event whenever one of the tablePreferences are updated
Expand Down Expand Up @@ -845,11 +853,31 @@ export default defineComponent({
}
watch(() => props.searchInput, (newValue) => {
search(newValue)
if (newValue === '') {
// Immediately triggers the search, ...
// 1) on the 1st time (input is empty)
// 2) after clearing the input
search(newValue)
} else {
// Triggers a debounced search
debouncedSearch(newValue)
}
}, { immediate: true })
watch(() => [query.value, page.value, pageSize.value], () => {
revalidate()
watch(query, (newQuery) => {
if (newQuery === '') {
// Immediately triggers the revalidate, ...
// 1) on the 1st time (query is empty)
// 2) after clearing the input (query becomes empty)
revalidate()
} else {
// Triggers a debounced revalidate
debouncedRevalidate()
}
}, { deep: true, immediate: true })
watch(() => [page.value, pageSize.value], () => {
debouncedRevalidate()
}, { deep: true, immediate: true })
onMounted(() => {
Expand Down
107 changes: 73 additions & 34 deletions src/composables/useUtilities.ts
Expand Up @@ -14,20 +14,31 @@ const swrvState = {
}

export default function useUtilities() {
const useRequest = <Data = unknown, Error = { message: string }> (key: IKey, fn?: fetcherFn<AxiosResponse<Data>>, config?: IConfig) => {
const useSwrvFn = typeof useSWRV === 'function'
? useSWRV
: () => ({
data: {},
error: null,
isValidating: false,
mutate: () => ({}),
})

const { data: response, error, isValidating, mutate: revalidate } = useSwrvFn<
AxiosResponse<Data>,
AxiosError<Error>
>(key, fn, { revalidateDebounce: 500, dedupingInterval: 100, ...config })
const useRequest = <Data = unknown, Error = { message: string }>(
key: IKey,
fn?: fetcherFn<AxiosResponse<Data>>,
config?: IConfig,
) => {
const useSwrvFn =
typeof useSWRV === 'function'
? useSWRV
: () => ({
data: {},
error: null,
isValidating: false,
mutate: () => ({}),
})

const {
data: response,
error,
isValidating,
mutate: revalidate,
} = useSwrvFn<AxiosResponse<Data>, AxiosError<Error>>(key, fn, {
revalidateDebounce: 500,
dedupingInterval: 100,
...config,
})

const data = computed(() => {
// @ts-ignore
Expand All @@ -43,30 +54,50 @@ export default function useUtilities() {
}
}

const useDebounce = (initialQuery: string, delay = 300) => {
/**
* useDebounce accepts a function and a default delay.
* It returns a debounced function with the default delay and a debounced function
* wrapper which can be used to generate a debounced function with any delay.
* @param fn the function to wrap
* @param defaultDelay the default delay in milliseconds to use
* @returns a debounced function with default delay and a debounced function generator
*/
const useDebounce = <F extends (...args: any[]) => any>(
fn: F,
defaultDelay = 300,
): {
debouncedFn: (...args: Parameters<F>) => void
generateDebouncedFn: (delay: number) => (...args: Parameters<F>) => void
} => {
let timeout: any
const query = ref(initialQuery)

function search(q: string) {
clearTimeout(timeout)
timeout = setTimeout(() => {
query.value = q
}, delay)
}
const wrapDebouncedWithDelay =
(delay: number) =>
(...args: Parameters<F>) => {
clearTimeout(timeout)
if (delay > 0) {
timeout = setTimeout(() => {
fn(...args)
}, delay)
} else {
timeout = undefined
fn(...args)
}
}

return {
query,
search,
debouncedFn: wrapDebouncedWithDelay(defaultDelay),
generateDebouncedFn: wrapDebouncedWithDelay,
}
}

/**
* @param {String} key - the current key to sort by
* @param {String} previousKey - the previous key used to sort by
* @param {String} sortOrder - either ascending or descending
* @param {Array} items - the list of items to sort
* @return {Object} an object containing the previousKey and sortOrder
*/
* @param {String} key - the current key to sort by
* @param {String} previousKey - the previous key used to sort by
* @param {String} sortOrder - either ascending or descending
* @param {Array} items - the list of items to sort
* @return {Object} an object containing the previousKey and sortOrder
*/
const clientSideSorter = (key: string, previousKey: string, sortOrder: string, items: []) => {
let comparator = null

Expand Down Expand Up @@ -130,9 +161,11 @@ export default function useUtilities() {

watchEffect(() => {
const hasData =
response.value?.data?.length ||
response.value?.data?.data?.length ||
(!response.value?.data?.data && typeof response.value?.data === 'object' && Object.keys(response.value?.data).length)
response.value?.data?.length ||
response.value?.data?.data?.length ||
(!response.value?.data?.data &&
typeof response.value?.data === 'object' &&
Object.keys(response.value?.data).length)

if (response.value && hasData && isValidating.value) {
state.value = swrvState.VALIDATING_HAS_DATA
Expand Down Expand Up @@ -187,7 +220,13 @@ export default function useUtilities() {
* @returns A string to be used for the height of an element.
*/
const getSizeFromString = (sizeStr: string): string => {
return sizeStr === 'auto' || sizeStr.endsWith('%') || sizeStr.endsWith('vw') || sizeStr.endsWith('vh') || sizeStr.endsWith('px') ? sizeStr : sizeStr + 'px'
return sizeStr === 'auto' ||
sizeStr.endsWith('%') ||
sizeStr.endsWith('vw') ||
sizeStr.endsWith('vh') ||
sizeStr.endsWith('px')
? sizeStr
: sizeStr + 'px'
}

/**
Expand Down

0 comments on commit a550695

Please sign in to comment.