Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat(b-table): add filter-debounce prop for debouncing filter updates #3891

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Aug 16, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions src/components/table/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

> For displaying tabular data, `<b-table>` supports pagination, filtering, sorting, custom
> rendering, various style options, events, and asynchronous data. For simple display of tabular
> data without all the fancy features, BootstrapVue provides lightweight alternative components
> data without all the fancy features, BootstrapVue provides two lightweight alternative components
> [`<b-table-lite>`](#light-weight-tables) and [`<b-table-simple>`](#simple-tables).

**Example: Basic usage**
Expand Down Expand Up @@ -800,8 +800,8 @@ function.

Scoped field slots give you greater control over how the record data appears. You can use scoped
slots to provided custom rendering for a particular field. If you want to add an extra field which
does not exist in the records, just add it to the [`fields`](#fields-column-definitions) array,
and then reference the field(s) in the scoped slot(s). Scoped field slots use the following naming
does not exist in the records, just add it to the [`fields`](#fields-column-definitions) array, and
then reference the field(s) in the scoped slot(s). Scoped field slots use the following naming
syntax: `'cell[' + field key + ']'`.

You can use the default _fall-back_ scoped slot `'cell[]'` to format any cells that do not have an
Expand Down Expand Up @@ -1984,6 +1984,25 @@ When local filtering is applied, and the resultant number of items change, `<b-t

Setting the prop `filter` to null or an empty string will clear local items filtering.

### Debouncing filter criteria changes

If you have a text input tied to the `filter` prop of `<b-table>`, the filtering process will occur
for each character typed by the user. With large items datasets, this process can take a while and
may cause the text input to appear sluggish.

To help alleviate this type of situation, `<b-table>` accepts a debounce timout value (in
milliseconds) via the `filter-debounce` prop. The default is `0` (milliseconds). When a value
greater than `0` is provided, the filter will wait for that time before updating the table results.
If the value of the `filter` prop changes before this timeout expires, the filtering will be once
again delayed until the debounce timeout expires.

When used, the suggested value of `filter-debounce` should be in the range of `100` to `200`
milliseconds, but other values may be more suitable for your use case.

The use of this prop can be beneficial when using provider filtering with
[items provider functions](#using-items-provider-functions), to help reduce the number of calls to
your back end API.

### Filtering notes

See the [Complete Example](#complete-example) below for an example of using the `filter` feature.
Expand Down Expand Up @@ -2173,6 +2192,9 @@ of records.
`filter` props on `b-table` to trigger the provider update function call (unless you have the
respective `no-provider-*` prop set to `true`).
- The `no-local-sorting` prop has no effect when `items` is a provider function.
- When using provider filtering, you may find that setting the
[`filter-debounce` prop](#debouncing-filter-criteria-changes) to a value greater than `100` ms
will help minimize the number of calls to your back end API as the user types in the criteria.

### Force refreshing of table data

Expand Down
86 changes: 68 additions & 18 deletions src/components/table/helpers/mixin-filtering.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,19 @@ export default {
filterIncludedFields: {
type: Array
// default: undefined
},
filterDebounce: {
type: [Number, String],
default: 0,
validator: val => /^\d+/.test(String(val))
}
},
data() {
return {
// Flag for displaying which empty slot to show and some event triggering
isFiltered: false
isFiltered: false,
// Where we store the copy of the filter citeria after debouncing
localFilter: null
}
},
computed: {
Expand All @@ -36,6 +43,9 @@ export default {
computedFilterIncluded() {
return this.filterIncludedFields ? concat(this.filterIncludedFields).filter(Boolean) : null
},
computedFilterDebounce() {
return parseInt(this.filterDebounce, 10) || 0
},
localFiltering() {
return this.hasProvider ? !!this.noProviderFiltering : true
},
Expand All @@ -47,22 +57,6 @@ export default {
localFilter: this.localFilter
}
},
// Sanitized/normalized version of filter prop
localFilter() {
// Using internal filter function, which only accepts string or RegExp
if (
this.localFiltering &&
!isFunction(this.filterFunction) &&
!(isString(this.filter) || isRegExp(this.filter))
) {
return ''
}

// Could be a string, object or array, as needed by external filter function
// We use `cloneDeep` to ensure we have a new copy of an object or array
// without Vue reactive observers
return cloneDeep(this.filter)
},
// Sanitized/normalize filter-function prop
localFilterFn() {
// Return `null` to signal to use internal filter function
Expand All @@ -72,13 +66,14 @@ export default {
// Returns the original `localItems` array if not sorting
filteredItems() {
const items = this.localItems || []
// Note the criteria is debounced
const criteria = this.filterSanitize(this.localFilter)

// Resolve the filtering function, when requested
// We prefer the provided filtering function and fallback to the internal one
// When no filtering criteria is specified the filtering factories will return `null`
let filterFn = null
if (this.localFiltering) {
const criteria = this.localFilter
filterFn =
this.filterFnFactory(this.localFilterFn, criteria) ||
this.defaultFilterFnFactory(criteria)
Expand All @@ -94,6 +89,32 @@ export default {
}
},
watch: {
// Watch for debounce being set to 0
computedFilterDebounce(newVal, oldVal) {
if (!newVal && this.filterTimer) {
clearTimeout(this.filterTimer)
this.filterTimer = null
this.localFilter = this.filter
}
},
// Watch for changes to the filter criteria, and debounce if necessary
filter(newFilter, oldFilter) {
const timeout = this.computedFilterDebounce
if (this.filterTimer) {
clearTimeout(this.filterTimer)
this.filterTimer = null
}
if (timeout) {
// If we have a debounce time, delay the update of this.localFilter
this.filterTimer = setTimeout(() => {
this.filterTimer = null
this.localFilter = this.filterSanitize(this.filter)
}, timeout)
} else {
// Otherwise, immediately update this.localFilter
this.localFilter = this.filterSanitize(this.filter)
}
},
// Watch for changes to the filter criteria and filtered items vs localItems).
// And set visual state and emit events as required
filteredCheck({ filteredItems, localItems, localFilter }) {
Expand Down Expand Up @@ -123,13 +144,42 @@ export default {
}
},
created() {
// Create non-reactive prop where we store the debounce timer id
this.filterTimer = null
// If filter is "pre-set", set the criteria
// This will trigger any watchers/dependants
this.localFilter = this.filterSanitize(this.filter)
// Set the initial filtered state.
// In a nextTick so that we trigger a filtered event if needed
this.$nextTick(() => {
this.isFiltered = Boolean(this.localFilter)
})
},
beforeDestroy() {
/* istanbul ignore next */
if (this.filterTimer) {
clearTimeout(this.filterTimer)
this.filterTimer = null
}
},
methods: {
filterSanitize(criteria) {
// Sanitizes filter criteria based on internal or external filtering
if (
this.localFiltering &&
!isFunction(this.filterFunction) &&
!(isString(criteria) || isRegExp(criteria))
) {
// If using internal filter function, which only accepts string or RegExp
// return null to signify no filter
return null
}

// Could be a string, object or array, as needed by external filter function
// We use `cloneDeep` to ensure we have a new copy of an object or array
// without Vue's reactive observers
return cloneDeep(criteria)
},
// Filter Function factories
filterFnFactory(filterFn, criteria) {
// Wrapper factory for external filter functions
Expand Down
63 changes: 63 additions & 0 deletions src/components/table/table-filtering.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -235,4 +235,67 @@ describe('table > filtering', () => {

wrapper.destroy()
})

it('filter debouncing works', async () => {
jest.useFakeTimers()
const wrapper = mount(BTable, {
propsData: {
fields: testFields,
items: testItems,
filterDebounce: 100 // 100ms
}
})
expect(wrapper).toBeDefined()
expect(wrapper.findAll('tbody > tr').exists()).toBe(true)
expect(wrapper.findAll('tbody > tr').length).toBe(3)
expect(wrapper.vm.filterTimer).toBe(null)
await waitNT(wrapper.vm)
expect(wrapper.emitted('input')).toBeDefined()
expect(wrapper.emitted('input').length).toBe(1)
expect(wrapper.emitted('input')[0][0]).toEqual(testItems)
expect(wrapper.vm.filterTimer).toBe(null)

// Set filter to a single character
wrapper.setProps({
filter: '1'
})
await waitNT(wrapper.vm)
expect(wrapper.emitted('input').length).toBe(1)
expect(wrapper.vm.filterTimer).not.toBe(null)

// Change filter
wrapper.setProps({
filter: 'z'
})
await waitNT(wrapper.vm)
expect(wrapper.emitted('input').length).toBe(1)
expect(wrapper.vm.filterTimer).not.toBe(null)

jest.runTimersToTime(101)
await waitNT(wrapper.vm)
expect(wrapper.emitted('input').length).toBe(2)
expect(wrapper.emitted('input')[1][0]).toEqual([testItems[2]])
expect(wrapper.vm.filterTimer).toBe(null)

// Change filter
wrapper.setProps({
filter: '1'
})
await waitNT(wrapper.vm)
expect(wrapper.vm.filterTimer).not.toBe(null)
expect(wrapper.emitted('input').length).toBe(2)

// Change filter-debounce to no debouncing
wrapper.setProps({
filterDebounce: 0
})
await waitNT(wrapper.vm)
// Should clear the pending timer
expect(wrapper.vm.filterTimer).toBe(null)
// Should immediately filter the items
expect(wrapper.emitted('input').length).toBe(3)
expect(wrapper.emitted('input')[2][0]).toEqual([testItems[1]])

wrapper.destroy()
})
})