Skip to content

Commit

Permalink
feat(b-table): allow users to specify top-level keys to be ignored or…
Browse files Browse the repository at this point in the history
… included when filtering, plus add option to filter based on formatted value (closes #3749) (#3786)
  • Loading branch information
tmorehouse committed Jul 30, 2019
1 parent bcb132e commit 142b31b
Show file tree
Hide file tree
Showing 6 changed files with 248 additions and 109 deletions.
223 changes: 159 additions & 64 deletions src/components/table/README.md

Large diffs are not rendered by default.

52 changes: 36 additions & 16 deletions src/components/table/helpers/mixin-filtering.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import cloneDeep from '../../../utils/clone-deep'
import looseEqual from '../../../utils/loose-equal'
import warn from '../../../utils/warn'
import { concat } from '../../../utils/array'
import { isFunction, isString, isRegExp } from '../../../utils/inspect'
import stringifyRecordValues from './stringify-record-values'

Expand All @@ -20,6 +21,14 @@ export default {
filterFunction: {
type: Function,
default: null
},
filterIgnoredFields: {
type: Array
// default: undefined
},
filterIncludedFields: {
type: Array
// default: undefined
}
},
data() {
Expand All @@ -29,6 +38,12 @@ export default {
}
},
computed: {
computedFilterIgnored() {
return this.filterIgnoredFields ? concat(this.filterIgnoredFields).filter(Boolean) : null
},
computedFilterIncluded() {
return this.filterIncludedFields ? concat(this.filterIncludedFields).filter(Boolean) : null
},
localFiltering() {
return this.hasProvider ? !!this.noProviderFiltering : true
},
Expand Down Expand Up @@ -148,10 +163,10 @@ export default {
methods: {
// Filter Function factories
filterFnFactory(filterFn, criteria) {
// Wrapper factory for external filter functions.
// Wrap the provided filter-function and return a new function.
// Returns null if no filter-function defined or if criteria is falsey.
// Rather than directly grabbing this.computedLocalFilterFn or this.filterFunction
// Wrapper factory for external filter functions
// Wrap the provided filter-function and return a new function
// Returns `null` if no filter-function defined or if criteria is falsey
// Rather than directly grabbing `this.computedLocalFilterFn` or `this.filterFunction`
// we have it passed, so that the caller computed prop will be reactive to changes
// in the original filter-function (as this routine is a method)
if (
Expand Down Expand Up @@ -184,34 +199,39 @@ export default {
// Build the regexp needed for filtering
let regexp = criteria
if (isString(regexp)) {
// Escape special RegExp characters in the string and convert contiguous
// whitespace to \s+ matches
// Escape special `RegExp` characters in the string and convert contiguous
// whitespace to `\s+` matches
const pattern = criteria
.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')
.replace(/[\s\uFEFF\xA0]+/g, '\\s+')
// Build the RegExp (no need for global flag, as we only need
// Build the `RegExp` (no need for global flag, as we only need
// to find the value once in the string)
regexp = new RegExp(`.*${pattern}.*`, 'i')
}

// Generate the wrapped filter test function to use
const fn = item => {
// This searches all row values (and sub property values) in the entire (excluding
// special _ prefixed keys), because we convert the record to a space-separated
// special `_` prefixed keys), because we convert the record to a space-separated
// string containing all the value properties (recursively), even ones that are
// not visible (not specified in this.fields).
// not visible (not specified in this.fields)
// Users can ignore filtering on specific fields, or on only certain fields,
// and can optionall specify searching results of fields with formatter
//
// TODO: Enable searching on formatted fields and scoped slots
// TODO: Should we filter only on visible fields (i.e. ones in this.fields) by default?
// TODO: Allow for searching on specific fields/key, this could be combined with the previous TODO
// TODO: Give stringifyRecordValues extra options for filtering (i.e. passing the
// fields definition and a reference to $scopedSlots)
// TODO: Enable searching on scoped slots
//
// Generated function returns true if the criteria matches part of
// the serialized data, otherwise false
// We set lastIndex = 0 on regex in case someone uses the /g global flag
// We set `lastIndex = 0` on the `RegExp` in case someone specifies the `/g` global flag
regexp.lastIndex = 0
return regexp.test(stringifyRecordValues(item))
return regexp.test(
stringifyRecordValues(
item,
this.computedFilterIgnored,
this.computedFilterIncluded,
this.computedFieldsObj
)
)
}

// Return the generated function
Expand Down
54 changes: 32 additions & 22 deletions src/components/table/helpers/mixin-items.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ export default {
default: null
},
primaryKey: {
// Primary key for record.
// If provided the value in each row must be unique!!!
// Primary key for record
// If provided the value in each row must be unique!
type: String,
default: null
},
value: {
// v-model for retrieving the current displayed rows
// `v-model` for retrieving the current displayed rows
type: Array,
default() {
return []
Expand All @@ -32,21 +32,38 @@ export default {
},
data() {
return {
// Our local copy of the items. Must be an array
// Our local copy of the items
// Must be an array
localItems: isArray(this.items) ? this.items.slice() : []
}
},
computed: {
computedFields() {
// We normalize fields into an array of objects
// [ { key:..., label:..., ...}, {...}, ..., {..}]
// `[ { key:..., label:..., ...}, {...}, ..., {..}]`
return normalizeFields(this.fields, this.localItems)
},
computedFieldsObj() {
// Fields as a simple lookup hash object
// Mainly for formatter lookup and scopedSlots for convenience
// Mainly for formatter lookup and use in `scopedSlots` for convenience
// If the field has a formatter, it normalizes formatter to a
// function ref or `undefined` if no formatter
const parent = this.$parent
return this.computedFields.reduce((obj, f) => {
obj[f.key] = f
// We use object spread here so we don't mutate the original field object
obj[f.key] = { ...f }
if (f.formatter) {
// Normalize formatter to a function ref or `undefined`
let formatter = f.formatter
if (isString(formatter) && isFunction(parent[formatter])) {
formatter = parent[formatter]
} else if (!isFunction(formatter)) {
/* istanbul ignore next */
formatter = undefined
}
// Return formatter function or `undefined` if none
obj[f.key].formatter = formatter
}
return obj
}, {})
},
Expand Down Expand Up @@ -76,43 +93,36 @@ export default {
items(newItems) {
/* istanbul ignore else */
if (isArray(newItems)) {
// Set localItems/filteredItems to a copy of the provided array
// Set `localItems`/`filteredItems` to a copy of the provided array
this.localItems = newItems.slice()
} else if (isUndefined(newItems) || isNull(newItems)) {
/* istanbul ignore next */
this.localItems = []
}
},
// Watch for changes on computedItems and update the v-model
// Watch for changes on `computedItems` and update the `v-model`
computedItems(newVal) {
this.$emit('input', newVal)
},
// Watch for context changes
context(newVal, oldVal) {
// Emit context info for external paging/filtering/sorting handling
// Emit context information for external paging/filtering/sorting handling
if (!looseEqual(newVal, oldVal)) {
this.$emit('context-changed', newVal)
}
}
},
mounted() {
// Initially update the v-model of displayed items
// Initially update the `v-model` of displayed items
this.$emit('input', this.computedItems)
},
methods: {
// Method to get the formatter method for a given field key
getFieldFormatter(key) {
const fieldsObj = this.computedFieldsObj
const field = fieldsObj[key]
const parent = this.$parent
let formatter = field && field.formatter
if (isString(formatter) && isFunction(parent[formatter])) {
formatter = parent[formatter]
} else if (!isFunction(formatter)) {
formatter = undefined
}
// Return formatter function or undefined if none
return formatter
const field = this.computedFieldsObj[key]
// `this.computedFieldsObj` has pre-normalized the formatter to a
// function ref if present, otherwise `undefined`
return field ? field.formatter : undefined
}
}
}
16 changes: 12 additions & 4 deletions src/components/table/helpers/sanitize-row.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import { keys } from '../../../utils/object'
import { arrayIncludes } from '../../../utils/array'
import { IGNORED_FIELD_KEYS } from './constants'

// Return a copy of a row after all reserved fields have been filtered out
// TODO: add option to specify which fields to include
const sanitizeRow = row =>
const sanitizeRow = (row, ignoreFields, includeFields, fieldsObj = {}) =>
keys(row).reduce((obj, key) => {
// Ignore special fields that start with `_`
if (!IGNORED_FIELD_KEYS[key]) {
obj[key] = row[key]
// Ignore fields in the `ignoreFields` array
// Include only fields in the `includeFields` array
if (
!IGNORED_FIELD_KEYS[key] &&
!(ignoreFields && ignoreFields.length > 0 && arrayIncludes(ignoreFields, key)) &&
!(includeFields && includeFields.length > 0 && !arrayIncludes(includeFields, key))
) {
const f = fieldsObj[key]
const val = row[key]
obj[key] = f && f.filterByFormatted && f.formatter ? f.formatter(val, key, row) : val
}
return obj
}, {})
Expand Down
9 changes: 6 additions & 3 deletions src/components/table/helpers/stringify-record-values.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ import sanitizeRow from './sanitize-row'
import stringifyObjectValues from './stringify-object-values'

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

export default stringifyRecordValues
3 changes: 3 additions & 0 deletions src/components/table/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export declare class BTable extends BvComponent {
currentPage?: number | string
filter?: string | Array<any> | RegExp | object | any
filterFunction?: BvTableFilterCallback
filterIgnoredFields?: Array<string>
filterIncludedFields?: Array<string>
busy?: boolean
tbodyTrClass?: string | Array<any> | object | BvTableTbodyTrClassCallback
}
Expand Down Expand Up @@ -114,6 +116,7 @@ export interface BvTableField {
sortable?: boolean
sortDirection?: BvTableSortDirection
sortByFormatted?: boolean
filterByFormatted?: boolean
tdClass?: string | string[] | ((value: any, key: string, item: any) => any)
thClass?: string | string[]
thStyle?: any
Expand Down

0 comments on commit 142b31b

Please sign in to comment.