Skip to content

Commit 9ddd115

Browse files
authored
fix(b-table): handle filter as an object when using providers and prevent duplicate provider calls on mount (fixes #4065) (#4068)
1 parent 9f95ef1 commit 9ddd115

File tree

3 files changed

+171
-76
lines changed

3 files changed

+171
-76
lines changed

src/components/table/helpers/mixin-filtering.js

Lines changed: 49 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@ export default {
3232
return {
3333
// Flag for displaying which empty slot to show and some event triggering
3434
isFiltered: false,
35-
// Where we store the copy of the filter citeria after debouncing
36-
localFilter: null
35+
// Where we store the copy of the filter criteria after debouncing
36+
// We pre-set it with the sanitized filter value
37+
localFilter: this.filterSanitize(this.filter)
3738
}
3839
},
3940
computed: {
@@ -66,57 +67,55 @@ export default {
6667
// Returns the original `localItems` array if not sorting
6768
filteredItems() {
6869
const items = this.localItems || []
69-
// Note the criteria is debounced
70-
const criteria = this.filterSanitize(this.localFilter)
70+
// Note the criteria is debounced and sanitized
71+
const criteria = this.localFilter
7172

7273
// Resolve the filtering function, when requested
7374
// We prefer the provided filtering function and fallback to the internal one
7475
// When no filtering criteria is specified the filtering factories will return `null`
75-
let filterFn = null
76-
if (this.localFiltering) {
77-
filterFn =
78-
this.filterFnFactory(this.localFilterFn, criteria) ||
76+
const filterFn = this.localFiltering
77+
? this.filterFnFactory(this.localFilterFn, criteria) ||
7978
this.defaultFilterFnFactory(criteria)
80-
}
79+
: null
8180

8281
// We only do local filtering when requested and there are records to filter
83-
if (filterFn && items.length > 0) {
84-
return items.filter(filterFn)
85-
}
86-
87-
// Otherwise return all items
88-
return items
82+
return filterFn && items.length > 0 ? items.filter(filterFn) : items
8983
}
9084
},
9185
watch: {
9286
// Watch for debounce being set to 0
9387
computedFilterDebounce(newVal, oldVal) {
94-
if (!newVal && this.filterTimer) {
95-
clearTimeout(this.filterTimer)
96-
this.filterTimer = null
97-
this.localFilter = this.filter
88+
if (!newVal && this.$_filterTimer) {
89+
clearTimeout(this.$_filterTimer)
90+
this.$_filterTimer = null
91+
this.localFilter = this.filterSanitize(this.filter)
9892
}
9993
},
10094
// Watch for changes to the filter criteria, and debounce if necessary
101-
filter(newFilter, oldFilter) {
102-
const timeout = this.computedFilterDebounce
103-
if (this.filterTimer) {
104-
clearTimeout(this.filterTimer)
105-
this.filterTimer = null
106-
}
107-
if (timeout) {
108-
// If we have a debounce time, delay the update of this.localFilter
109-
this.filterTimer = setTimeout(() => {
110-
this.filterTimer = null
111-
this.localFilter = this.filterSanitize(this.filter)
112-
}, timeout)
113-
} else {
114-
// Otherwise, immediately update this.localFilter
115-
this.localFilter = this.filterSanitize(this.filter)
95+
filter: {
96+
// We need a deep watcher in case the user passes
97+
// an object when using `filter-function`
98+
deep: true,
99+
handler(newFilter, oldFilter) {
100+
const timeout = this.computedFilterDebounce
101+
if (this.$_filterTimer) {
102+
clearTimeout(this.$_filterTimer)
103+
this.$_filterTimer = null
104+
}
105+
if (timeout) {
106+
// If we have a debounce time, delay the update of `localFilter`
107+
this.$_filterTimer = setTimeout(() => {
108+
this.$_filterTimer = null
109+
this.localFilter = this.filterSanitize(this.filter)
110+
}, timeout)
111+
} else {
112+
// Otherwise, immediately update `localFilter` with `newFilter` value
113+
this.localFilter = this.filterSanitize(newFilter)
114+
}
116115
}
117116
},
118-
// Watch for changes to the filter criteria and filtered items vs localItems).
119-
// And set visual state and emit events as required
117+
// Watch for changes to the filter criteria and filtered items vs `localItems`
118+
// Set visual state and emit events as required
120119
filteredCheck({ filteredItems, localItems, localFilter }) {
121120
// Determine if the dataset is filtered or not
122121
let isFiltered = false
@@ -145,34 +144,34 @@ export default {
145144
},
146145
created() {
147146
// Create non-reactive prop where we store the debounce timer id
148-
this.filterTimer = null
147+
this.$_filterTimer = null
149148
// If filter is "pre-set", set the criteria
150-
// This will trigger any watchers/dependants
151-
this.localFilter = this.filterSanitize(this.filter)
152-
// Set the initial filtered state.
153-
// In a nextTick so that we trigger a filtered event if needed
149+
// This will trigger any watchers/dependents
150+
// this.localFilter = this.filterSanitize(this.filter)
151+
// Set the initial filtered state in a `$nextTick()` so that
152+
// we trigger a filtered event if needed
154153
this.$nextTick(() => {
155154
this.isFiltered = Boolean(this.localFilter)
156155
})
157156
},
158157
beforeDestroy() {
159158
/* istanbul ignore next */
160-
if (this.filterTimer) {
161-
clearTimeout(this.filterTimer)
162-
this.filterTimer = null
159+
if (this.$_filterTimer) {
160+
clearTimeout(this.$_filterTimer)
161+
this.$_filterTimer = null
163162
}
164163
},
165164
methods: {
166165
filterSanitize(criteria) {
167166
// Sanitizes filter criteria based on internal or external filtering
168167
if (
169168
this.localFiltering &&
170-
!isFunction(this.filterFunction) &&
169+
!this.localFilterFn &&
171170
!(isString(criteria) || isRegExp(criteria))
172171
) {
173-
// If using internal filter function, which only accepts string or RegExp
174-
// return null to signify no filter
175-
return null
172+
// If using internal filter function, which only accepts string or RegExp,
173+
// return '' to signify no filter
174+
return ''
176175
}
177176

178177
// Could be a string, object or array, as needed by external filter function
@@ -210,6 +209,7 @@ export default {
210209
},
211210
defaultFilterFnFactory(criteria) {
212211
// Generates the default filter function, using the given filter criteria
212+
// Returns `null` if no criteria or criteria format not supported
213213
if (!criteria || !(isString(criteria) || isRegExp(criteria))) {
214214
// Built in filter can only support strings or RegExp criteria (at the moment)
215215
return null
@@ -237,7 +237,7 @@ export default {
237237
// Users can ignore filtering on specific fields, or on only certain fields,
238238
// and can optionall specify searching results of fields with formatter
239239
//
240-
// TODO: Enable searching on scoped slots
240+
// TODO: Enable searching on scoped slots (optional, as it will be SLOW)
241241
//
242242
// Generated function returns true if the criteria matches part of
243243
// the serialized data, otherwise false

src/components/table/table-filtering.spec.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -248,41 +248,41 @@ describe('table > filtering', () => {
248248
expect(wrapper).toBeDefined()
249249
expect(wrapper.findAll('tbody > tr').exists()).toBe(true)
250250
expect(wrapper.findAll('tbody > tr').length).toBe(3)
251-
expect(wrapper.vm.filterTimer).toBe(null)
251+
expect(wrapper.vm.$_filterTimer).toBe(null)
252252
await waitNT(wrapper.vm)
253253
expect(wrapper.emitted('input')).toBeDefined()
254254
expect(wrapper.emitted('input').length).toBe(1)
255255
expect(wrapper.emitted('input')[0][0]).toEqual(testItems)
256-
expect(wrapper.vm.filterTimer).toBe(null)
256+
expect(wrapper.vm.$_filterTimer).toBe(null)
257257

258258
// Set filter to a single character
259259
wrapper.setProps({
260260
filter: '1'
261261
})
262262
await waitNT(wrapper.vm)
263263
expect(wrapper.emitted('input').length).toBe(1)
264-
expect(wrapper.vm.filterTimer).not.toBe(null)
264+
expect(wrapper.vm.$_filterTimer).not.toBe(null)
265265

266266
// Change filter
267267
wrapper.setProps({
268268
filter: 'z'
269269
})
270270
await waitNT(wrapper.vm)
271271
expect(wrapper.emitted('input').length).toBe(1)
272-
expect(wrapper.vm.filterTimer).not.toBe(null)
272+
expect(wrapper.vm.$_filterTimer).not.toBe(null)
273273

274274
jest.runTimersToTime(101)
275275
await waitNT(wrapper.vm)
276276
expect(wrapper.emitted('input').length).toBe(2)
277277
expect(wrapper.emitted('input')[1][0]).toEqual([testItems[2]])
278-
expect(wrapper.vm.filterTimer).toBe(null)
278+
expect(wrapper.vm.$_filterTimer).toBe(null)
279279

280280
// Change filter
281281
wrapper.setProps({
282282
filter: '1'
283283
})
284284
await waitNT(wrapper.vm)
285-
expect(wrapper.vm.filterTimer).not.toBe(null)
285+
expect(wrapper.vm.$_filterTimer).not.toBe(null)
286286
expect(wrapper.emitted('input').length).toBe(2)
287287

288288
// Change filter-debounce to no debouncing
@@ -291,7 +291,7 @@ describe('table > filtering', () => {
291291
})
292292
await waitNT(wrapper.vm)
293293
// Should clear the pending timer
294-
expect(wrapper.vm.filterTimer).toBe(null)
294+
expect(wrapper.vm.$_filterTimer).toBe(null)
295295
// Should immediately filter the items
296296
expect(wrapper.emitted('input').length).toBe(3)
297297
expect(wrapper.emitted('input')[2][0]).toEqual([testItems[1]])

0 commit comments

Comments
 (0)