Skip to content

Commit 142b31b

Browse files
authored
feat(b-table): allow users to specify top-level keys to be ignored or included when filtering, plus add option to filter based on formatted value (closes #3749) (#3786)
1 parent bcb132e commit 142b31b

File tree

6 files changed

+248
-109
lines changed

6 files changed

+248
-109
lines changed

src/components/table/README.md

Lines changed: 159 additions & 64 deletions
Large diffs are not rendered by default.

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

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import cloneDeep from '../../../utils/clone-deep'
22
import looseEqual from '../../../utils/loose-equal'
33
import warn from '../../../utils/warn'
4+
import { concat } from '../../../utils/array'
45
import { isFunction, isString, isRegExp } from '../../../utils/inspect'
56
import stringifyRecordValues from './stringify-record-values'
67

@@ -20,6 +21,14 @@ export default {
2021
filterFunction: {
2122
type: Function,
2223
default: null
24+
},
25+
filterIgnoredFields: {
26+
type: Array
27+
// default: undefined
28+
},
29+
filterIncludedFields: {
30+
type: Array
31+
// default: undefined
2332
}
2433
},
2534
data() {
@@ -29,6 +38,12 @@ export default {
2938
}
3039
},
3140
computed: {
41+
computedFilterIgnored() {
42+
return this.filterIgnoredFields ? concat(this.filterIgnoredFields).filter(Boolean) : null
43+
},
44+
computedFilterIncluded() {
45+
return this.filterIncludedFields ? concat(this.filterIncludedFields).filter(Boolean) : null
46+
},
3247
localFiltering() {
3348
return this.hasProvider ? !!this.noProviderFiltering : true
3449
},
@@ -148,10 +163,10 @@ export default {
148163
methods: {
149164
// Filter Function factories
150165
filterFnFactory(filterFn, criteria) {
151-
// Wrapper factory for external filter functions.
152-
// Wrap the provided filter-function and return a new function.
153-
// Returns null if no filter-function defined or if criteria is falsey.
154-
// Rather than directly grabbing this.computedLocalFilterFn or this.filterFunction
166+
// Wrapper factory for external filter functions
167+
// Wrap the provided filter-function and return a new function
168+
// Returns `null` if no filter-function defined or if criteria is falsey
169+
// Rather than directly grabbing `this.computedLocalFilterFn` or `this.filterFunction`
155170
// we have it passed, so that the caller computed prop will be reactive to changes
156171
// in the original filter-function (as this routine is a method)
157172
if (
@@ -184,34 +199,39 @@ export default {
184199
// Build the regexp needed for filtering
185200
let regexp = criteria
186201
if (isString(regexp)) {
187-
// Escape special RegExp characters in the string and convert contiguous
188-
// whitespace to \s+ matches
202+
// Escape special `RegExp` characters in the string and convert contiguous
203+
// whitespace to `\s+` matches
189204
const pattern = criteria
190205
.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')
191206
.replace(/[\s\uFEFF\xA0]+/g, '\\s+')
192-
// Build the RegExp (no need for global flag, as we only need
207+
// Build the `RegExp` (no need for global flag, as we only need
193208
// to find the value once in the string)
194209
regexp = new RegExp(`.*${pattern}.*`, 'i')
195210
}
196211

197212
// Generate the wrapped filter test function to use
198213
const fn = item => {
199214
// This searches all row values (and sub property values) in the entire (excluding
200-
// special _ prefixed keys), because we convert the record to a space-separated
215+
// special `_` prefixed keys), because we convert the record to a space-separated
201216
// string containing all the value properties (recursively), even ones that are
202-
// not visible (not specified in this.fields).
217+
// not visible (not specified in this.fields)
218+
// Users can ignore filtering on specific fields, or on only certain fields,
219+
// and can optionall specify searching results of fields with formatter
203220
//
204-
// TODO: Enable searching on formatted fields and scoped slots
205-
// TODO: Should we filter only on visible fields (i.e. ones in this.fields) by default?
206-
// TODO: Allow for searching on specific fields/key, this could be combined with the previous TODO
207-
// TODO: Give stringifyRecordValues extra options for filtering (i.e. passing the
208-
// fields definition and a reference to $scopedSlots)
221+
// TODO: Enable searching on scoped slots
209222
//
210223
// Generated function returns true if the criteria matches part of
211224
// the serialized data, otherwise false
212-
// We set lastIndex = 0 on regex in case someone uses the /g global flag
225+
// We set `lastIndex = 0` on the `RegExp` in case someone specifies the `/g` global flag
213226
regexp.lastIndex = 0
214-
return regexp.test(stringifyRecordValues(item))
227+
return regexp.test(
228+
stringifyRecordValues(
229+
item,
230+
this.computedFilterIgnored,
231+
this.computedFilterIncluded,
232+
this.computedFieldsObj
233+
)
234+
)
215235
}
216236

217237
// Return the generated function

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

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ export default {
1717
default: null
1818
},
1919
primaryKey: {
20-
// Primary key for record.
21-
// If provided the value in each row must be unique!!!
20+
// Primary key for record
21+
// If provided the value in each row must be unique!
2222
type: String,
2323
default: null
2424
},
2525
value: {
26-
// v-model for retrieving the current displayed rows
26+
// `v-model` for retrieving the current displayed rows
2727
type: Array,
2828
default() {
2929
return []
@@ -32,21 +32,38 @@ export default {
3232
},
3333
data() {
3434
return {
35-
// Our local copy of the items. Must be an array
35+
// Our local copy of the items
36+
// Must be an array
3637
localItems: isArray(this.items) ? this.items.slice() : []
3738
}
3839
},
3940
computed: {
4041
computedFields() {
4142
// We normalize fields into an array of objects
42-
// [ { key:..., label:..., ...}, {...}, ..., {..}]
43+
// `[ { key:..., label:..., ...}, {...}, ..., {..}]`
4344
return normalizeFields(this.fields, this.localItems)
4445
},
4546
computedFieldsObj() {
4647
// Fields as a simple lookup hash object
47-
// Mainly for formatter lookup and scopedSlots for convenience
48+
// Mainly for formatter lookup and use in `scopedSlots` for convenience
49+
// If the field has a formatter, it normalizes formatter to a
50+
// function ref or `undefined` if no formatter
51+
const parent = this.$parent
4852
return this.computedFields.reduce((obj, f) => {
49-
obj[f.key] = f
53+
// We use object spread here so we don't mutate the original field object
54+
obj[f.key] = { ...f }
55+
if (f.formatter) {
56+
// Normalize formatter to a function ref or `undefined`
57+
let formatter = f.formatter
58+
if (isString(formatter) && isFunction(parent[formatter])) {
59+
formatter = parent[formatter]
60+
} else if (!isFunction(formatter)) {
61+
/* istanbul ignore next */
62+
formatter = undefined
63+
}
64+
// Return formatter function or `undefined` if none
65+
obj[f.key].formatter = formatter
66+
}
5067
return obj
5168
}, {})
5269
},
@@ -76,43 +93,36 @@ export default {
7693
items(newItems) {
7794
/* istanbul ignore else */
7895
if (isArray(newItems)) {
79-
// Set localItems/filteredItems to a copy of the provided array
96+
// Set `localItems`/`filteredItems` to a copy of the provided array
8097
this.localItems = newItems.slice()
8198
} else if (isUndefined(newItems) || isNull(newItems)) {
8299
/* istanbul ignore next */
83100
this.localItems = []
84101
}
85102
},
86-
// Watch for changes on computedItems and update the v-model
103+
// Watch for changes on `computedItems` and update the `v-model`
87104
computedItems(newVal) {
88105
this.$emit('input', newVal)
89106
},
90107
// Watch for context changes
91108
context(newVal, oldVal) {
92-
// Emit context info for external paging/filtering/sorting handling
109+
// Emit context information for external paging/filtering/sorting handling
93110
if (!looseEqual(newVal, oldVal)) {
94111
this.$emit('context-changed', newVal)
95112
}
96113
}
97114
},
98115
mounted() {
99-
// Initially update the v-model of displayed items
116+
// Initially update the `v-model` of displayed items
100117
this.$emit('input', this.computedItems)
101118
},
102119
methods: {
103120
// Method to get the formatter method for a given field key
104121
getFieldFormatter(key) {
105-
const fieldsObj = this.computedFieldsObj
106-
const field = fieldsObj[key]
107-
const parent = this.$parent
108-
let formatter = field && field.formatter
109-
if (isString(formatter) && isFunction(parent[formatter])) {
110-
formatter = parent[formatter]
111-
} else if (!isFunction(formatter)) {
112-
formatter = undefined
113-
}
114-
// Return formatter function or undefined if none
115-
return formatter
122+
const field = this.computedFieldsObj[key]
123+
// `this.computedFieldsObj` has pre-normalized the formatter to a
124+
// function ref if present, otherwise `undefined`
125+
return field ? field.formatter : undefined
116126
}
117127
}
118128
}

src/components/table/helpers/sanitize-row.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
import { keys } from '../../../utils/object'
2+
import { arrayIncludes } from '../../../utils/array'
23
import { IGNORED_FIELD_KEYS } from './constants'
34

45
// Return a copy of a row after all reserved fields have been filtered out
5-
// TODO: add option to specify which fields to include
6-
const sanitizeRow = row =>
6+
const sanitizeRow = (row, ignoreFields, includeFields, fieldsObj = {}) =>
77
keys(row).reduce((obj, key) => {
88
// Ignore special fields that start with `_`
9-
if (!IGNORED_FIELD_KEYS[key]) {
10-
obj[key] = row[key]
9+
// Ignore fields in the `ignoreFields` array
10+
// Include only fields in the `includeFields` array
11+
if (
12+
!IGNORED_FIELD_KEYS[key] &&
13+
!(ignoreFields && ignoreFields.length > 0 && arrayIncludes(ignoreFields, key)) &&
14+
!(includeFields && includeFields.length > 0 && !arrayIncludes(includeFields, key))
15+
) {
16+
const f = fieldsObj[key]
17+
const val = row[key]
18+
obj[key] = f && f.filterByFormatted && f.formatter ? f.formatter(val, key, row) : val
1119
}
1220
return obj
1321
}, {})

src/components/table/helpers/stringify-record-values.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ import sanitizeRow from './sanitize-row'
33
import stringifyObjectValues from './stringify-object-values'
44

55
// Stringifies the values of a record, ignoring any special top level field keys
6-
// TODO: add option to stringify formatted/scopedSlot items, and only specific fields
7-
/* istanbul ignore next */
8-
const stringifyRecordValues = row => (isObject(row) ? stringifyObjectValues(sanitizeRow(row)) : '')
6+
// TODO: Add option to stringify `scopedSlot` items
7+
const stringifyRecordValues = (row, ignoreFields, includeFields, fieldsObj) => {
8+
return isObject(row)
9+
? stringifyObjectValues(sanitizeRow(row, ignoreFields, includeFields, fieldsObj))
10+
: ''
11+
}
912

1013
export default stringifyRecordValues

src/components/table/index.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export declare class BTable extends BvComponent {
2828
currentPage?: number | string
2929
filter?: string | Array<any> | RegExp | object | any
3030
filterFunction?: BvTableFilterCallback
31+
filterIgnoredFields?: Array<string>
32+
filterIncludedFields?: Array<string>
3133
busy?: boolean
3234
tbodyTrClass?: string | Array<any> | object | BvTableTbodyTrClassCallback
3335
}
@@ -114,6 +116,7 @@ export interface BvTableField {
114116
sortable?: boolean
115117
sortDirection?: BvTableSortDirection
116118
sortByFormatted?: boolean
119+
filterByFormatted?: boolean
117120
tdClass?: string | string[] | ((value: any, key: string, item: any) => any)
118121
thClass?: string | string[]
119122
thStyle?: any

0 commit comments

Comments
 (0)