` field `` cell. If custom attributes per cell are required, a callback function can be specified instead. |
+| `isRowHeader` | Boolean | When set to `true`, the field's item data cell will be rendered with ` ` rather than the default of ` `. |
+| `stickyColumn` | Boolean | NEW in 2.0.0-rc.28 When set to `true`, and the table in in [responsive](#responsive-tables) mode or has [sticky headers](#sticky-headers), will cause the column to become fixed to the left when the table's horizontal scrollbar is scrolled. See [Sticky columns](#sticky-columns) for more details |
**Notes:**
-- _Field properties, if not present, default to `null` (falsey) unless otherwise stated above._
-- _`class`, `thClass`, `tdClass` etc. will not work with classes that are defined in scoped CSS_
-- _For information on the syntax supported by `thStyle`, see
+- Field properties, if not present, default to `null` (falsey) unless otherwise stated above.
+- `class`, `thClass`, `tdClass` etc. will not work with classes that are defined in scoped CSS,
+ unless you are using VueLoader's
+ [Deep selector](https://vue-loader.vuejs.org/guide/scoped-css.html#child-component-root-elements).
+- For information on the syntax supported by `thStyle`, see
[Class and Style Bindings](https://vuejs.org/v2/guide/class-and-style.html#Binding-Inline-Styles)
- in the Vue.js guide._
-- _Any additional properties added to the field objects will be left intact - so you can access them
- via the named scoped slots for custom data, header, and footer rendering._
+ in the Vue.js guide.
+- Any additional properties added to the field objects will be left intact - so you can access them
+ via the named scoped slots for custom data, header, and footer rendering.
For information and usage about scoped slots and formatters, refer to the
-[**Custom Data Rendering**](#custom-data-rendering) section below.
+[Custom Data Rendering](#custom-data-rendering) section below.
Feel free to mix and match simple array and object array together:
@@ -359,8 +364,8 @@ const fields = [
## Primary key
-`` provides an additional prop `primary-key`, which you can use to identify the field key
-that _uniquely_ identifies the row.
+`` provides an additional prop `primary-key`, which you can use to identify the _name_ of
+the field key that _uniquely_ identifies the row.
The value specified by the primary column key **must be** either a `string` or `number`, and **must
be unique** across all rows in the table.
@@ -381,8 +386,8 @@ Internally, the value of the field key specified by the `primary-key` prop is us
value for each rendered item row `` element.
If you are seeing rendering issue (i.e. tooltips hiding or unexpected subcomponent re-usage when
-item data changes or data is sorted/filtered/edited), setting the `primary-key` prop (if you have a
-unique identifier per row) can alleviate these issues.
+item data changes or data is sorted/filtered/edited) or table row transitions are not working,
+setting the `primary-key` prop (if you have a unique identifier per row) can alleviate these issues.
Specifying the `primary-key` column is handy if you are using 3rd party table transitions or drag
and drop plugins, as they rely on having a consistent and unique per row `:key` value.
@@ -393,35 +398,44 @@ components/elements that are rendering with previous results (i.e. being re-used
patch optimization routines). Specifying a `primary-key` column can alleviate this issue (or you can
place a unique `:key` on your element/components in your custom formatted field slots).
+Refer to the [Table body transition support](#table-body-transition-support) section for additional
+details.
+
## Table style options
### Table styling
`` provides several props to alter the style of the table:
-| prop | Type | Description |
-| ------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `striped` | Boolean | Add zebra-striping to the table rows within the `` |
-| `bordered` | Boolean | For borders on all sides of the table and cells. |
-| `borderless` | Boolean | removes inner borders from table. |
-| `outlined` | Boolean | For a thin border on all sides of the table. Has no effect if `bordered` is set. |
-| `small` | Boolean | To make tables more compact by cutting cell padding in half. |
-| `hover` | Boolean | To enable a hover highlighting state on table rows within a ` ` |
-| `dark` | Boolean | Invert the colors — with light text on dark backgrounds (equivalent to Bootstrap v4 class `.table-dark`) |
-| `fixed` | Boolean | Generate a table with equal fixed-width columns (`table-layout: fixed;`) |
-| `foot-clone` | Boolean | Turns on the table footer, and defaults with the same contents a the table header |
-| `no-footer-sorting` | Boolean | When `foot-clone` is true and the table is sortable, disables the sorting icons and click behaviour on the footer heading cells. Refer to the [**Sorting**](#sorting) section below for more details. |
-| `responsive` | Boolean or String | Generate a responsive table to make it scroll horizontally. Set to `true` for an always responsive table, or set it to one of the breakpoints `'sm'`, `'md'`, `'lg'`, or `'xl'` to make the table responsive (horizontally scroll) only on screens smaller than the breakpoint. See [**Responsive tables**](#responsive-tables) below for details. |
-| `stacked` | Boolean or String | Generate a responsive stacked table. Set to `true` for an always stacked table, or set it to one of the breakpoints `'sm'`, `'md'`, `'lg'`, or `'xl'` to make the table visually stacked only on screens smaller than the breakpoint. See [**Stacked tables**](#stacked-tables) below for details. |
-| `head-variant` | String | Use `'light'` or `'dark'` to make table header appear light or dark gray, respectively |
-| `foot-variant` | String | Use `'light'` or `'dark'` to make table footer appear light or dark gray, respectively. If not set, `head-variant` will be used. Has no effect if `foot-clone` is not set |
+| prop | Type | Description |
+| ------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `striped` | Boolean | Add zebra-striping to the table rows within the ` ` |
+| `bordered` | Boolean | For borders on all sides of the table and cells. |
+| `borderless` | Boolean | removes inner borders from table. |
+| `outlined` | Boolean | For a thin border on all sides of the table. Has no effect if `bordered` is set. |
+| `small` | Boolean | To make tables more compact by cutting cell padding in half. |
+| `hover` | Boolean | To enable a hover highlighting state on table rows within a ` ` |
+| `dark` | Boolean | Invert the colors — with light text on dark backgrounds (equivalent to Bootstrap v4 class `.table-dark`) |
+| `fixed` | Boolean | Generate a table with equal fixed-width columns (`table-layout: fixed;`) |
+| `responsive` | Boolean or String | Generate a responsive table to make it scroll horizontally. Set to `true` for an always responsive table, or set it to one of the breakpoints `'sm'`, `'md'`, `'lg'`, or `'xl'` to make the table responsive (horizontally scroll) only on screens smaller than the breakpoint. See [Responsive tables](#responsive-tables) below for details. |
+| `sticky-header` | Boolean or String | NEW in 2.0.0-rc.28 Generates a vertically scrollable table with sticky headers. Set to `true` to enable sticky headers (default table max-height of `300px`), or set it to a string containing a height (with CSS units) to specify a maximum height other than `300px`. See the [Sticky header](#sticky-headers) section below for details. |
+| `stacked` | Boolean or String | Generate a responsive stacked table. Set to `true` for an always stacked table, or set it to one of the breakpoints `'sm'`, `'md'`, `'lg'`, or `'xl'` to make the table visually stacked only on screens smaller than the breakpoint. See [Stacked tables](#stacked-tables) below for details. |
+| `caption-top` | Boolean | If the table has a caption, and this prop is set to `true`, the caption will be visually placed above the table. If `false` (the default), the caption will be visually placed below the table. |
+| `table-variant` | String | NEW in 2.0.0-rc.28 Give the table an overall theme color variant. |
+| `head-variant` | String | Use `'light'` or `'dark'` to make table header appear light or dark gray, respectively |
+| `foot-variant` | String | Use `'light'` or `'dark'` to make table footer appear light or dark gray, respectively. If not set, `head-variant` will be used. Has no effect if `foot-clone` is not set |
+| `foot-clone` | Boolean | Turns on the table footer, and defaults with the same contents a the table header |
+| `no-footer-sorting` | Boolean | When `foot-clone` is true and the table is sortable, disables the sorting icons and click behaviour on the footer heading cells. Refer to the [Sorting](#sorting) section below for more details. |
+
+**Note:** table style options `fixed`, `stacked`, and `caption-top`, and the table sorting feature,
+requires BootstrapVue's custom CSS.
**Example: Basic table styles**
```html
-
+
Striped
Bordered
Borderless
@@ -432,6 +446,22 @@ place a unique `:key` on your element/components in your custom formatted field
Fixed
Foot Clone
+
+
+ None
+ Light
+ Dark
+
+
+
+
+ -- None --
+
+
@@ -459,6 +491,16 @@ place a unique `:key` on your element/components in your custom formatted field
{ age: 21, first_name: 'Larsen', last_name: 'Shaw' },
{ age: 89, first_name: 'Geneva', last_name: 'Wilson' }
],
+ tableVariants: [
+ 'primary',
+ 'secondary',
+ 'info',
+ 'danger',
+ 'warning',
+ 'success',
+ 'light',
+ 'dark'
+ ],
striped: false,
bordered: false,
borderless: false,
@@ -467,7 +509,9 @@ place a unique `:key` on your element/components in your custom formatted field
hover: false,
dark: false,
fixed: false,
- footClone: false
+ footClone: false,
+ headVariant: null,
+ tableVariant: ''
}
}
}
@@ -529,7 +573,11 @@ values: `sm`, `md`, `lg`, or `xl`.
```html
-
+
+
+ {{ scope.label }}
+ {{ scope.value }}
+
@@ -600,7 +648,8 @@ breakpoint values `'sm'`, `'md'`, `'lg'`, or `'xl'`.
Column header labels will be rendered to the left of each field value using a CSS `::before` pseudo
element, with a width of 40%.
-The prop `stacked` takes precedence over the `responsive` prop.
+The prop `stacked` takes precedence over the `responsive` prop, [`sticky-header`](#sticky-headers)
+props, and the [`stickyColumn`](#sticky-columns) field definition property.
**Example: Always stacked table**
@@ -634,14 +683,16 @@ The prop `stacked` takes precedence over the `responsive` prop.
- Custom rendered header slots will not be shown, rather, the fields' `label` will be used.
- The table **cannot** be sorted by clicking the rendered field labels. You will need to provide an
external control to select the field to sort by and the sort direction. See the
- [**Sorting**](#sorting) section below for sorting control information, as well as the
- [**complete example**](#complete-example) at the bottom of this page for an example of controlling
+ [Sorting](#sorting) section below for sorting control information, as well as the
+ [complete example](#complete-example) at the bottom of this page for an example of controlling
sorting via the use of form controls.
- The slots `top-row` and `bottom-row` will be hidden when visually stacked.
- The table caption, if provided, will always appear at the top of the table when visually stacked.
- In an always stacked table, the table header and footer, and the fixed top and bottom row slots
will not be rendered.
+BootstrapVue's custom CSS is required in order to support stacked tables.
+
### Table caption
Add an optional caption to your table via the prop `caption` or the named slot `table-caption` (the
@@ -716,10 +767,35 @@ usage of ``
Slot `table-colgroup` can be optionally scoped, receiving an object with the following properties:
-| Property | Type | Description |
-| --------- | ------ | ----------------------------------------------------------------------------- |
-| `columns` | Number | The number of columns in the rendered table |
-| `fields` | Array | Array of field definition objects (normalized to the array of objects format) |
+| Property | Type | Description |
+| --------- | ------ | --------------------------------------------------------------------------------------------------------------- |
+| `columns` | Number | The number of columns in the rendered table |
+| `fields` | Array | Array of field definition objects (normalized to the [array of objects](#fields-as-an-array-of-objects) format) |
+
+When provided, the content of the `table-colgroup` slot will be placed _inside_ of a ` `
+element. there is no need to provide your own outer ` ` element. When a series of table
+columns should be grouped for assistive technology reasons (for conveying logical column
+associations, use a ` ` element (with `#` replaced with the number of grouped columns)
+to group the series of columns.
+
+**Tip:** In some situations when trying to set column widths via `style` or `class` on the ` `
+element, you may find that placing the table in `fixed` header width (table fixed layout mode) mode,
+combined with `responsive` (horizontal scrolling) mode will help, although you will need to have
+explicit widths, or minimum widths, via a style or a class for each column's respective ` `
+element. For example:
+
+```html
+
+
+
+
+
+
+```
### Table busy state
@@ -783,8 +859,8 @@ the table's busy state is `true`. The slot will be placed in a ` ` element wi
```
-Also see the [**Using Items Provider Functions**](#using-items-provider-functions) below for
-additional information on the `busy` state.
+Also see the [Using Items Provider Functions](#using-items-provider-functions) below for additional
+information on the `busy` state.
**Notes:**
@@ -800,9 +876,21 @@ function.
### Scoped field slots
-Scoped slots give you greater control over how the record data appears. If you want to add an extra
-field which does not exist in the records, just add it to the `fields` array, And then reference the
-field(s) in the scoped slot(s).
+CHANGED in 2.0.0-rc.28
+
+Scoped field slots give you greater control over how the record data appears. If you want to add an
+extra field which does not exist in the records, just add it to the `fields` array, And then
+reference the field(s) in the scoped slot(s). Scoped field slots use the following naming syntax:
+`'[' + field key + ']'`.
+
+NEW in 2.0.0-rc.28 You can use the default _fall-back_
+scoped slot `'[]'` to format any cells that do not have an explicit scoped slot provided.
+
+DEPRECATION in 2.0.0-rc.28 Versions prior to
+`2.0.0-rc.28` did not surround the field key with square brackets, which could cause slot name
+collisions (i.e. if you had a field key `default`). Using the old field slot names has been
+deprecated in favour of the new bracketed syntax, and support will be removed in a future release.
+Users are encouraged to switch to the new bracketed syntax.
**Example: Custom data rendering with scoped slots**
@@ -811,19 +899,24 @@ field(s) in the scoped slot(s).
-
+
{{ data.index + 1 }}
-
- {{ data.value.first }} {{ data.value.last }}
+
+ {{ data.value.last }} , {{ data.value.first }}
-
+
{{ data.item.name.first }} is {{ data.item.age }} years old
+
+
+
+ {{ data.value }}
+
@@ -860,22 +953,22 @@ field(s) in the scoped slot(s).
The slot's scope variable (`data` in the above sample) will have the following properties:
-| Property | Type | Description |
-| ---------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `index` | Number | The row number (indexed from zero) relative to the displayed rows |
-| `item` | Object | The entire raw record data (i.e. `items[index]`) for this row (before any formatter is applied) |
-| `value` | Any | The value for this key in the record (`null` or `undefined` if a virtual column), or the output of the field's `formatter` function (see below for information on field `formatter` callback functions) |
-| `unformatted` | Any | The raw value for this key in the item record (`null` or `undefined` if a virtual column), before being passed to the field's `formatter` function |
-| `detailsShowing` | Boolean | Will be `true` if the row's `row-details` scoped slot is visible. See section [**Row details support**](#row-details-support) below for additional information |
-| `toggleDetails` | Function | Can be called to toggle the visibility of the rows `row-details` scoped slot. See section [**Row details support**](#row-details-support) below for additional information |
-| `rowSelected` | Boolean | Will be `true` if the row has been selected. See section [**Row select support**](#row-select-support) for additional information |
+| Property | Type | Description |
+| ---------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `index` | Number | The row number (indexed from zero) relative to the _displayed_ rows |
+| `item` | Object | The entire raw record data (i.e. `items[index]`) for this row (before any formatter is applied) |
+| `value` | Any | The value for this key in the record (`null` or `undefined` if a virtual column), or the output of the field's [`formatter` function](#formatter-callback) |
+| `unformatted` | Any | The raw value for this key in the item record (`null` or `undefined` if a virtual column), before being passed to the field's [`formatter` function](#formatter-callback) |
+| `detailsShowing` | Boolean | Will be `true` if the row's `row-details` scoped slot is visible. See section [Row details support](#row-details-support) below for additional information |
+| `toggleDetails` | Function | Can be called to toggle the visibility of the rows `row-details` scoped slot. See section [Row details support](#row-details-support) below for additional information |
+| `rowSelected` | Boolean | Will be `true` if the row has been selected. See section [Row select support](#row-select-support) for additional information |
**Notes:**
- `index` will not always be the actual row's index number, as it is computed after filtering,
sorting and pagination have been applied to the original table data. The `index` value will refer
to the **displayed row number**. This number will align with the indexes from the optional
- `v-model` bound variable.
+ [`v-model` bound](#v-model-binding) variable.
#### Displaying raw HTML
@@ -887,7 +980,7 @@ scoped field slot.
-
+
@@ -936,7 +1029,7 @@ formatted value as a string (HTML strings are not supported)
-
+
{{ data.value }}
@@ -993,67 +1086,78 @@ formatted value as a string (HTML strings are not supported)
```
-## Custom empty and emptyfiltered rendering via slots
+## Header and Footer custom rendering via scoped slots
-Aside from using `empty-text`, `empty-filtered-text`, `empty-html`, and `empty-filtered-html`, it is
-also possible to provide custom rendering for tables that have no data to display using named slots.
+CHANGED in 2.0.0-rc.28
-In order for these slots to be shown, the `show-empty` attribute must be set and `items` must be
-either falsy or an array of length 0.
+It is also possible to provide custom rendering for the tables `thead` and `tfoot` elements. Note by
+default the table footer is not rendered unless `foot-clone` is set to `true`.
-```html
-
-
-
- {{ scope.emptyText }}
-
-
- {{ scope.emptyFilteredText }}
-
-
-
-```
+Scoped slots for the header and footer cells uses a special naming convention of
+`'HEAD[]'` and `'FOOT[]'` respectively. if a `'FOOT[...]'` slot for a field is
+not provided, but a `'HEAD[...]'` slot is provided, then the footer will use the `'HEAD[...]'` slot
+content.
-The slot can optionally be scoped. The slot's scope (`scope` in the above example) will have the
-following properties:
+NEW in 2.0.0-rc.28 You can use a default _fall-back_
+scoped slot `'HEAD[]'` or `'FOOT[]'` to format any header or footer cells that do not have an
+explicit scoped slot provided.
-| Property | Type | Description |
-| ------------------- | ------ | -------------------------------------------------- |
-| `emptyHtml` | String | The `empty-html` prop |
-| `emptyText` | String | The `empty-text` prop |
-| `emptyFilteredHtml` | String | The `empty-filtered-html` prop |
-| `emptyFilteredText` | String | The `empty-filtered-text` prop |
-| `fields` | Array | The `fields` prop |
-| `items` | Array | The `items` prop. Exposed here to check null vs [] |
+DEPRECATION in 2.0.0-rc.28 Versions prior to
+`2.0.0-rc.28` used slot names `'HEAD_'` and `'FOOT_'`. Using the old slot names has been
+deprecated in favour of the new bracketed syntax, and support will be removed in a future release.
+Users are encouraged to switch to the new bracketed syntax.
-## Header and Footer custom rendering via scoped slots
+```html
+
+
+
+
+
+ {{ data.value.first }} {{ data.value.last }}
+
-It is also possible to provide custom rendering for the tables `thead` and `tfoot` elements. Note by
-default the table footer is not rendered unless `foot-clone` is set to `true`.
+
+
+ {{ data.label }}
+
-Scoped slots for the header and footer cells uses a special naming convention of `HEAD_`
-and `FOOT_` respectively. if a `FOOT_` slot for a field is not provided, but a `HEAD_`
-slot is provided, then the footer will use the `HEAD_` slot content.
+
+
+ {{ data.label }}
+
-```html
-
-
-
-
- {{ data.value.first }} {{ data.value.last }}
-
+
+
+ {{ data.label }}
+
+
+
+
-
-
- {{ data.label }}
-
+
-
-
- {{ data.label }}
-
-
-
+
```
The slots can be optionally scoped (`data` in the above example), and will have the following
@@ -1065,15 +1169,19 @@ properties:
| `field` | Object | the field's object (from the `fields` prop) |
| `label` | String | The fields label value (also available as `data.field.label`) |
-When placing inputs, buttons, selects or links within a `HEAD_` or `FOOT_` slot, note that
+When placing inputs, buttons, selects or links within a `HEAD[...]` or `FOOT[...]` slot, note that
`head-clicked` event will not be emitted when the input, select, textarea is clicked (unless they
are disabled). `head-clicked` will never be emitted when clicking on links or buttons inside the
scoped slots (even when disabled)
### Adding additional rows to the header
+ENHANCED in 2.0.0-rc.28
+
If you wish to add additional rows to the header you may do so via the `thead-top` slot. This slot
-is inserted before the header cells row, and is not encapsulated by `.. ` tags.
+is inserted before the header cells row, and is not automatically encapsulated by `.. `
+tags. It is recommended to use the BootstrapVue [table helper components](#table-helper-components),
+rather than native browser table child elements.
```html
@@ -1084,12 +1192,12 @@ is inserted before the header cells row, and is not encapsulated by `..
responsive="sm"
>
-
-
- Type 1
- Type 2
- Type 3
-
+
+
+ Type 1
+ Type 2
+ Type 3
+
@@ -1100,20 +1208,20 @@ is inserted before the header cells row, and is not encapsulated by `..
data() {
return {
items: [
- { name: "Stephen Hawking", id: 1, type1: false, type2a: true, type2b: false, type2c: false, type3: false },
- { name: "Johnny Appleseed", id: 2, type1: false, type2a: true, type2b: true, type2c: false, type3: false },
- { name: "George Washington", id: 3, type1: false, type2a: false, type2b: false, type2c: false, type3: true },
- { name: "Albert Einstein", id: 4, type1: true, type2a: false, type2b: false, type2c: true, type3: false },
- { name: "Isaac Newton", id: 5, type1: true, type2a: true, type2b: false, type2c: true, type3: false },
+ { name: 'Stephen Hawking', id: 1, type1: false, type2a: true, type2b: false, type2c: false, type3: false },
+ { name: 'Johnny Appleseed', id: 2, type1: false, type2a: true, type2b: true, type2c: false, type3: false },
+ { name: 'George Washington', id: 3, type1: false, type2a: false, type2b: false, type2c: false, type3: true },
+ { name: 'Albert Einstein', id: 4, type1: true, type2a: false, type2b: false, type2c: true, type3: false },
+ { name: 'Isaac Newton', id: 5, type1: true, type2a: true, type2b: false, type2c: true, type3: false },
],
fields: [
- "name",
- { key: "id", label: "ID" },
- { key: "type1", label: "Type 1" },
- { key: "type2a", label: "Type 2A" },
- { key: "type2b", label: "Type 2B" },
- { key: "type2c", label: "Type 2C" },
- { key: "type3", label: "Type 3" }
+ 'name',
+ { key: 'id', label: 'ID' },
+ { key: 'type1', label: 'Type 1' },
+ { key: 'type2a', label: 'Type 2A' },
+ { key: 'type2b', label: 'Type 2B' },
+ { key: 'type2c', label: 'Type 2C' },
+ { key: 'type3', label: 'Type 3' }
]
}
}
@@ -1130,44 +1238,57 @@ Slot `thead-top` can be optionally scoped, receiving an object with the followin
| `columns` | Number | The number of columns in the rendered table |
| `fields` | Array | Array of field definition objects (normalized to the array of objects format) |
-## Row select support
+## Custom empty and emptyfiltered rendering via slots
+
+Aside from using `empty-text`, `empty-filtered-text`, `empty-html`, and `empty-filtered-html`, it is
+also possible to provide custom rendering for tables that have no data to display using named slots.
-You can make rows selectable, by using the prop `selectable`.
+In order for these slots to be shown, the `show-empty` attribute must be set and `items` must be
+either falsy or an array of length 0.
-Users can easily change the selecting mode by setting the `select-mode` prop.
+```html
+
+
+
+ {{ scope.emptyText }}
+
+
+ {{ scope.emptyFilteredText }}
+
+
+
+```
-- `multi`: each click will select/deselect the row (default mode)
-- `single`: only a single row can be selected at one time
-- `range`: any row clicked is selected, any other deselected. the SHIFT key selects a range of rows,
- and CTRL/CMD click will toggle the selected row.
+The slot can optionally be scoped. The slot's scope (`scope` in the above example) will have the
+following properties:
-When a table is `selectable` and the user clicks on a row, `` will emit the `row-selected`
-event, passing a single argument which is the complete list of selected items. **Treat this argument
-as read-only.**
+| Property | Type | Description |
+| ------------------- | ------ | -------------------------------------------------- |
+| `emptyHtml` | String | The `empty-html` prop |
+| `emptyText` | String | The `empty-text` prop |
+| `emptyFilteredHtml` | String | The `empty-filtered-html` prop |
+| `emptyFilteredText` | String | The `empty-filtered-text` prop |
+| `fields` | Array | The `fields` prop |
+| `items` | Array | The `items` prop. Exposed here to check null vs [] |
+
+## Advanced features
+
+### Sticky headers
+
+NEW in 2.0.0-rc.28
+
+Use the `sticky-header` prop to enable a vertically scrolling table with headers that remain fixed
+(sticky) as the table body scrolls. Setting the prop to `true` (or no explicit value) will generate
+a table that has a maximum height of `300px`. To specify a maximum height other than `300px`, set
+the `sticky-header` prop to a valid CSS height (including units), i.e. `sticky-header="200px"`.
+Tables with `sticky-header` enabled will also automatically become always responsive horizontally,
+regardless of the [`responsive`](#responsive-tables) prop setting, if the table is wider than the
+available horizontal space.
```html
-
-
-
-
-
-
-
- ✔
-
-
-
- {{ selected }}
+
@@ -1175,48 +1296,124 @@ as read-only.**
export default {
data() {
return {
- modes: ['multi', 'single', 'range'],
- fields: ['selected', 'isActive', 'age', 'first_name', 'last_name'],
items: [
- { isActive: true, age: 40, first_name: 'Dickerson', last_name: 'Macdonald' },
- { isActive: false, age: 21, first_name: 'Larsen', last_name: 'Shaw' },
- { isActive: false, age: 89, first_name: 'Geneva', last_name: 'Wilson' },
- { isActive: true, age: 38, first_name: 'Jami', last_name: 'Carney' }
- ],
- selectMode: 'multi',
- selected: []
- }
- },
- methods: {
- rowSelected(items) {
- this.selected = items
+ { 'heading 1': 'table cell', 'heading 2': 'table cell', 'heading 3': 'table cell' },
+ { 'heading 1': 'table cell', 'heading 2': 'table cell', 'heading 3': 'table cell' },
+ { 'heading 1': 'table cell', 'heading 2': 'table cell', 'heading 3': 'table cell' },
+ { 'heading 1': 'table cell', 'heading 2': 'table cell', 'heading 3': 'table cell' },
+ { 'heading 1': 'table cell', 'heading 2': 'table cell', 'heading 3': 'table cell' },
+ { 'heading 1': 'table cell', 'heading 2': 'table cell', 'heading 3': 'table cell' },
+ { 'heading 1': 'table cell', 'heading 2': 'table cell', 'heading 3': 'table cell' },
+ { 'heading 1': 'table cell', 'heading 2': 'table cell', 'heading 3': 'table cell' },
+ { 'heading 1': 'table cell', 'heading 2': 'table cell', 'heading 3': 'table cell' },
+ { 'heading 1': 'table cell', 'heading 2': 'table cell', 'heading 3': 'table cell' },
+ { 'heading 1': 'table cell', 'heading 2': 'table cell', 'heading 3': 'table cell' },
+ { 'heading 1': 'table cell', 'heading 2': 'table cell', 'heading 3': 'table cell' }
+ ]
}
}
}
-
+
```
-When table is selectable, it will have class `b-table-selectable`, and one of the following three
-classes (depending on which mode is in use), on the `` element:
+**Sticky header notes:**
-- `b-table-select-single`
-- `b-table-select-multi`
-- `b-table-select-range`
+- The `sticky-header` prop has no effect if the table has the [`stacked`](#stacked-tables) prop set.
+- Sticky header tables are wrapped inside a vertically scrollable `` with a maximum height set.
+- BootstrapVue's custom CSS is required in order to support `sticky-header`.
+- Bootstrap v4 uses the CSS style `border-collapse: collapsed` on table elements. This prevents the
+ borders on the sticky header from "sticking" to the header, and hence the borders will scroll when
+ the body scrolls.
+- The sticky header feature uses CSS style `position: sticky` to position the headings.
+- Internet Explorer does not support `position: sticky`, hence for IE11 the table headings will
+ scroll with the table body.
-When at least one row is selected the class `b-table-selecting` will be active on the `
`
-element.
+### Sticky columns
-**Notes:**
+NEW in 2.0.0-rc.28
+
+Columns can be made sticky, where they stick to the left of the table when the table has a
+horizontal scrollbar. To make a column a sticky column, set the `stickyColumn` prop in the
+[field's header definition](#field-definition-reference). Sticky columns will only work when the
+table has either the `sticky-header` prop set and/or the [`responsive`](#responsive-tables) prop is
+set.
+
+**Example: Sticky columns and headers**
+
+```html
+
+
+
Sticky header
+
+
+ Row ID
+
+ Heading {{ scope.label }}
+
+
+
+
+
+
-- _Paging, filtering, or sorting will clear the selection. The `row-selected` event will be emitted
- with an empty array if needed._
-- _Selected rows will have a class of `b-row-selected` added to them._
-- _When the table is in `selectable` mode, all data item `` elements will be in the document tab
- sequence (`tabindex="0"`) for accessibility reasons._
+
+```
-## Row details support
+**Sticky column notes:**
+
+- Sticky columns has no effect if the table has the [`stacked`](#stacked-tables) prop set.
+- Sticky columns tables require either the `sticky-header` and/or `responsive` modes, and are
+ wrapped inside a horizontally scrollable ``.
+- When you have multiple columns that are set as `stickyColumn`, the columns will stack over each
+ other visually, and the left-most sticky columns may "peek" out from under the next sticky column.
+ To get around this behaviour, make sure your latter stickyColumns are the same width or wider than
+ previous sticky columns.
+- Bootstrap v4 uses the CSS style `border-collapse: collapsed` on table elements. This prevents any
+ left or right borders on the sticky columns from "sticking" to the column, and hence those borders
+ will scroll when the body scrolls.
+- BootstrapVue's custom CSS is required in order to support sticky columns.
+- The sticky column feature uses CSS style `position: sticky` to position the column cells.
+- Internet Explorer does not support `position: sticky`, hence for IE11 the sticky column will
+ scroll with the table body.
+
+### Row details support
If you would optionally like to display additional record information (such as columns not specified
in the fields definition array), you can use the scoped slot `row-details`, in combination with the
@@ -1252,7 +1449,7 @@ initially showing.
-
+
{{ row.detailsShowing ? 'Hide' : 'Show'}} Details
@@ -1307,104 +1504,270 @@ initially showing.
```
-## Sorting
-
-ENHANCED in v2.0.0-rc.25
-
-As mentioned in the [**Fields**](#fields-column-definitions-) section above, you can make columns
-sortable. Clicking on a sortable column header will sort the column in ascending direction (smallest
-first), while clicking on it again will switch the direction of sorting. Clicking on a non-sortable
-column will clear the sorting. The prop `no-sort-reset` can be used to disable this feature.
-
-You can control which column is pre-sorted and the order of sorting (ascending or descending). To
-pre-specify the column to be sorted, set the `sort-by` prop to the field's key. Set the sort
-direction by setting `sort-desc` to either `true` (for descending) or `false` (for ascending, the
-default).
-
-- **Ascending**: Items are sorted lowest to highest (i.e. `A` to `Z`) and will be displayed with the
- lowest value in the first row with progressively higher values in the following rows. The header
- indicator arrow will point in the direction of lowest to highest. (i.e. down for ascending).
-- **Descending**: Items are sorted highest to lowest (i.e. `Z` to `A`) and will be displayed with
- the highest value in the first row with progressively lower values in the following rows. The
- header indicator arrow will point in the direction of lowest to highest (i.e. up for descending).
+### Row select support
-The props `sort-by` and `sort-desc` can be turned into _two-way_ (syncable) props by adding the
-`.sync` modifier. Your bound variables will then be updated accordingly based on the current sort
-criteria. See the [Vue docs](http://vuejs.org/v2/guide/components.html#sync-Modifier) for details on
-the `.sync` prop modifier.
+You can make rows selectable, by using the `` prop `selectable`.
-Setting `sort-by` to a column that is not defined in the fields as `sortable` will result in the
-table not being sorted.
+Users can easily change the selecting mode by setting the `select-mode` prop.
-When the prop `foot-clone` is set, the footer headings will also allow sorting by clicking, even if
-you have custom formatted footer field headers. To disable the sort icons and sorting via heading
-clicks in the footer, set the `no-footer-sorting` prop to true.
+- `multi`: each click will select/deselect the row (default mode)
+- `single`: only a single row can be selected at one time
+- `range`: any row clicked is selected, any other deselected. the SHIFT key selects a range of rows,
+ and CTRL/CMD click will toggle the selected row.
-The internal sort-compare routine uses `String.prototype.localeCompare()` for comparing the
-stringified column value (if value type is not `Number` or `Date`). `localeCompare()` accepts a
-`locale` string and an `options` object for controlling how strings are sorted. The default options
-used is `{ numeric: true }`, and locale is `undefined` (which uses the browser default locale).
+When a table is `selectable` and the user clicks on a row, `` will emit the `row-selected`
+event, passing a single argument which is the complete list of selected items. **Treat this argument
+as read-only.**
-NEW in v2.0.0-rc.25 You can change the locale via the
-`sort-compare-locale` prop to set the locale for sorting, as well as pass sort options via the
-`sort-compare-options` prop. Valid sort option properties are:
+Rows can also be programmatically selected and unselected via the following exposed methods on the
+`` instance (i.e. via a reference to the table instance via `this.$refs`):
-- `localeMatcher`: The locale matching algorithm to use. Possible values are `'lookup'` and
- `'best fit'`. The default is `'best fit'`. For information about this option, see the
- [MDN Intl page](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl#Locale_negotiation)
- for details.
-- `sensitivity`: Which differences in the strings should lead to non-zero compare result values.
- Possible values are:
- - `'base'`: Only strings that differ in base letters compare as unequal. Examples: `a ≠ b`,
- `a = á`, `a = A`.
- - `'accent'`: Only strings that differ in base letters or accents and other diacritic marks
- compare as unequal. Examples: `a ≠ b`, `a ≠ á`, `a = A`.
- - `'case'`: Only strings that differ in base letters or case compare as unequal. Examples:
- `a ≠ b`, `a = á`, `a ≠ A`.
- - `'variant'`: **(default)** Strings that differ in base letters, accents and other diacritic
- marks, or case compare as unequal. Other differences may also be taken into consideration.
- Examples: `a ≠ b`, `a ≠ á`, `a ≠ A`.
-- `ignorePunctuation`: Whether punctuation should be ignored. Possible values are `true` and
- `false`. The default is `false`.
-- `numeric`: Whether numeric collation should be used, such that `'1' < '2' < '10'`. Possible values
- are `true` and `false`. The default is `false`. Implementations are not required to support this
- property.
-- `caseFirst`: Whether upper case or lower case should sort first. Possible values are `'upper'`,
- `'lower'`, or `'false'` (use the locale's default). The default is `'false'`. Implementations are
- not required to support this property.
-- `'usage'`: Always set to `'sort'` by ``
+| Method | Description |
+| ---------------------- | ---------------------------------------------------------------------------------------------------- |
+| `selectRow(index)` | Selects a row with the given `index` number. |
+| `unselectRow(index)` | Unselects a row with the given `index` number. |
+| `selectAllRows()` | Selects all rows in the table, except in `single` mode in which case only the first row is selected. |
+| `clearSelected()` | Unselects all rows. |
+| `isRowSelected(index)` | Returns `true` if the row with the given `index` is selected, otherwise it returns `false`. |
-**Example 1:** If you want to sort German words, set `sort-compare-locale="de"` (in German, `ä`
-sorts _before_ `z`) or Swedish set `sort-compare-locale="sv"` (in Swedish, `ä` sorts _after_ `z`)
+Programmatic selection notes:
-**Example 2:** To compare numbers that are strings numerically, and to ignore case and accents:
+- `index` the zero-based index of the table's **visible rows**, after filtering, sorting, and
+ pagination have been applied.
+- In `single` mode, `selectRow(index)` will unselect any previous selected row.
+- Attempting to `selectRow(index)` or `unselectRow(index)` on a non-existent row will be ignored.
+- The table must be `selectable` for any of these methods to have effect.
```html
-
-```
-
+
+
+
+
+
+
+
+
+
+
+ ✓
+ Selected
+
+
+
+ Not selected
+
+
+
+
+ Select all
+ Clear selected
+ Select 3rd row
+ Unselect 3rd row
+
+
+ Selected Rows:
+ {{ selected }}
+
+
+
+
+
+
+
+```
+
+When a table is selectable, it will have class `b-table-selectable`, and one of the following three
+classes (depending on which mode is in use), on the `` element:
+
+- `b-table-select-single`
+- `b-table-select-multi`
+- `b-table-select-range`
+
+When at least one row is selected the class `b-table-selecting` will be active on the ``
+element.
+
+Use the prop `selected-variant` to apply a Bootstrap theme color to the selected row(s). Note, due
+to the order that the table variants are defined in Bootstrap's CSS, any row-variant's may take
+precedence over the `selected-variant`. You can set `selected-variant` to an empty string if you
+will be using other means to convey that a row is selected (such as a scoped field slot in the above
+example).
+
**Notes:**
-- The built-in `sort-compare` routine **cannot** sort sort based on the custom rendering of the
- field data (scoped slots are used only for presentation only, and do not affect the underlying
- data).
-- NEW in v2.0.0-rc.25 Fields that have a
- [`formatter` function](#formatter-callback) (virtual field or regular field) will be sorted by the
- value returned via the formatter function.
-- Refer to
- [MDN `String.prototype.localeCompare()` documentation](https://developer.mozilla.org/enUS/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare)
- for details on the options object property values.
-- Refer to
- [MDN locales documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl#locales_argument)
- for details on locale values.
-- Not all browsers (or Node.js) support the `locale` and `options` with
- `String.prototype.localeCompare()`. Refer to [Can I Use](https://caniuse.com/#feat=localecompare)
- for browser support. For Node.js, you may need to add in
- [Intl support](https://nodejs.org/api/intl.html) for handling locales, other than the default, to
- prevent [SSR hydration mismatch](https://ssr.vuejs.org/guide/hydration.html) errors.
+- Paging, filtering, or sorting will clear the selection. The `row-selected` event will be emitted
+ with an empty array if needed.
+- Selected rows will have a class of `b-row-selected` added to them.
+- When the table is in `selectable` mode, all data item `` elements will be in the document tab
+ sequence (`tabindex="0"`) for [accessibility](#accessibility) reasons, and will have the attribute
+ `aria-selected` set to either `'true'` or `'false'` depending on the selected state of the row.
+- When a table is `selectable`, the table will have the attribute `aria-multiselect` set to either
+ `'false'` for `single` mode, and `'true'` for either `multi` or `range` modes.
+
+### Table body transition support
+
+Vue transitions and animations are optionally supported on the ` ` element via the use of
+Vue's `` component internally. Three props are available for transitions support
+(all three default to undefined):
+
+| Prop | Type | Description |
+| --------------------------- | ------ | ----------------------------------------------------------------- |
+| `tbody-transition-props` | Object | Object of transition-group properties |
+| `tbody-transition-handlers` | Object | Object of transition-group event handlers |
+| `primary-key` | String | String specifying the field to use as a unique row key (required) |
-Refer to the [**Sort-compare routine**](#sort-compare-routine) section below for details on sorting
-by presentational data.
+To enable transitions you need to specify `tbody-transition-props` and/or
+`tbody-transition-handlers`, and must specify which field key to use as a unique key via the
+`primary-key` prop. Your data **must have** a column (specified by the `primary-key` prop) that has
+a **unique value per row** in order for transitions to work properly. The `primary-key` field's
+_value_ can either be a unique string or number. The field specified does not need to appear in the
+rendered table output, but it **must** exist in each row of your items data.
+
+You must also provide CSS to handle your transitions (if using CSS transitions) in your project.
+
+For more information of Vue's list rendering transitions, see the
+[Vue JS official docs](https://vuejs.org/v2/guide/transitions.html#List-Move-Transitions).
+
+In the example below, we have used the following custom CSS:
+
+```css
+table#table-transition-example .flip-list-move {
+ transition: transform 1s;
+}
+```
+
+```html
+
+
+
+
+
+
+
+
+
+```
+
+### `v-model` binding
+
+If you bind a variable to the `v-model` prop, the contents of this variable will be the currently
+displayed item records (zero based index, up to `page-size` - 1). This variable (the `value` prop)
+should usually be treated as readonly.
+
+The records within the `v-model` are a filtered/paginated _shallow copy_ of `items`, and hence any
+changes to a record's properties in the `v-model` will be reflected in the original `items` array
+(except when `items` is set to a provider function). Deleting a record from the `v-model` array will
+**not** remove the record from the original items array nor will it remove it from the displayed
+rows.
+
+**Note:** Do not bind any value directly to the `value` prop. Use the `v-model` binding.
+
+## Sorting
+
+ENHANCED in v2.0.0-rc.25
+
+As mentioned in the [Fields](#fields-column-definitions) section above, you can make columns
+sortable. Clicking on a sortable column header will sort the column in ascending direction (smallest
+first), while clicking on it again will switch the direction of sorting. Clicking on a non-sortable
+column will clear the sorting. The prop `no-sort-reset` can be used to disable this feature.
+
+You can control which column is pre-sorted and the order of sorting (ascending or descending). To
+pre-specify the column to be sorted, set the `sort-by` prop to the field's key. Set the sort
+direction by setting `sort-desc` to either `true` (for descending) or `false` (for ascending, the
+default).
+
+- **Ascending**: Items are sorted lowest to highest (i.e. `A` to `Z`) and will be displayed with the
+ lowest value in the first row with progressively higher values in the following rows. The header
+ indicator arrow will point in the direction of lowest to highest. (i.e. down for ascending).
+- **Descending**: Items are sorted highest to lowest (i.e. `Z` to `A`) and will be displayed with
+ the highest value in the first row with progressively lower values in the following rows. The
+ header indicator arrow will point in the direction of lowest to highest (i.e. up for descending).
+
+The props `sort-by` and `sort-desc` can be turned into _two-way_ (syncable) props by adding the
+`.sync` modifier. Your bound variables will then be updated accordingly based on the current sort
+criteria. See the [Vue docs](http://vuejs.org/v2/guide/components.html#sync-Modifier) for details on
+the `.sync` prop modifier.
+
+Setting `sort-by` to a column that is not defined in the fields as `sortable` will result in the
+table not being sorted.
+
+When the prop `foot-clone` is set, the footer headings will also allow sorting by clicking, even if
+you have custom formatted footer field headers. To disable the sort icons and sorting via heading
+clicks in the footer, set the `no-footer-sorting` prop to true.
```html
@@ -1452,17 +1815,106 @@ by presentational data.
### Sort-compare routine
-ENHANCED in v2.0.0-rc.25
+ENHANCED in v2.0.0-rc.28
-The built-in default `sort-compare` function sorts the specified field `key` based on the data in
-the underlying record object (formatted value if a field has a formatter function). The field value
-is first stringified if it is an object, and then sorted.
+The internal built-in default `sort-compare` function sorts the specified field `key` based on the
+data in the underlying record object (or by formatted value if a field has a formatter function, and
+the field has its `sortByFormatted` property is set to `true`). The field value is first stringified
+if it is an object and then sorted.
-The default `sort-compare` routine **cannot** sort based on the custom rendering of the field data
-(scoped slots are used only for presentation). For this reason, you can provide your own custom sort
-compare routine by passing a function reference to the prop `sort-compare`.
+**Notes:**
+
+- The built-in `sort-compare` routine **cannot** sort based on the custom rendering of the field
+ data: scoped slots are used only for _presentation only_, and do not affect the underlying data.
+- NEW in v2.0.0-rc.25
+ CHANGED in v2.0.0-rc.28 Fields that have a
+ [`formatter` function](#formatter-callback) (virtual field or regular field) can be sorted by the
+ value returned via the formatter function if the [field](#field-definition-reference) property
+ `sortByFormatted` is set to `true`. The default is `false` which will sort by the original field
+ value. This is only applicable for the built-in sort-compare routine.
+- NEW in v2.0.0-rc.28 By default, the internal sorting
+ routine will sort `null`, `undefined`, or empty string values first (less than any other values).
+ To sort so that `null`, `undefined` or empty string values appear last (greater than any other
+ value), set the `sort-null-last` prop to `true`.
+
+For customizing the sort-compare handling, refer to the
+[Custom sort-compare routine](#custom-sort-compare-routine) section below.
+
+### Internal sorting and locale handling
+
+The internal sort-compare routine uses
+[`String.prototype.localeCompare()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare)
+for comparing the stringified column value (if values being compared are not both `Number` or both
+`Date` types). The browser native `localeCompare()` method accepts a `locale` string (or array of
+locale strings) and an `options` object for controlling how strings are sorted. The default options
+are `{ numeric: true }`, and the locale is `undefined` (which uses the browser default locale).
-The `sort-compare` routine is passed seven (7) arguments:
+NEW in v2.0.0-rc.25 You can change the locale (or
+locales) via the `sort-compare-locale` prop to set the locale(s) for sorting, as well as pass sort
+options via the `sort-compare-options` prop.
+
+The `sort-compare-locale` prop defaults to `undefined`, which uses the browser (or Node.js runtime)
+default locale. The prop `sort-compare-locale` can either accept a
+[BCP 47 language tag](http://tools.ietf.org/html/rfc5646) string or an _array_ of such tags. For
+more details on locales, please see
+[Locale identification and negotiation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl#Locale_identification_and_negotiation)
+on MDN.
+
+The `sort-compare-options` prop accepts an object containing any of the following properties:
+
+- `localeMatcher`: The locale matching algorithm to use. Possible values are `'lookup'` and
+ `'best fit'`. The default is `'best fit'`. For information about this option, see the
+ [MDN Intl page](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl#Locale_negotiation)
+ for details.
+- `sensitivity`: Which differences in the strings should lead to _non-zero_ compare result values.
+ Possible values are:
+ - `'base'`: Only strings that differ in base letters compare as unequal. Examples: `a ≠ b`,
+ `a = á`, `a = A`.
+ - `'accent'`: Only strings that differ in base letters or accents and other diacritic marks
+ compare as unequal. Examples: `a ≠ b`, `a ≠ á`, `a = A`.
+ - `'case'`: Only strings that differ in base letters or case compare as unequal. Examples:
+ `a ≠ b`, `a = á`, `a ≠ A`.
+ - `'variant'`: **(default)** Strings that differ in base letters, accents and other diacritic
+ marks, or case compare as unequal. Other differences _may also_ be taken into consideration.
+ Examples: `a ≠ b`, `a ≠ á`, `a ≠ A`.
+- `ignorePunctuation`: Whether punctuation should be ignored. Possible values are `true` and
+ `false`. The default is `false`.
+- `numeric`: Whether numeric collation should be used, such that `'1' < '2' < '10'`. Possible values
+ are `true` and `false`. The default is `false`. Note that implementations (browsers, runtimes) are
+ not required to support this property, and therefore it might be ignored.
+- `caseFirst`: Whether upper case or lower case should sort first. Possible values are `'upper'`,
+ `'lower'`, or `'false'` (use the locale's default). The default is `'false'`. Implementations are
+ not required to support this property.
+- `'usage'`: **Always** set to `'sort'` by ``
+
+**Example 1:** If you want to sort German words, set `sort-compare-locale="de"` (in German, `ä`
+sorts _before_ `z`) or Swedish set `sort-compare-locale="sv"` (in Swedish, `ä` sorts _after_ `z`)
+
+**Example 2:** To compare numbers that are strings numerically, and to ignore case and accents:
+
+```html
+
+```
+
+**Notes:**
+
+- Refer to
+ [MDN `String.prototype.localeCompare()` documentation](https://developer.mozilla.org/enUS/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare)
+ for details on the options object property values.
+- Refer to
+ [MDN locales documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl#locales_argument)
+ for details on locale values.
+- Not all browsers (or Node.js) support the `locale` and `options` with
+ `String.prototype.localeCompare()`. Refer to [Can I Use](https://caniuse.com/#feat=localecompare)
+ for browser support. For Node.js, you may need to add in
+ [Intl support](https://nodejs.org/api/intl.html) for handling locales, other than the default, to
+ prevent [SSR hydration mismatch errors](https://ssr.vuejs.org/guide/hydration.html).
+
+### Custom sort-compare routine
+
+You can provide your own custom sort compare routine by passing a function reference to the prop
+`sort-compare`. The `sort-compare` routine is passed seven (7) arguments, of which the last 4 are
+optional:
- the first two arguments (`a` and `b`) are the _record objects_ for the rows being compared
- the third argument is the field `key` being sorted on (`sortBy`)
@@ -1470,7 +1922,9 @@ The `sort-compare` routine is passed seven (7) arguments:
for descending, `false` for ascending)
- the fifth argument is a reference to the field's [formatter function](#formatter-callback) (or
`undefined` if no field formatter). You will need to call this method to get the formatted field
- value: `val = formatter(a[key], key, a)`
+ value: `valA = formatter(a[key], key, a)` and `valB = formatter(b[key], key, b)`, if you need to
+ sort by the formatted value. This will be `undefined` if the field's `sortByFormatted` property is
+ not `true`
- the sixth argument is the value of the `sort-compare-options` prop (default is
`{ numeric: true }`)
- the seventh argument is the value of the `sort-compare-locale` prop (default is `undefined`)
@@ -1479,14 +1933,19 @@ The sixth and seventh arguments can be used if you are using the
[`String.prototype.localeCompare()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare)
method to compare strings.
-The routine should always return either `-1` for `a[key] < b[key]` , `0` for `a[key] === b[key]`, or
-`1` for `a[key] > b[key]` (the fourth argument, sorting direction, should not normally be used, as
-`b-table` will handle the direction, and is typically only needed when special handling of how
-`null` values are sorted). Your custom sort-compare routine can also return `null` or `false` to
-fall back to the _built-in sort-compare routine_ for the particular `key`. You can use this feature
-(i.e. by returning `null`) to have your custom sort-compare routine handle only certain fields
-(keys) such as the special case of virtual (scoped slot) columns, and have the internal sort-compare
-handle all other fields.
+In most typical situations, you only need to use the first three arguments. The fourth argument -
+sorting direction - should not normally be used, as `b-table` will handle the direction, and this
+value is typically only needed when special handling of how `null` and/or `undefined` values are
+sorted (i.e. sorting `null`/`undefined` first or last).
+
+The routine should return either `-1` (or a negative value) for `a[key] < b[key]` , `0` for
+`a[key] === b[key]`, or `1` (or a positive value) for `a[key] > b[key]`.
+
+Your custom sort-compare routine can also return `null` or `false`, to fall back to the _built-in
+sort-compare routine_ for the particular `key`. You can use this feature (i.e. by returning `null`)
+to have your custom sort-compare routine handle _only_ certain fields (keys) such as the special
+case of virtual (scoped slot) columns, and have the internal (built in) sort-compare handle all
+other fields.
The default sort-compare routine works similar to the following. Note the fourth argument (sorting
direction) is **not** used in the sort comparison:
@@ -1494,20 +1953,18 @@ direction) is **not** used in the sort comparison:
```js
-function sortCompare(aRow, bRow, key) {
- const a = aRow[key] // or use Lodash _.get()
+function sortCompare(aRow, bRow, key, sortDesc, formatter, compareOptions, compareLocale) {
+ const a = aRow[key] // or use Lodash `_.get()`
const b = bRow[key]
if (
(typeof a === 'number' && typeof b === 'number') ||
(a instanceof Date && b instanceof Date)
) {
- // If both compared fields are native numbers or both are dates
+ // If both compared fields are native numbers or both are native dates
return a < b ? -1 : a > b ? 1 : 0
} else {
// Otherwise stringify the field data and use String.prototype.localeCompare
- return toString(a).localeCompare(toString(b), undefined, {
- numeric: true
- })
+ return toString(a).localeCompare(toString(b), compareLocale, compareOptions)
}
}
@@ -1540,6 +1997,8 @@ with a single argument containing the context object of ``. See the
[Detection of sorting change](#detection-of-sorting-change) section below for details about the
sort-changed event and the context object.
+When `no-local-sorting` is true, the `sort-compare` prop has no effect.
+
### Change initial sort direction
Control the order in which ascending and descending sorting is applied when a sortable column header
@@ -1557,15 +2016,20 @@ unsorted to sorted), specify the property `sortDirection` in `fields`. See the
## Filtering
-Filtering, when used, is applied to the **original items** array data, and hence it is not currently
-possible to filter data based on custom rendering of virtual columns.
+ENHANCED in 2.0.0-rc.28
+
+Filtering, when used, is applied by default to the **original items** array data. `b-table` provides
+several options for how data is filtered.
+
+It is currently not possible to filter based on result of formatting via
+[scoped field slots](#scoped-field-slots).
### Built in filtering
The item's row data values are stringified (see the sorting section above for how stringification is
done) and the filter searches that stringified data (excluding any of the special properties that
-begin with an underscore `_`). The stringification also includes any data not shown in the presented
-columns.
+begin with an underscore `'_'`). The stringification also, by default, includes any data not shown
+in the presented columns.
With the default built-in filter function, The `filter` prop value can either be a string or a
`RegExp` object (regular expressions should _not_ have the `/g` global flag set).
@@ -1573,7 +2037,30 @@ With the default built-in filter function, The `filter` prop value can either be
If the stringified row contains the provided string value or matches the RegExp expression then it
is included in the displayed results.
-Set the `filter` prop to `null` or the empty string to clear the current filter.
+Set the `filter` prop to `null` or an empty string to clear the current filter.
+
+### Built in filtering options
+
+NEW in 2.0.0-rc.28
+
+There are several options for controlling what data the filter is applied against.
+
+- The `filter-ignored-fields` prop accepts an array of _top-level_ (immediate properties of the row
+ data) field keys that should be ignored when filtering.
+- The `filter-included-fields` prop accepts an array of _top-level_ (immediate properties of the row
+ data) field keys that should used when filtering. All other field keys not included in this array
+ will be ignored. This feature can be handy when you want to filter on specific columns. If the
+ specified array is empty, then _all_ fields are included, except those specified via the prop
+ `filter-ignored-fields`. If a field key is specified in both `filter-ignored-fields` and
+ `filter-included-fields`, then `filter-included-fields` takes precedence.
+- Normally, `` filters based on the stringified record data. If the field has a `formatter`
+ function specified, you can optionally filter based on the result of the formatter by setting the
+ [field definition property](#field-definition-reference) `filterByFormatted` to `true`. If the
+ field does not have a formatter function, this option is ignored.
+
+The props `filter-ignored-fields` and `filter-included-fields`, and the field definition property
+`filterByFormatted` have no effect when using a [custom filter function](#custom-filter-function),
+or [items provider](#using-items-provider-functions) based filtering.
### Custom filter function
@@ -1622,98 +2109,10 @@ You can use the [``](/docs/components/pagination) component in con
Setting `per-page` to `0` (default) will disable the local items pagination feature.
-## `v-model` binding
-
-If you bind a variable to the `v-model` prop, the contents of this variable will be the currently
-displayed item records (zero based index, up to `page-size` - 1). This variable (the `value` prop)
-should usually be treated as readonly.
-
-The records within the `v-model` are a filtered/paginated shallow copy of `items`, and hence any
-changes to a record's properties in the `v-model` will be reflected in the original `items` array
-(except when `items` is set to a provider function). Deleting a record from the `v-model` will
-**not** remove the record from the original items array nor will it remove it from the displayed
-rows.
-
-**Note:** _Do not bind any value directly to the `value` prop. Use the `v-model` binding._
-
-## Table body transition support
-
-Vue transitions and animations are optionally supported on the `` element via the use of
-Vue's `` component internally. Three props are available for transitions support
-(all three default to undefined):
-
-| Prop | Type | Description |
-| --------------------------- | ------ | ----------------------------------------------------------------- |
-| `tbody-transition-props` | Object | Object of transition-group properties |
-| `tbody-transition-handlers` | Object | Object of transition-group event handlers |
-| `primary-key` | String | String specifying the field to use as a unique row key (required) |
-
-To enable transitions you need to specify `tbody-transition-props` and/or
-`tbody-transition-handlers`, and must specify which field key to use as a unique key via the
-`primary-key` prop. Your data **must have** a column (specified by the `primary-key` prop) that has
-a **unique value per row** in order for transitions to work properly. The `primary-key` field's
-_value_ can either be a unique string or number. The field specified does not need to appear in the
-rendered table output, but it **must** exist in each row of your items data.
-
-You must also provide CSS to handle your transitions (if using CSS transitions) in your project.
-
-For more information of Vue's list rendering transitions, see the
-[Vue JS official docs](https://vuejs.org/v2/guide/transitions.html#List-Move-Transitions).
-
-In the example below, we have used the following custom CSS:
-
-```css
-table#table-transition-example .flip-list-move {
- transition: transform 1s;
-}
-```
-
-```html
-
-
-
-
-
-
-
-
-
-```
-
## Using items provider functions
-As mentioned under the [**Items**](#items-record-data-) prop section, it is possible to use a
-function to provide the row data (items), by specifying a function reference via the `items` prop.
+As mentioned under the [Items](#items-record-data) prop section, it is possible to use a function to
+provide the row data (items), by specifying a function reference via the `items` prop.
The provider function is called with the following signature:
@@ -1936,8 +2335,7 @@ export default {
```
You can also obtain the current sortBy and sortDesc values by using the `:sort-by.sync` and
-`:sort-desc.sync` two-way props respectively (see section [**Sorting**](#sorting) above for
-details).
+`:sort-desc.sync` two-way props respectively (see section [Sorting](#sorting) above for details).
```html
@@ -1958,7 +2356,7 @@ When `
` is mounted in the document, it will automatically trigger a pro
`` provides a great alternative to `` if you just need simple display of
tabular data. The `` component provides all of the styling and formatting features of
-`` (including row details support), while **excluding** the following features:
+`` (including row details and stacked support), while **excluding** the following features:
- Filtering
- Sorting
@@ -1969,27 +2367,370 @@ tabular data. The `` component provides all of the styling and for
- Fixed top and bottom rows
- Empty row support
+### Table lite as a plugin
+
+The `TablePlugin` includes ``. For convenience, BootstrapVue also provides a
+`TableLitePlugin` which installs only ``. `TableLitePlugin` is available as a top
+level named export.
+
+## Simple tables
+
+NEW in v2.0.0-rc.28
+
+The `` component gives the user complete control over the rendering of the table
+content, while providing basic Bootstrap v4 table styling. `` is a wrapper component
+around the `` element. Inside the component, via the `default` slot, you can use any or all
+of the BootstrapVue [table helper components](#table-helper-components): ``, ``,
+``, ``, ``, ``, and the HTML5 elements `` and ` ` and
+` `. Contrary to the component's name, one can create simple or complex table layouts with
+``.
+
+`` provides basic styling options via props: `striped`, `bordered`, `borderless`,
+`outlined`, `small`, `hover`, `dark`, `fixed`, `responsive` and `sticky-header`. Note that `stacked`
+mode is available but requires some additional markup to generate the cell headings, as described in
+the [Simple tables and stacked mode](#simple-tables-and-stacked-mode) section below. Sticky columns
+are also supported, but also require a bit of additional markup to specify which columns are to be
+sticky. See below for more information on using [sticky columns](#simple-tables-and-sticky-columns).
+
+Since `b-table-simple` is just a wrapper component, of which you will need to render content inside,
+it does not provide any of the advanced features of `` (i.e. row events, head events,
+sorting, pagination, filtering, foot-clone, etc).
+
+```html
+
+
+ Items sold in August, grouped by Country and City:
+
+
+
+
+
+ Region
+ Clothes
+ Accessories
+
+
+ Country
+ City
+ Trousers
+ Skirts
+ Dresses
+ Bracelets
+ Rings
+
+
+
+
+ Belgium
+ Antwerp
+ 56
+ 22
+ 43
+ 72
+ 23
+
+
+ Gent
+ 46
+ 18
+ 50
+ 61
+ 15
+
+
+ Brussels
+ 51
+ 27
+ 38
+ 69
+ 28
+
+
+ The Netherlands
+ Amsterdam
+ 89
+ 34
+ 69
+ 85
+ 38
+
+
+ Utrecht
+ 80
+ 12
+ 43
+ 36
+ 19
+
+
+
+
+
+ Total Rows: 5
+
+
+
+
+
+
+
+```
+
+When in `responsive` or `sticky-header` mode, the `` element is wrapped inside a ``
+element. If you need to apply additional classes to the `
` element, use the `table-classes`
+prop.
+
+Any additional attributes given to `` will always be applied to the ``
+element.
+
+### Simple tables and stacked mode
+
+A bit of additional markup is required on your `` body cells when the table is in
+stacked mode. Specifically, BootstrapVue uses a special data attribute to create the cell's heading,
+of which you can supply to `` or `` via the `stacked-heading` prop. Only plain strings
+are supported (not HTML markup), as we use the pseudo element `::before` and css `content` property.
+
+Here is the same table as above, set to be always stacked, which has the extra markup to handle
+stacked mode (specifically for generating the cell headings):
+
+```html
+
+
+ Items sold in August, grouped by Country and City:
+
+
+
+
+
+ Region
+ Clothes
+ Accessories
+
+
+ Country
+ City
+ Trousers
+ Skirts
+ Dresses
+ Bracelets
+ Rings
+
+
+
+
+ Belgium (3 Cities)
+ Antwerp
+ 56
+ 22
+ 43
+ 72
+ 23
+
+
+ Gent
+ 46
+ 18
+ 50
+ 61
+ 15
+
+
+ Brussels
+ 51
+ 27
+ 38
+ 69
+ 28
+
+
+ The Netherlands (2 Cities)
+ Amsterdam
+ 89
+ 34
+ 69
+ 85
+ 38
+
+
+ Utrecht
+ 80
+ 12
+ 43
+ 36
+ 19
+
+
+
+
+
+ Total Rows: 5
+
+
+
+
+
+
+
+```
+
+Like `` and ``, table headers (``) and footers (` `) are
+visually hidden when the table is visually stacked. If you need a header or footer, you can do so by
+creating an extra `` inside of the `` component (or in a second ``
+component), and set a role of `columnheader` on the child `` cells, and use Bootstrap v4
+[responsive display utility classes](/docs/reference/utility-classes) to hide the extra row (or
+``) above a certain breakpoint when the table is no longer visually stacked (the breakpoint
+should match the stacked table breakpoint you have set), i.e. `` would hide
+the row on medium and wider screens, while `` would hide the row group on
+medium and wider screens.
+
+**Note:** stacked mode with `` requires that you use the BootstrapVue
+[table helper components](#table-helper-components). Use of the regular ``, ``, ``
+and ` ` element tags will not work as expected, nor will they automatically apply any of the
+required accessibility attributes.
+
+### Simple tables and sticky columns
+
+Sticky columns are supported with ``, but you will need to set the `sticky-column`
+prop on each table cell (in the `thead`, `tbody`, and `tfoot` row groups) in the column that is to
+be sticky. For example:
+
+```html
+
+
+
+ Sticky Column Header
+ Heading 1
+ Heading 2
+ Heading 3
+ Heading 4
+
+
+
+
+ Sticky Column Row Header
+ Cell
+ Cell
+ Cell
+ Cell
+
+
+ Sticky Column Row Header
+ Cell
+ Cell
+ Cell
+ Cell
+
+
+
+
+ Sticky Column Footer
+ Heading 1
+ Heading 2
+ Heading 3
+ Heading 4
+
+
+
+```
+
+As with `` and ``, sticky columns are not supported when the `stacked` prop
+is set on ``.
+
+### Table simple as a plugin
+
+The `TablePlugin` includes `` and all of the helper components. For convenience,
+BootstrapVue also provides a `TableSimplePlugin` which installs `` and all of the
+helper components. `TableSimplePlugin` is available as a top level named export.
+
+## Table helper components
+
+NEW in v2.0.0-rc.28
+
+BootstrapVue provides additional helper child components when using ``, or the named
+slots `top-row`, `bottom-row`, and `thead-top` (all of which accept table child elements). The
+helper components are as follows:
+
+- `b-tbody`
+- `b-thead`
+- `b-tfoot`
+- `b-tr`
+- `b-td`
+- `b-th`
+
+These components are optimized to handle converting variants to the appropriate classes (such as
+handling table `dark` mode), and automatically applying certain accessibility attributes (i.e.
+`role`s and `scope`s) and can handle the stacked table and sticky-header requirements. Components
+`` and `` use these helper components internally.
+
+In the [Simple tables](#simple-tables) example, we are using the helper components ``,
+``, ``, ``, `` and ``. While you can use regular table child
+elements (i.e. ``, ``, ``, etc) within ``, and the named slots
+`top-row`, `bottom-row`, and `thead-top`, it is recommended to use these BootstrapVue table ``
+helper components. Note that there are no helper components for `` or ` `+` `,
+so you may these two HTML5 elements directly in ``.
+
+- Table helper components ``, `` and `` all accept a `variant` prop, which will
+ apply one of the Bootstrap theme colors (custom theme colors are supported via
+ [theming](/docs/reference/theming).) and will automatically adjust to use the correct variant
+ class based on the table's `dark` mode.
+- The helper components ``, `` accept a `head-variant` and `foot-variant` prop
+ respectively. Supported values are `'dark'`, `'light'` or `null` (`null` uses the default table
+ background). These variants also control the text color (light text for `'dark'` variant, and dark
+ text for the `'light'` variant).
+- Accessibility attributes `role` and `scope` are automatically set on `` and ``
+ components based on their location (thead, tbody, or tfoot) and their `rowspan` or `colspan`
+ props. You can override the automatic `scope` and `role` values by setting the appropriate
+ attribute on the helper component.
+- For ``, ``, and `` helper components, the appropriate default `role` of
+ `'rowgroup'` will be applied, unless you override the role by supplying a `role` attribute.
+- For the `` helper component, the appropriate default `role` of `'row'` will be applied,
+ unless you override the role by supplying a `role` attribute. `` does not add a `scope`.
+- The `` element supports rendering a Vue `` when either, or both, of the
+ `tbody-transition-props` and `tbody-transition-handlers` props are used. See the
+ [Table body transition support](#table-body-transition-support) section for more details.
+
## Accessibility
-When a column (field) is sortable, the header (and footer) heading cells will also be placed into
-the document tab sequence for accessibility.
+The `` and `` components, when using specific features, will attempt to
+provide the best accessibility markup possible.
+
+When using `` with the helper table components, elements will have the appropriate
+roles applied by default, of which you can optionally override. When using click handlers on the
+`` helper components, you will need to apply appropriate `aria-*` attributes, and
+set `tabindex="0"` to make the click actions accessible to screen reader and keyboard-only users.
+You should also listen for `@keydown.enter.prevent` to handle users pressing ENTER to
+trigger your click on cells or rows (required for accessibility for keyboard-only users).
+
+### Heading accessibility
-When the table is in `selectable` mode, or if there is a `row-clicked` event listener registered,
-all data item rows (`` elements) will be placed into the document tab sequence (via
-`tabindex="0"`) to allow keyboard-only and screen reader users the ability to click the rows.
+When a column (field) is sortable (`` only) or there is a `head-clicked` listener
+registered, the header (and footer) `` cells will be placed into the document tab sequence (via
+`tabindex="0"`) for accessibility by keyboard-only and screen reader users, so that the user may
+trigger a click (by pressing ENTER on the header cells.
-When the table items rows are in the table sequence, they will also support basic keyboard
-navigation when focused:
+### Data row accessibility
+
+When the table is in `selectable` mode (`` only), or if there is a `row-clicked` event
+listener registered (`` and ``), all data item rows (`` elements) will be
+placed into the document tab sequence (via `tabindex="0"`) to allow keyboard-only and screen reader
+users the ability to click the rows by pressing ENTER .
+
+When the table items rows are placed in the document tab sequence (`` and
+``), they will also support basic keyboard navigation when focused:
- DOWN will move to the next row
- UP will move to the previous row
- END or DOWN +SHIFT will move to the last row
- HOME or UP +SHIFT will move to the first row
-- ENTER or SPACE to click the row. SHIFT and CTRL
- modifiers will also work (depending on the table selectable mode).
+- ENTER or SPACE to click the row.
+- SHIFT and CTRL modifiers will also work (depending on the table selectable
+ mode, for `` only).
+
+### Row event accessibility
-Note the following row based events/actions are not considered accessible, and should only be used
-if the functionality is non critical or can be provided via other means:
+Note the following row based events/actions (available with `` and ``) are
+not considered accessible, and should only be used if the functionality is non critical or can be
+provided via other means:
- `row-dblclicked`
- `row-contextmenu`
@@ -1997,17 +2738,20 @@ if the functionality is non critical or can be provided via other means:
- `row-unhovered`
- `row-middle-clicked`
-Also, `row-middle-clicked` event is not supported in all browsers (i.e. IE, Safari and most mobile
-browsers). When listening for `row-middle-clicked` events originating on elements that do not
+Note that the `row-middle-clicked` event is not supported in all browsers (i.e. IE, Safari and most
+mobile browsers). When listening for `row-middle-clicked` events originating on elements that do not
support input or navigation, you will often want to explicitly prevent other default actions mapped
to the down action of the middle mouse button. On Windows this is usually autoscroll, and on macOS
and Linux this is usually clipboard paste. This can be done by preventing the default behaviour of
the `mousedown` or `pointerdown` event.
-Additionally, you may need to avoid opening a system context menu after a right click. Due to timing
-differences between operating systems, this too is not a preventable default behaviour of
-`row-middle-clicked`. Instead, this can be done by preventing the default behaviour of the
-`contextmenu` event.
+Additionally, you may need to avoid opening a default system or browser context menu after a right
+click. Due to timing differences between operating systems, this too is not a preventable default
+behaviour of `row-middle-clicked`. Instead, this can be done by preventing the default behaviour of
+the `row-contextmenu` event.
+
+It is recommended you test your app in as many browser and device variants as possible to ensure
+your app handles the various inconsistencies with events.
## Complete example
@@ -2016,10 +2760,61 @@ differences between operating systems, this too is not a preventable default beh
-
-
-
-
+
+
+
+
+ -- none --
+
+
+ Asc
+ Desc
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Clear
@@ -2027,59 +2822,75 @@ differences between operating systems, this too is not a preventable default beh
-
-
-
-
- -- none --
-
-
- Asc Desc
-
-
+
+
+
+ Name
+ Age
+ Active
+
-
-
-
- Asc
- Desc
- Last
-
+
+
+
-
-
-
-
+
+
-
+
{{ row.value.first }} {{ row.value.last }}
-
- {{ row.value ? 'Yes :)' : 'No :(' }}
-
-
-
+
Info modal
@@ -2097,17 +2908,6 @@ differences between operating systems, this too is not a preventable default beh
-
-
-
-
-
-
{{ infoModal.content }}
@@ -2146,17 +2946,27 @@ differences between operating systems, this too is not a preventable default beh
fields: [
{ key: 'name', label: 'Person Full name', sortable: true, sortDirection: 'desc' },
{ key: 'age', label: 'Person age', sortable: true, class: 'text-center' },
- { key: 'isActive', label: 'is Active' },
+ {
+ key: 'isActive',
+ label: 'is Active',
+ formatter: (value, key, item) => {
+ return value ? 'Yes' : 'No'
+ },
+ sortable: true,
+ sortByFormatted: true,
+ filterByFormatted: true
+ },
{ key: 'actions', label: 'Actions' }
],
totalRows: 1,
currentPage: 1,
perPage: 5,
pageOptions: [5, 10, 15],
- sortBy: null,
+ sortBy: '',
sortDesc: false,
sortDirection: 'asc',
filter: null,
+ filterOn: [],
infoModal: {
id: 'info-modal',
title: '',
diff --git a/src/components/table/_table.scss b/src/components/table/_table.scss
index 491b39c764c..8a580bb65c4 100644
--- a/src/components/table/_table.scss
+++ b/src/components/table/_table.scss
@@ -1,6 +1,6 @@
-.table.b-table {
- // --- General styling ---
+// --- General styling ---
+.table.b-table {
// Table fixed header width layout
&.b-table-fixed {
// Fixed width columns
@@ -20,14 +20,169 @@
// Caption positioning
> caption {
caption-side: bottom;
+ }
- &.b-table-caption-top {
+ &.b-table-caption-top {
+ > caption {
caption-side: top !important;
}
}
}
+// --- Table sticky header styling ---
+
+@if $bv-enable-table-sticky {
+ .b-table-sticky-header,
+ .table-responsive,
+ [class*="table-responsive-"] {
+ // Move the table bottom margin to the wrapper
+ margin-bottom: $spacer;
+
+ > .table {
+ // Reset `margin-bottom` to we don't get a space after
+ // the table inside the scroll area
+ margin-bottom: 0;
+ }
+ }
+
+ .b-table-sticky-header {
+ overflow-y: auto;
+ // Annoyingly, when overflow-y is set, browsers convert
+ // 'overflow-x: visible' to 'overflow-x: auto' - so it becomes
+ // responsive in the x axis automatically
+ // Default `max-height` before a scrollbar will show
+ // We don't use `height` as table could be shorter than this value
+ max-height: $b-table-sticky-header-max-height;
+ }
+
+ @supports (position: sticky) {
+ // Positioning of sticky headers
+ .b-table-sticky-header > .table.b-table > thead > tr > th {
+ // Header cells need to be sticky on top
+ position: sticky;
+ top: 0;
+ z-index: 2;
+ }
+
+ // Positioning of sticky columns
+ // Sticky columns only work when table has sticky
+ // headers and/or is responsive
+ .b-table-sticky-header,
+ .table-responsive,
+ [class*="table-responsive-"] {
+ > .table.b-table {
+ > thead,
+ > tbody,
+ > tfoot {
+ > tr > .b-table-sticky-column {
+ position: sticky;
+ left: 0;
+ }
+ }
+
+ > thead {
+ > tr > .b-table-sticky-column {
+ // z-index needs to be higher than sticky columns and
+ // sticky headers for correct layering
+ z-index: 5;
+ }
+ }
+
+ > tbody,
+ > tfoot {
+ > tr > .b-table-sticky-column {
+ // z-index needs to be lower than sticky header that
+ // is also a sticky column
+ z-index: 2;
+ }
+ }
+ }
+ }
+
+ // Default theme color background for table cells that are sticky
+ // Applied only when no variant is applied to the rows, or no head-variant
+ // Needed because Bootstrap v4 does not have table child elements set up
+ // to inherit their background color from parent element by default
+ //
+ // An issue or PR should be made at twbs/bootstrap repo for table
+ // background color inheritance
+ .table.b-table {
+ > thead,
+ > tbody,
+ > tfoot {
+ > tr > .table-b-table-default {
+ // Default cell color
+ color: $table-color;
+ // `$table-bg` is null by default in Bootstrap v4 variables
+ // but could have a value set by the consumer
+ background-color: if($table-bg, $table-bg, $body-bg);
+ }
+ }
+
+ &.table-dark {
+ > thead,
+ > tbody,
+ > tfoot {
+ > tr > .bg-b-table-default {
+ // Default cell color in table dark mode
+ color: $table-dark-color;
+ // Default cell background color in table dark mode
+ background-color: $table-dark-bg;
+ }
+ }
+ }
+
+ // Handle case of zebra striping
+ &.table-striped {
+ // "fake" zebra striping via use of a transparent background image
+ > tbody > tr:nth-of-type(#{$table-striped-order}) > .table-b-table-default {
+ // `$table-accent-bg` (used for striping) default is a very transparent black
+ // We overlay it over the background color to achieve the same color
+ // effect while keeping the background solid.
+ background-image: linear-gradient($table-accent-bg, $table-accent-bg);
+ background-repeat: no-repeat;
+ }
+
+ &.table-dark {
+ > tbody > tr:nth-of-type(#{$table-striped-order}) > .bg-b-table-default {
+ // `$table-dark-accent-bg` (used for striping) default is a very transparent white
+ // We overlay it over the background color to achieve the same color
+ // effect while keeping the background solid.
+ background-image: linear-gradient($table-dark-accent-bg, $table-dark-accent-bg);
+ background-repeat: no-repeat;
+ }
+ }
+ }
+
+ // Handle case of hover
+ &.table-hover {
+ // "fake" hover via use of a transparent background image
+ > tbody > tr:hover > .table-b-table-default {
+ color: $table-hover-color;
+ // `$table-hover-bg` default is a very transparent black
+ // We overlay it over the background color to achieve the same color
+ // effect while keeping the background solid.
+ background-image: linear-gradient($table-hover-bg, $table-hover-bg);
+ background-repeat: no-repeat;
+ }
+
+ &.table-dark {
+ > tbody > tr:hover > .bg-b-table-default {
+ color: $table-dark-hover-color;
+ // `$table-dark-hover-bg` default is a very transparent white
+ // We overlay it over the background color to achieve the same color
+ // effect while keeping the background solid.
+ background-image: linear-gradient($table-dark-hover-bg, $table-dark-hover-bg);
+ background-repeat: no-repeat;
+ }
+ }
+ }
+ }
+ }
+}
+
// --- Header sort styling ---
+
.table.b-table {
> thead,
> tfoot {
@@ -37,9 +192,8 @@
// `&.sorting`
cursor: pointer;
- // Up/down sort=null indicator
+ // Up/down `sort=null` indicator
&::before {
- display: inline-block;
float: right;
margin-left: $b-table-sort-icon-margin-left;
width: $b-table-sort-icon-width;
@@ -69,87 +223,111 @@
}
}
-// --- Stacked tables ---
+// --- Selectable rows ---
.table.b-table {
- &.b-table-stacked {
- @each $breakpoint in map-keys($grid-breakpoints) {
- $next: breakpoint-next($breakpoint, $grid-breakpoints);
- $infix: breakpoint-infix($next, $grid-breakpoints);
-
- {$infix} {
- @include media-breakpoint-down($breakpoint) {
- display: block;
- width: 100%;
-
- // Convert to blocks when stacked
- > caption,
- > tbody,
- > tbody > tr,
- > tbody > tr > td,
- > tbody > tr > td {
+ &.b-table-selectable {
+ & > tbody > tr {
+ cursor: pointer;
+ }
+
+ &.b-table-selecting {
+ // Disabled text-selection when in range mode when
+ // at least one row selected
+ &.b-table-select-range > tbody > tr {
+ user-select: none;
+ }
+ }
+ }
+}
+
+// --- Stacked tables ---
+@if $bv-enable-table-stacked {
+ .table.b-table {
+ &.b-table-stacked {
+ @each $breakpoint in map-keys($grid-breakpoints) {
+ $next: breakpoint-next($breakpoint, $grid-breakpoints);
+ $infix: breakpoint-infix($next, $grid-breakpoints);
+
+ {$infix} {
+ @include media-breakpoint-down($breakpoint) {
display: block;
- }
+ width: 100%;
- // Hide when stacked
- > thead,
- > tfoot {
- display: none;
+ // Convert to blocks when stacked
+ > caption,
+ > tbody,
+ > tbody > tr,
+ > tbody > tr > td,
+ > tbody > tr > th {
+ display: block;
+ }
- > tr.b-table-top-row,
- > tr.b-table-bottom-row {
+ // Hide when stacked
+ > thead,
+ > tfoot {
display: none;
+
+ > tr.b-table-top-row,
+ > tr.b-table-bottom-row {
+ display: none;
+ }
}
- }
- // Caption positioning
- > caption {
- caption-side: top !important;
- }
+ // Caption positioning
+ > caption {
+ caption-side: top !important;
+ }
- > tbody {
- > tr {
- // Turn cells with labels into micro-grids
- > [data-label] {
- // Cell header label pseudo element
- &::before {
- content: attr(data-label);
- display: inline-block;
- width: $b-table-stacked-heading-width;
- float: left;
- text-align: right;
- overflow-wrap: break-word;
- font-weight: bold;
- font-style: normal;
- padding: 0;
- margin: 0;
- }
+ > tbody {
+ > tr {
+ // Turn cells with labels into micro-grids
+ > [data-label] {
+ // Cell header label pseudo element
+ &::before {
+ content: attr(data-label);
+ width: $b-table-stacked-heading-width;
+ float: left;
+ text-align: right;
+ overflow-wrap: break-word;
+ font-weight: bold;
+ font-style: normal;
+ padding: 0 calc(#{$b-table-stacked-gap} / 2) 0 0;
+ margin: 0;
+ }
+
+ // Add clearfix in-case field label wraps
+ &::after {
+ display: block;
+ clear: both;
+ content: "";
+ }
- // Add clearfix in-case field label wraps
- &::after {
- display: block;
- clear: both;
- content: "";
+ // Cell value (we wrap the cell value in a div when stacked)
+ > div {
+ display: inline-block;
+ width: calc(100% - #{$b-table-stacked-heading-width});
+ // Add "gap" between "cells"
+ padding: 0 0 0 calc(#{$b-table-stacked-gap} / 2);
+ margin: 0;
+ }
}
- // Cell value (we wrap the cell value in a div when stacked)
- > div {
- display: inline-block;
- width: calc(100% - #{$b-table-stacked-heading-width});
- // Add "gap" between "cells"
- padding: 0 0 0 $b-table-stacked-gap;
- margin: 0;
+ // Dont show the fixed top/bottom rows
+ &.top-row,
+ &.bottom-row {
+ display: none;
}
- }
- // Dont show the fixed top/bottom rows
- &.top-row,
- &.bottom-row {
- display: none;
- }
+ // Give the top cell of each "row" a heavy border
+ > :first-child {
+ border-top-width: (3 * $table-border-width);
+ }
- // Give the top cell of each "row" a heavy border
- > :first-child {
- border-top-width: (3 * $table-border-width);
+ // Give any cell after a rowspan'ed cell a heavy top border
+ > [rowspan] + td,
+ > [rowspan] + th {
+ border-top-width: (3 * $table-border-width);
+ }
}
}
}
@@ -158,20 +336,3 @@
}
}
}
-
-// --- Selectable rows ---
-.table.b-table {
- &.b-table-selectable {
- & > tbody > tr {
- cursor: pointer;
- }
-
- &.b-table-selecting {
- // Disabled text-selection when in range mode when
- // at least one row selected
- &.b-table-select-range > tbody > tr {
- user-select: none;
- }
- }
- }
-}
diff --git a/src/components/table/helpers/constants.js b/src/components/table/helpers/constants.js
index 42e22501dd1..ac3f7161a73 100644
--- a/src/components/table/helpers/constants.js
+++ b/src/components/table/helpers/constants.js
@@ -1,19 +1,20 @@
// Constants used by table helpers
-// Object of item keys that should be ignored for headers and stringification and filter events
+// Object of item keys that should be ignored for headers and
+// stringification and filter events
export const IGNORED_FIELD_KEYS = {
_rowVariant: true,
_cellVariants: true,
_showDetails: true
}
-// Filter CSS Selector for click/dblclick/etc events
+// Filter CSS selector for click/dblclick/etc. events
// If any of these selectors match the clicked element, we ignore the event
export const EVENT_FILTER = [
'a',
- 'a *', // include content inside links
+ 'a *', // Include content inside links
'button',
- 'button *', // include content inside buttons
+ 'button *', // Include content inside buttons
'input:not(.disabled):not([disabled])',
'select:not(.disabled):not([disabled])',
'textarea:not(.disabled):not([disabled])',
diff --git a/src/components/table/helpers/default-sort-compare.js b/src/components/table/helpers/default-sort-compare.js
index 9f31ccd5e2c..75236d278b9 100644
--- a/src/components/table/helpers/default-sort-compare.js
+++ b/src/components/table/helpers/default-sort-compare.js
@@ -1,32 +1,37 @@
import get from '../../../utils/get'
-import { isDate, isUndefined, isFunction, isNull, isNumber } from '../../../utils/inspect'
+import { isDate, isFunction, isNumber, isUndefinedOrNull } from '../../../utils/inspect'
import stringifyObjectValues from './stringify-object-values'
// Default sort compare routine
//
// TODO: Add option to sort by multiple columns (tri-state per column,
// plus order of columns in sort) where sortBy could be an array
-// of objects [ {key: 'foo', sortDir: 'asc'}, {key:'bar', sortDir: 'desc'} ...]
-// or an array of arrays [ ['foo','asc'], ['bar','desc'] ]
+// of objects `[ {key: 'foo', sortDir: 'asc'}, {key:'bar', sortDir: 'desc'} ...]`
+// or an array of arrays `[ ['foo','asc'], ['bar','desc'] ]`
// Multisort will most likely be handled in mixin-sort.js by
// calling this method for each sortBy
-const defaultSortCompare = (a, b, sortBy, formatter, localeOpts, locale) => {
- let aa = get(a, sortBy, '')
- let bb = get(b, sortBy, '')
+const defaultSortCompare = (a, b, sortBy, sortDesc, formatter, localeOpts, locale, nullLast) => {
+ let aa = get(a, sortBy, null)
+ let bb = get(b, sortBy, null)
if (isFunction(formatter)) {
aa = formatter(aa, sortBy, a)
bb = formatter(bb, sortBy, b)
}
- aa = isUndefined(aa) || isNull(aa) ? '' : aa
- bb = isUndefined(bb) || isNull(bb) ? '' : bb
+ aa = isUndefinedOrNull(aa) ? '' : aa
+ bb = isUndefinedOrNull(bb) ? '' : bb
if ((isDate(aa) && isDate(bb)) || (isNumber(aa) && isNumber(bb))) {
// Special case for comparing dates and numbers
// Internally dates are compared via their epoch number values
return aa < bb ? -1 : aa > bb ? 1 : 0
- } else {
- // Do localized string comparison
- return stringifyObjectValues(aa).localeCompare(stringifyObjectValues(bb), locale, localeOpts)
+ } else if (nullLast && aa === '' && bb !== '') {
+ // Special case when sorting null/undefined/empty string last
+ return 1
+ } else if (nullLast && aa !== '' && bb === '') {
+ // Special case when sorting null/undefined/empty string last
+ return -1
}
+ // Do localized string comparison
+ return stringifyObjectValues(aa).localeCompare(stringifyObjectValues(bb), locale, localeOpts)
}
export default defaultSortCompare
diff --git a/src/components/table/helpers/default-sort-compare.spec.js b/src/components/table/helpers/default-sort-compare.spec.js
index 6a501491d2a..fc57fba5f4b 100644
--- a/src/components/table/helpers/default-sort-compare.spec.js
+++ b/src/components/table/helpers/default-sort-compare.spec.js
@@ -54,6 +54,33 @@ describe('table/helpers/default-sort-compare', () => {
.join('')
}
expect(defaultSortCompare({ a: 'ab' }, { a: 'b' }, 'a')).toBe(-1)
- expect(defaultSortCompare({ a: 'ab' }, { a: 'b' }, 'a', formatter)).toBe(1)
+ expect(defaultSortCompare({ a: 'ab' }, { a: 'b' }, 'a', false, formatter)).toBe(1)
+ })
+
+ it('sorts nulls always last when sor-null-lasst is set', async () => {
+ const x = { a: 'ab' }
+ const y = { a: null }
+ const z = {}
+ const w = { a: '' }
+ const u = undefined
+
+ // Without nullLast set (false)
+ expect(defaultSortCompare(x, y, 'a', u, u, { numeric: true }, u, false)).toBe(1)
+ expect(defaultSortCompare(y, x, 'a', u, u, { numeric: true }, u, false)).toBe(-1)
+ expect(defaultSortCompare(x, z, 'a', u, u, { numeric: true }, u, false)).toBe(1)
+ expect(defaultSortCompare(z, x, 'a', u, u, { numeric: true }, u, false)).toBe(-1)
+ expect(defaultSortCompare(y, z, 'a', u, u, { numeric: true }, u, false)).toBe(0)
+ expect(defaultSortCompare(z, y, 'a', u, u, { numeric: true }, u, false)).toBe(0)
+ expect(defaultSortCompare(x, w, 'a', u, u, { numeric: true }, u, false)).toBe(1)
+ expect(defaultSortCompare(w, x, 'a', u, u, { numeric: true }, u, false)).toBe(-1)
+ // With nullLast set
+ expect(defaultSortCompare(x, y, 'a', u, u, { numeric: true }, u, true)).toBe(-1)
+ expect(defaultSortCompare(y, x, 'a', u, u, { numeric: true }, u, true)).toBe(1)
+ expect(defaultSortCompare(x, z, 'a', u, u, { numeric: true }, u, true)).toBe(-1)
+ expect(defaultSortCompare(z, x, 'a', u, u, { numeric: true }, u, true)).toBe(1)
+ expect(defaultSortCompare(y, z, 'a', u, u, { numeric: true }, u, true)).toBe(0)
+ expect(defaultSortCompare(z, y, 'a', u, u, { numeric: true }, u, true)).toBe(0)
+ expect(defaultSortCompare(x, w, 'a', u, u, { numeric: true }, u, true)).toBe(-1)
+ expect(defaultSortCompare(w, x, 'a', u, u, { numeric: true }, u, true)).toBe(1)
})
})
diff --git a/src/components/table/helpers/mixin-bottom-row.js b/src/components/table/helpers/mixin-bottom-row.js
index e014f4d01da..e02857ace16 100644
--- a/src/components/table/helpers/mixin-bottom-row.js
+++ b/src/components/table/helpers/mixin-bottom-row.js
@@ -1,4 +1,7 @@
import { isFunction } from '../../../utils/inspect'
+import { BTr } from '../tr'
+
+const slotName = 'bottom-row'
export default {
methods: {
@@ -6,26 +9,25 @@ export default {
const h = this.$createElement
// Static bottom row slot (hidden in visibly stacked mode as we can't control the data-label)
- // If in always stacked mode, we don't bother rendering the row
- if (!this.hasNormalizedSlot('bottom-row') || this.isStacked === true) {
+ // If in *always* stacked mode, we don't bother rendering the row
+ if (!this.hasNormalizedSlot(slotName) || this.stacked === true || this.stacked === '') {
return h()
}
const fields = this.computedFields
return h(
- 'tr',
+ BTr,
{
- key: '__b-table-bottom-row__',
+ key: 'b-bottom-row',
staticClass: 'b-table-bottom-row',
class: [
isFunction(this.tbodyTrClass)
? this.tbodyTrClass(null, 'row-bottom')
: this.tbodyTrClass
- ],
- attrs: { role: 'row' }
+ ]
},
- this.normalizeSlot('bottom-row', { columns: fields.length, fields: fields })
+ this.normalizeSlot(slotName, { columns: fields.length, fields: fields })
)
}
}
diff --git a/src/components/table/helpers/mixin-busy.js b/src/components/table/helpers/mixin-busy.js
index 1ec790c1cd4..9ebe709e08f 100644
--- a/src/components/table/helpers/mixin-busy.js
+++ b/src/components/table/helpers/mixin-busy.js
@@ -1,4 +1,8 @@
import { isFunction } from '../../../utils/inspect'
+import { BTr } from '../tr'
+import { BTd } from '../td'
+
+const busySlotName = 'table-busy'
export default {
props: {
@@ -35,37 +39,33 @@ export default {
}
return false
},
- // Renter the busy indicator or return null if not busy
+ // Render the busy indicator or return `null` if not busy
renderBusy() {
const h = this.$createElement
- // Return a busy indicator row, or null if not busy
- if (this.computedBusy && this.hasNormalizedSlot('table-busy')) {
+ // Return a busy indicator row, or `null` if not busy
+ if (this.computedBusy && this.hasNormalizedSlot(busySlotName)) {
// Show the busy slot
- const trAttrs = {
- role: this.isStacked ? 'row' : null
- }
- const tdAttrs = {
- colspan: String(this.computedFields.length),
- role: this.isStacked ? 'cell' : null
- }
return h(
- 'tr',
+ BTr,
{
key: 'table-busy-slot',
staticClass: 'b-table-busy-slot',
class: [
isFunction(this.tbodyTrClass)
- ? this.tbodyTrClass(null, 'table-busy')
+ ? this.tbodyTrClass(null, busySlotName)
: this.tbodyTrClass
- ],
- attrs: trAttrs
+ ]
},
- [h('td', { attrs: tdAttrs }, [this.normalizeSlot('table-busy')])]
+ [
+ h(BTd, { props: { colspan: this.computedFields.length || null } }, [
+ this.normalizeSlot(busySlotName)
+ ])
+ ]
)
} else {
- // We return null here so that we can determine if we need to
- // render the table items rows or not.
+ // We return `null` here so that we can determine if we need to
+ // render the table items rows or not
return null
}
}
diff --git a/src/components/table/helpers/mixin-caption.js b/src/components/table/helpers/mixin-caption.js
index f03bb15e26b..0a242f65a8e 100644
--- a/src/components/table/helpers/mixin-caption.js
+++ b/src/components/table/helpers/mixin-caption.js
@@ -2,26 +2,22 @@ import { htmlOrText } from '../../../utils/html'
export default {
props: {
+ // `caption-top` is part of table-redere mixin (styling)
+ // captionTop: {
+ // type: Boolean,
+ // default: false
+ // },
caption: {
type: String,
default: null
},
captionHtml: {
type: String
- },
- captionTop: {
- type: Boolean,
- default: false
}
},
computed: {
- captionClasses() {
- return {
- 'b-table-caption-top': this.captionTop
- }
- },
captionId() {
- // Even though this.safeId looks like a method, it is a computed prop
+ // Even though `this.safeId` looks like a method, it is a computed prop
// that returns a new function if the underlying ID changes
return this.isStacked ? this.safeId('_caption_') : null
}
@@ -37,7 +33,6 @@ export default {
if ($captionSlot || this.caption || this.captionHtml) {
const data = {
key: 'caption',
- class: this.captionClasses,
attrs: { id: this.captionId }
}
if (!$captionSlot) {
diff --git a/src/components/table/helpers/mixin-empty.js b/src/components/table/helpers/mixin-empty.js
index 0bb3c9fc895..f8c42a8e00e 100644
--- a/src/components/table/helpers/mixin-empty.js
+++ b/src/components/table/helpers/mixin-empty.js
@@ -1,5 +1,7 @@
import { htmlOrText } from '../../../utils/html'
import { isFunction } from '../../../utils/inspect'
+import { BTr } from '../tr'
+import { BTd } from '../td'
export default {
props: {
@@ -50,27 +52,19 @@ export default {
: htmlOrText(this.emptyHtml, this.emptyText)
})
}
+ $empty = h(BTd, { props: { colspan: this.computedFields.length || null } }, [
+ h('div', { attrs: { role: 'alert', 'aria-live': 'polite' } }, [$empty])
+ ])
$empty = h(
- 'td',
+ BTr,
{
- attrs: {
- colspan: String(this.computedFields.length),
- role: 'cell'
- }
- },
- [h('div', { attrs: { role: 'alert', 'aria-live': 'polite' } }, [$empty])]
- )
- $empty = h(
- 'tr',
- {
- key: this.isFiltered ? '_b-table-empty-filtered-row_' : '_b-table-empty-row_',
+ key: this.isFiltered ? 'b-empty-filtered-row' : 'b-empty-row',
staticClass: 'b-table-empty-row',
class: [
isFunction(this.tbodyTrClass)
? this.tbodyTrClass(null, 'row-empty')
: this.tbodyTrClass
- ],
- attrs: { role: 'row' }
+ ]
},
[$empty]
)
diff --git a/src/components/table/helpers/mixin-filtering.js b/src/components/table/helpers/mixin-filtering.js
index e481b0a5d07..5e0ef5c13d6 100644
--- a/src/components/table/helpers/mixin-filtering.js
+++ b/src/components/table/helpers/mixin-filtering.js
@@ -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'
@@ -20,6 +21,14 @@ export default {
filterFunction: {
type: Function,
default: null
+ },
+ filterIgnoredFields: {
+ type: Array
+ // default: undefined
+ },
+ filterIncludedFields: {
+ type: Array
+ // default: undefined
}
},
data() {
@@ -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
},
@@ -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 (
@@ -184,12 +199,12 @@ 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')
}
@@ -197,21 +212,26 @@ export default {
// 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
diff --git a/src/components/table/helpers/mixin-items.js b/src/components/table/helpers/mixin-items.js
index b6ce665b1d3..9e41ccd55e3 100644
--- a/src/components/table/helpers/mixin-items.js
+++ b/src/components/table/helpers/mixin-items.js
@@ -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 []
@@ -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
}, {})
},
@@ -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
}
}
}
diff --git a/src/components/table/helpers/mixin-selectable.js b/src/components/table/helpers/mixin-selectable.js
index 355245ce577..65fa7ccfe85 100644
--- a/src/components/table/helpers/mixin-selectable.js
+++ b/src/components/table/helpers/mixin-selectable.js
@@ -1,6 +1,8 @@
import looseEqual from '../../../utils/loose-equal'
+import range from '../../../utils/range'
import { isArray, arrayIncludes } from '../../../utils/array'
import { getComponentConfig } from '../../../utils/config'
+import { isNumber } from '../../../utils/inspect'
import sanitizeRow from './sanitize-row'
export default {
@@ -11,7 +13,8 @@ export default {
},
selectMode: {
type: String,
- default: 'multi'
+ default: 'multi',
+ validator: val => arrayIncludes(['range', 'multi', 'single'], val)
},
selectedVariant: {
type: String,
@@ -25,36 +28,42 @@ export default {
}
},
computed: {
+ isSelectable() {
+ return this.selectable && this.selectMode
+ },
+ selectableHasSelection() {
+ return (
+ this.isSelectable &&
+ this.selectedRows &&
+ this.selectedRows.length > 0 &&
+ this.selectedRows.some(Boolean)
+ )
+ },
+ selectableIsMultiSelect() {
+ return this.isSelectable && arrayIncludes(['range', 'multi'], this.selectMode)
+ },
selectableTableClasses() {
- const selectable = this.selectable
- const isSelecting = selectable && this.selectedRows && this.selectedRows.some(Boolean)
return {
- 'b-table-selectable': selectable,
- [`b-table-select-${this.selectMode}`]: selectable,
- 'b-table-selecting': isSelecting
+ 'b-table-selectable': this.isSelectable,
+ [`b-table-select-${this.selectMode}`]: this.isSelectable,
+ 'b-table-selecting': this.selectableHasSelection
}
},
selectableTableAttrs() {
return {
- 'aria-multiselectable': this.selectableIsMultiSelect
- }
- },
- selectableIsMultiSelect() {
- if (this.selectable) {
- return arrayIncludes(['range', 'multi'], this.selectMode) ? 'true' : 'false'
- } else {
- return null
+ 'aria-multiselectable': !this.isSelectable
+ ? null
+ : this.selectableIsMultiSelect
+ ? 'true'
+ : 'false'
}
}
},
watch: {
computedItems(newVal, oldVal) {
// Reset for selectable
- // TODO: Should selectedLastClicked be reset here?
- // As changes to _showDetails would trigger it to reset
- this.selectedLastRow = -1
let equal = false
- if (this.selectable && this.selectedRows.length > 0) {
+ if (this.isSelectable && this.selectedRows.length > 0) {
// Quick check against array length
equal = isArray(newVal) && isArray(oldVal) && newVal.length === oldVal.length
for (let i = 0; equal && i < newVal.length; i++) {
@@ -74,9 +83,9 @@ export default {
this.clearSelected()
},
selectedRows(selectedRows, oldVal) {
- if (this.selectable && !looseEqual(selectedRows, oldVal)) {
+ if (this.isSelectable && !looseEqual(selectedRows, oldVal)) {
const items = []
- // forEach skips over non-existant indicies (on sparse arrays)
+ // `.forEach()` skips over non-existent indices (on sparse arrays)
selectedRows.forEach((v, idx) => {
if (v) {
items.push(this.computedItems[idx])
@@ -88,35 +97,67 @@ export default {
},
beforeMount() {
// Set up handlers
- if (this.selectable) {
+ if (this.isSelectable) {
this.setSelectionHandlers(true)
}
},
methods: {
- isRowSelected(idx) {
- return Boolean(this.selectedRows[idx])
+ // Public methods
+ selectRow(index) {
+ // Select a particular row (indexed based on computedItems)
+ if (
+ this.isSelectable &&
+ isNumber(index) &&
+ index >= 0 &&
+ index < this.computedItems.length &&
+ !this.isRowSelected(index)
+ ) {
+ const selectedRows = this.selectableIsMultiSelect ? this.selectedRows.slice() : []
+ selectedRows[index] = true
+ this.selectedLastClicked = -1
+ this.selectedRows = selectedRows
+ }
},
- selectableRowClasses(idx) {
- const rowSelected = this.isRowSelected(idx)
- const base = this.dark ? 'bg' : 'table'
- const variant = this.selectedVariant
- return {
- 'b-table-row-selected': this.selectable && rowSelected,
- [`${base}-${variant}`]: this.selectable && rowSelected && variant
+ unselectRow(index) {
+ // Un-select a particular row (indexed based on `computedItems`)
+ if (this.isSelectable && isNumber(index) && this.isRowSelected(index)) {
+ const selectedRows = this.selectedRows.slice()
+ selectedRows[index] = false
+ this.selectedLastClicked = -1
+ this.selectedRows = selectedRows
}
},
- selectableRowAttrs(idx) {
- return {
- 'aria-selected': !this.selectable ? null : this.isRowSelected(idx) ? 'true' : 'false'
+ selectAllRows() {
+ const length = this.computedItems.length
+ if (this.isSelectable && length > 0) {
+ this.selectedLastClicked = -1
+ this.selectedRows = this.selectableIsMultiSelect ? range(length).map(i => true) : [true]
}
},
+ isRowSelected(index) {
+ // Determine if a row is selected (indexed based on `computedItems`)
+ return Boolean(isNumber(index) && this.selectedRows[index])
+ },
clearSelected() {
- const hasSelection = this.selectedRows.reduce((prev, v) => {
- return prev || v
- }, false)
- if (hasSelection) {
- this.selectedLastClicked = -1
- this.selectedRows = []
+ // Clear any active selected row(s)
+ this.selectedLastClicked = -1
+ this.selectedRows = []
+ },
+ // Internal private methods
+ selectableRowClasses(index) {
+ if (this.isSelectable && this.isRowSelected(index)) {
+ const variant = this.selectedVariant
+ return {
+ 'b-table-row-selected': true,
+ [`${this.dark ? 'bg' : 'table'}-${variant}`]: variant
+ }
+ } else {
+ return {}
+ }
+ },
+ selectableRowAttrs(index) {
+ return {
+ 'aria-selected': !this.isSelectable ? null : this.isRowSelected(index) ? 'true' : 'false'
}
},
setSelectionHandlers(on) {
@@ -129,20 +170,20 @@ export default {
},
selectionHandler(item, index, evt) {
/* istanbul ignore if: should never happen */
- if (!this.selectable) {
+ if (!this.isSelectable) {
// Don't do anything if table is not in selectable mode
/* istanbul ignore next: should never happen */
this.clearSelected()
/* istanbul ignore next: should never happen */
return
}
+ const selectMode = this.selectMode
let selectedRows = this.selectedRows.slice()
let selected = !selectedRows[index]
- const mode = this.selectMode
- // Note 'multi' mode needs no special handling
- if (mode === 'single') {
+ // Note 'multi' mode needs no special event handling
+ if (selectMode === 'single') {
selectedRows = []
- } else if (mode === 'range') {
+ } else if (selectMode === 'range') {
if (this.selectedLastRow > -1 && evt.shiftKey) {
// range
for (
@@ -155,7 +196,7 @@ export default {
selected = true
} else {
if (!(evt.ctrlKey || evt.metaKey)) {
- // clear range selection if any
+ // Clear range selection if any
selectedRows = []
selected = true
}
diff --git a/src/components/table/helpers/mixin-sorting.js b/src/components/table/helpers/mixin-sorting.js
index 18db1be863e..9f577fb83f9 100644
--- a/src/components/table/helpers/mixin-sorting.js
+++ b/src/components/table/helpers/mixin-sorting.js
@@ -1,7 +1,7 @@
import stableSort from '../../../utils/stable-sort'
import startCase from '../../../utils/startcase'
import { arrayIncludes } from '../../../utils/array'
-import { isFunction, isNull, isUndefined } from '../../../utils/inspect'
+import { isFunction, isUndefinedOrNull } from '../../../utils/inspect'
import defaultSortCompare from './default-sort-compare'
export default {
@@ -11,15 +11,14 @@ export default {
default: ''
},
sortDesc: {
- // To Do: Make this tri-state: true, false, null
+ // TODO: Make this tri-state: true, false, null
type: Boolean,
default: false
},
sortDirection: {
- // This prop is named incorrectly.
- // It should be initialSortDirection
- // As it is a bit misleading (not to mention screws up
- // the Aria Label on the headers)
+ // This prop is named incorrectly
+ // It should be `initialSortDirection` as it is a bit misleading
+ // (not to mention it screws up the ARIA label on the headers)
type: String,
default: 'asc',
validator: direction => arrayIncludes(['asc', 'desc', 'last'], direction)
@@ -37,9 +36,16 @@ export default {
}
},
sortCompareLocale: {
- type: String
+ // String: locale code
+ // Array: array of Locale strings
+ type: [String, Array]
// default: undefined
},
+ sortNullLast: {
+ // Sort null and undefined to appear last
+ type: Boolean,
+ default: false
+ },
noSortReset: {
// Another prop that should have had a better name.
// It should be noSortClear (on non-sortable headers).
@@ -92,8 +98,11 @@ export default {
const localSorting = this.localSorting
const sortOptions = { ...this.sortCompareOptions, usage: 'sort' }
const sortLocale = this.sortCompareLocale || undefined
+ const nullLast = this.sortNullLast
if (sortBy && localSorting) {
- const formatter = this.getFieldFormatter(sortBy)
+ const field = this.computedFieldsObj[sortBy]
+ const formatter =
+ field && field.sortByFormatted ? this.getFieldFormatter(sortBy) : undefined
// stableSort returns a new array, and leaves the original array intact
return stableSort(items, (a, b) => {
let result = null
@@ -101,10 +110,19 @@ export default {
// Call user provided sortCompare routine
result = sortCompare(a, b, sortBy, sortDesc, formatter, sortOptions, sortLocale)
}
- if (isUndefined(result) || isNull(result) || result === false) {
+ if (isUndefinedOrNull(result) || result === false) {
// Fallback to built-in defaultSortCompare if sortCompare
// is not defined or returns null/false
- result = defaultSortCompare(a, b, sortBy, formatter, sortOptions, sortLocale)
+ result = defaultSortCompare(
+ a,
+ b,
+ sortBy,
+ sortDesc,
+ formatter,
+ sortOptions,
+ sortLocale,
+ nullLast
+ )
}
// Negate result if sorting in descending order
return (result || 0) * (sortDesc ? -1 : 1)
diff --git a/src/components/table/helpers/mixin-stacked.js b/src/components/table/helpers/mixin-stacked.js
new file mode 100644
index 00000000000..486dc1b762c
--- /dev/null
+++ b/src/components/table/helpers/mixin-stacked.js
@@ -0,0 +1,25 @@
+// Mixin for providing stacked tables
+
+export default {
+ props: {
+ stacked: {
+ type: [Boolean, String],
+ default: false
+ }
+ },
+ computed: {
+ isStacked() {
+ // `true` when always stacked, or breakpoint specified
+ return this.stacked === '' ? true : Boolean(this.stacked)
+ },
+ isStackedAlways() {
+ return this.stacked === true || this.stacked === ''
+ },
+ stackedTableClasses() {
+ return {
+ 'b-table-stacked': this.isStackedAlways,
+ [`b-table-stacked-${this.stacked}`]: !this.isStackedAlways && this.isStacked
+ }
+ }
+ }
+}
diff --git a/src/components/table/helpers/mixin-table-renderer.js b/src/components/table/helpers/mixin-table-renderer.js
index 3da96e7ee9f..c60f0417847 100644
--- a/src/components/table/helpers/mixin-table-renderer.js
+++ b/src/components/table/helpers/mixin-table-renderer.js
@@ -1,9 +1,17 @@
+import { isBoolean } from '../../../utils/inspect'
+
// Main `` render mixin
-// Which indlues all main table stlying options
+// Includes all main table styling options
export default {
- // Don't place ATTRS on root element automatically, as table could be wrapped in responsive div
+ // Don't place attributes on root element automatically,
+ // as table could be wrapped in responsive ``
inheritAttrs: false,
+ provide() {
+ return {
+ bvTable: this
+ }
+ },
props: {
striped: {
type: Boolean,
@@ -41,10 +49,19 @@ export default {
type: [Boolean, String],
default: false
},
- stacked: {
+ stickyHeader: {
+ // If a string, it is assumed to be the table `max-height` value
type: [Boolean, String],
default: false
},
+ captionTop: {
+ type: Boolean,
+ default: false
+ },
+ tableVariant: {
+ type: String,
+ default: null
+ },
tableClass: {
type: [String, Array, Object],
default: null
@@ -52,84 +69,114 @@ export default {
},
computed: {
// Layout related computed props
- isStacked() {
- return this.stacked === '' ? true : this.stacked
- },
isResponsive() {
const responsive = this.responsive === '' ? true : this.responsive
return this.isStacked ? false : responsive
},
- responsiveClass() {
- return this.isResponsive === true
- ? 'table-responsive'
- : this.isResponsive
- ? `table-responsive-${this.responsive}`
- : ''
+ isStickyHeader() {
+ const stickyHeader = this.stickyHeader === '' ? true : this.stickyHeader
+ return this.isStacked ? false : stickyHeader
+ },
+ wrapperClasses() {
+ return [
+ this.isStickyHeader ? 'b-table-sticky-header' : '',
+ this.isResponsive === true
+ ? 'table-responsive'
+ : this.isResponsive
+ ? `table-responsive-${this.responsive}`
+ : ''
+ ].filter(Boolean)
+ },
+ wrapperStyles() {
+ return this.isStickyHeader && !isBoolean(this.isStickyHeader)
+ ? { maxHeight: this.isStickyHeader }
+ : {}
},
tableClasses() {
+ const hover = this.isTableSimple
+ ? this.hover
+ : this.hover && this.computedItems.length > 0 && !this.computedBusy
+
return [
// User supplied classes
this.tableClass,
// Styling classes
{
'table-striped': this.striped,
- 'table-hover': this.hover && this.computedItems.length > 0 && !this.computedBusy,
+ 'table-hover': hover,
'table-dark': this.dark,
'table-bordered': this.bordered,
'table-borderless': this.borderless,
'table-sm': this.small,
- border: this.outlined,
// The following are b-table custom styles
+ border: this.outlined,
'b-table-fixed': this.fixed,
- 'b-table-stacked': this.stacked === true || this.stacked === '',
- [`b-table-stacked-${this.stacked}`]: this.stacked !== true && this.stacked
+ 'b-table-caption-top': this.captionTop
},
+ this.tableVariant ? `${this.dark ? 'bg' : 'table'}-${this.tableVariant}` : '',
+ // Stacked table classes
+ this.stackedTableClasses,
// Selectable classes
this.selectableTableClasses
]
},
tableAttrs() {
- // Preserve user supplied aria-describedby, if provided in $attrs
+ // Preserve user supplied aria-describedby, if provided in `$attrs`
const adb =
[(this.$attrs || {})['aria-describedby'], this.captionId].filter(Boolean).join(' ') || null
const items = this.computedItems
+ const filteredItems = this.filteredItems
const fields = this.computedFields
const selectableAttrs = this.selectableTableAttrs || {}
+ const ariaAttrs = this.isTableSimple
+ ? {}
+ : {
+ 'aria-busy': this.computedBusy ? 'true' : 'false',
+ 'aria-colcount': String(fields.length),
+ 'aria-describedby': adb
+ }
+ const rowCount =
+ items && filteredItems && filteredItems.length > items.length
+ ? String(filteredItems.length)
+ : null
+
return {
- // We set aria-rowcount before merging in $attrs, in case user has supplied their own
- 'aria-rowcount':
- this.filteredItems && this.filteredItems.length > items.length
- ? String(this.filteredItems.length)
- : null,
- // Merge in user supplied $attrs if any
+ // We set `aria-rowcount` before merging in `$attrs`,
+ // in case user has supplied their own
+ 'aria-rowcount': rowCount,
+ // Merge in user supplied `$attrs` if any
...this.$attrs,
- // Now we can override any $attrs here
+ // Now we can override any `$attrs` here
id: this.safeId(),
- role: this.isStacked ? 'table' : null,
- 'aria-busy': this.computedBusy ? 'true' : 'false',
- 'aria-colcount': String(fields.length),
- 'aria-describedby': adb,
+ role: 'table',
+ ...ariaAttrs,
...selectableAttrs
}
}
},
render(h) {
- // Build the caption (from caption mixin)
- const $caption = this.renderCaption ? this.renderCaption() : null
+ const $content = []
- // Build the colgroup
- const $colgroup = this.renderColgroup ? this.renderColgroup() : null
+ if (this.isTableSimple) {
+ $content.push(this.normalizeSlot('default', {}))
+ } else {
+ // Build the `
` (from caption mixin)
+ $content.push(this.renderCaption ? this.renderCaption() : null)
- // Build the thead
- const $thead = this.renderThead()
+ // Build the ` `
+ $content.push(this.renderColgroup ? this.renderColgroup() : null)
- // Build the tfoot
- const $tfoot = this.renderTfoot()
+ // Build the ` `
+ $content.push(this.renderThead ? this.renderThead() : null)
- // Build the tbody
- const $tbody = this.renderTbody()
+ // Build the ` `
+ $content.push(this.renderTbody ? this.renderTbody() : null)
+
+ // Build the ` `
+ $content.push(this.renderTfoot ? this.renderTfoot() : null)
+ }
- // Assemble table
+ // Assemble ``
const $table = h(
'table',
{
@@ -138,12 +185,12 @@ export default {
class: this.tableClasses,
attrs: this.tableAttrs
},
- [$caption, $colgroup, $thead, $tfoot, $tbody].filter(Boolean)
+ $content.filter(Boolean)
)
- // Add responsive wrapper if needed and return table
- return this.isResponsive
- ? h('div', { key: 'b-table-responsive', class: this.responsiveClass }, [$table])
+ // Add responsive/sticky wrapper if needed and return table
+ return this.wrapperClasses.length > 0
+ ? h('div', { key: 'wrap', class: this.wrapperClasses, style: this.wrapperStyles }, [$table])
: $table
}
}
diff --git a/src/components/table/helpers/mixin-tbody-row.js b/src/components/table/helpers/mixin-tbody-row.js
index 333dd365ff1..b8552faf5d8 100644
--- a/src/components/table/helpers/mixin-tbody-row.js
+++ b/src/components/table/helpers/mixin-tbody-row.js
@@ -2,9 +2,14 @@ import KeyCodes from '../../../utils/key-codes'
import get from '../../../utils/get'
import toString from '../../../utils/to-string'
import { arrayIncludes } from '../../../utils/array'
-import { isFunction, isNull, isString, isUndefined } from '../../../utils/inspect'
+import { isFunction, isString, isUndefinedOrNull } from '../../../utils/inspect'
import filterEvent from './filter-event'
import textSelectionActive from './text-selection-active'
+import { BTr } from '../tr'
+import { BTd } from '../td'
+import { BTh } from '../th'
+
+const detailsSlotName = 'row-details'
export default {
props: {
@@ -15,39 +20,6 @@ export default {
},
methods: {
// Methods for computing classes, attributes and styles for table cells
- tdClasses(field, item) {
- let cellVariant = ''
- if (item._cellVariants && item._cellVariants[field.key]) {
- cellVariant = `${this.dark ? 'bg' : 'table'}-${item._cellVariants[field.key]}`
- }
- return [
- field.variant && !cellVariant ? `${this.dark ? 'bg' : 'table'}-${field.variant}` : '',
- cellVariant,
- field.class ? field.class : '',
- this.getTdValues(item, field.key, field.tdClass, '')
- ]
- },
- tdAttrs(field, item, colIndex) {
- const attrs = {
- role: 'cell',
- 'aria-colindex': String(colIndex + 1)
- }
- if (field.isRowHeader) {
- attrs.scope = 'row'
- attrs.role = 'rowheader'
- }
- if (this.isStacked) {
- // Generate the "header cell" label content in stacked mode
- attrs['data-label'] = field.label
- }
- return { ...attrs, ...this.getTdValues(item, field.key, field.tdAttr, {}) }
- },
- rowClasses(item) {
- return [
- item._rowVariant ? `${this.dark ? 'bg' : 'table'}-${item._rowVariant}` : '',
- isFunction(this.tbodyTrClass) ? this.tbodyTrClass(item, 'row') : this.tbodyTrClass
- ]
- },
getTdValues(item, key, tdValue, defValue) {
const parent = this.$parent
if (tdValue) {
@@ -69,36 +41,59 @@ export default {
if (isFunction(formatter)) {
value = formatter(value, key, item)
}
- return isUndefined(value) || isNull(value) ? '' : value
+ return isUndefinedOrNull(value) ? '' : value
+ },
+ // Factory function methods
+ toggleDetailsFactory(hasDetailsSlot, item) {
+ // Returns a function to toggle a row's details slot
+ return () => {
+ if (hasDetailsSlot) {
+ this.$set(item, '_showDetails', !item._showDetails)
+ }
+ }
},
+ rowEvtFactory(handler, item, rowIndex) {
+ // Return a row event handler
+ return evt => {
+ // If table is busy (via provider) then don't propagate
+ if (this.stopIfBusy && this.stopIfBusy(evt)) {
+ return
+ }
+ // Otherwise call the handler
+ handler(evt, item, rowIndex)
+ }
+ },
+ // Row event handlers (will be wrapped by the above rowEvtFactory function)
tbodyRowKeydown(evt, item, rowIndex) {
+ // Keypress handler
const keyCode = evt.keyCode
const target = evt.target
- const trs = this.$refs.itemRows
- if (this.stopIfBusy && this.stopIfBusy(evt)) {
- // If table is busy (via provider) then don't propagate
- return
- } else if (!(target && target.tagName === 'TR' && target === document.activeElement)) {
+ // `this.$refs.itemRow`s is most likely an array of `BTr` components, but it
+ // could be regular `tr` elements, so we map to the `tr` elements just in case
+ const trs = (this.$refs.itemRows || []).map(tr => tr.$el || tr)
+ if (!(target && target.tagName === 'TR' && target === document.activeElement)) {
// Ignore if not the active tr element
return
} else if (target.tabIndex !== 0) {
// Ignore if not focusable
/* istanbul ignore next */
return
- } else if (trs && trs.length === 0) {
+ } else if (trs.length === 0) {
+ // No item rows
/* istanbul ignore next */
return
}
const index = trs.indexOf(target)
if (keyCode === KeyCodes.ENTER || keyCode === KeyCodes.SPACE) {
+ // We also allow enter/space to trigger a click (when row is focused)
evt.stopPropagation()
evt.preventDefault()
- // We also allow enter/space to trigger a click (when row is focused)
// We translate to a row-clicked event
this.rowClicked(evt, item, rowIndex)
} else if (
arrayIncludes([KeyCodes.UP, KeyCodes.DOWN, KeyCodes.HOME, KeyCodes.END], keyCode)
) {
+ // Keyboard navigation of rows
evt.stopPropagation()
evt.preventDefault()
const shift = evt.shiftKey
@@ -117,12 +112,8 @@ export default {
}
}
},
- // Row event handlers
- rowClicked(e, item, index) {
- if (this.stopIfBusy && this.stopIfBusy(e)) {
- // If table is busy (via provider) then don't propagate
- return
- } else if (filterEvent(e)) {
+ rowClicked(evt, item, index) {
+ if (filterEvent(evt)) {
// clicked on a non-disabled control so ignore
return
} else if (textSelectionActive(this.$el)) {
@@ -130,108 +121,96 @@ export default {
/* istanbul ignore next: JSDOM doesn't support getSelection() */
return
}
- this.$emit('row-clicked', item, index, e)
+ this.$emit('row-clicked', item, index, evt)
},
- middleMouseRowClicked(e, item, index) {
- if (this.stopIfBusy && this.stopIfBusy(e)) {
- // If table is busy (via provider) then don't propagate
- return
+ middleMouseRowClicked(evt, item, index) {
+ if (evt.which === 2) {
+ this.$emit('row-middle-clicked', item, index, evt)
}
- this.$emit('row-middle-clicked', item, index, e)
},
- rowDblClicked(e, item, index) {
- if (this.stopIfBusy && this.stopIfBusy(e)) {
- // If table is busy (via provider) then don't propagate
- return
- } else if (filterEvent(e)) {
+ rowDblClicked(evt, item, index) {
+ if (filterEvent(evt)) {
// clicked on a non-disabled control so ignore
/* istanbul ignore next: event filtering already tested via click handler */
return
}
- this.$emit('row-dblclicked', item, index, e)
+ this.$emit('row-dblclicked', item, index, evt)
},
- rowHovered(e, item, index) {
- if (this.stopIfBusy && this.stopIfBusy(e)) {
- // If table is busy (via provider) then don't propagate
- return
- }
- this.$emit('row-hovered', item, index, e)
+ rowHovered(evt, item, index) {
+ this.$emit('row-hovered', item, index, evt)
},
- rowUnhovered(e, item, index) {
- if (this.stopIfBusy && this.stopIfBusy(e)) {
- // If table is busy (via provider) then don't propagate
- return
- }
- this.$emit('row-unhovered', item, index, e)
+ rowUnhovered(evt, item, index) {
+ this.$emit('row-unhovered', item, index, evt)
},
- rowContextmenu(e, item, index) {
- if (this.stopIfBusy && this.stopIfBusy(e)) {
- // If table is busy (via provider) then don't propagate
- return
- }
- this.$emit('row-contextmenu', item, index, e)
+ rowContextmenu(evt, item, index) {
+ this.$emit('row-contextmenu', item, index, evt)
},
// Render helpers
renderTbodyRowCell(field, colIndex, item, rowIndex) {
- const h = this.$createElement
-
// Renders a TD or TH for a row's field
- const $scoped = this.$scopedSlots
- const detailsSlot = $scoped['row-details']
+ const h = this.$createElement
+ const hasDetailsSlot = this.hasNormalizedSlot(detailsSlotName)
const formatted = this.getFormattedValue(item, field)
+ const key = field.key
const data = {
// For the Vue key, we concatenate the column index and
- // field key (as field keys can be duplicated)
- key: `row-${rowIndex}-cell-${colIndex}-${field.key}`,
- class: this.tdClasses(field, item),
- attrs: this.tdAttrs(field, item, colIndex)
- }
- const toggleDetailsFn = () => {
- if (detailsSlot) {
- this.$set(item, '_showDetails', !item._showDetails)
+ // field key (as field keys could be duplicated)
+ // TODO: Although we do prevent duplicate field keys...
+ // So we could change this to: `row-${rowIndex}-cell-${key}`
+ key: `row-${rowIndex}-cell-${colIndex}-${key}`,
+ class: [field.class ? field.class : '', this.getTdValues(item, key, field.tdClass, '')],
+ props: {
+ stackedHeading: this.isStacked ? field.label : null,
+ stickyColumn: field.stickyColumn,
+ variant:
+ item._cellVariants && item._cellVariants[key]
+ ? item._cellVariants[key]
+ : field.variant || null
+ },
+ attrs: {
+ 'aria-colindex': String(colIndex + 1),
+ ...this.getTdValues(item, key, field.tdAttr, {})
}
}
const slotScope = {
item: item,
index: rowIndex,
field: field,
- unformatted: get(item, field.key, ''),
+ unformatted: get(item, key, ''),
value: formatted,
- toggleDetails: toggleDetailsFn,
+ toggleDetails: this.toggleDetailsFactory(hasDetailsSlot, item),
detailsShowing: Boolean(item._showDetails)
}
if (this.selectedRows) {
// Add in rowSelected scope property if selectable rows supported
- slotScope.rowSelected = Boolean(this.selectedRows[rowIndex])
+ slotScope.rowSelected = this.isRowSelected(rowIndex)
}
- let $childNodes = $scoped[field.key] ? $scoped[field.key](slotScope) : toString(formatted)
+ // TODO:
+ // Using `field.key` as scoped slot name is deprecated, to be removed in future release
+ // New format uses the square bracketed naming convention
+ let $childNodes =
+ this.normalizeSlot([`[${key}]`, '[]', key], slotScope) || toString(formatted)
if (this.isStacked) {
// We wrap in a DIV to ensure rendered as a single cell when visually stacked!
$childNodes = [h('div', {}, [$childNodes])]
}
// Render either a td or th cell
- return h(field.isRowHeader ? 'th' : 'td', data, [$childNodes])
+ return h(field.isRowHeader ? BTh : BTd, data, [$childNodes])
},
renderTbodyRow(item, rowIndex) {
// Renders an item's row (or rows if details supported)
const h = this.$createElement
- const $scoped = this.$scopedSlots
const fields = this.computedFields
const tableStriped = this.striped
- const hasRowClickHandler = this.$listeners['row-clicked'] || this.selectable
- const $detailsSlot = $scoped['row-details']
- const rowShowDetails = Boolean(item._showDetails && $detailsSlot)
+ const hasDetailsSlot = this.hasNormalizedSlot(detailsSlotName)
+ const rowShowDetails = Boolean(item._showDetails && hasDetailsSlot)
+ const hasRowClickHandler = this.$listeners['row-clicked'] || this.isSelectable
// We can return more than one TR if rowDetails enabled
const $rows = []
// Details ID needed for aria-describedby when details showing
const detailsId = rowShowDetails ? this.safeId(`_details_${rowIndex}_`) : null
- const toggleDetailsFn = () => {
- if ($detailsSlot) {
- this.$set(item, '_showDetails', !item._showDetails)
- }
- }
// For each item data field in row
const $tds = fields.map((field, colIndex) => {
@@ -249,26 +228,18 @@ export default {
// rows index within the tbody.
// See: https://github.com/bootstrap-vue/bootstrap-vue/issues/2410
const primaryKey = this.primaryKey
- const rowKey =
- primaryKey && !isUndefined(item[primaryKey]) && !isNull(item[primaryKey])
- ? toString(item[primaryKey])
- : String(rowIndex)
+ const hasPkValue = primaryKey && !isUndefinedOrNull(item[primaryKey])
+ const rowKey = hasPkValue ? toString(item[primaryKey]) : String(rowIndex)
// If primary key is provided, use it to generate a unique ID on each tbody > tr
// In the format of '{tableId}__row_{primaryKeyValue}'
- const rowId =
- primaryKey && !isUndefined(item[primaryKey]) && !isNull(item[primaryKey])
- ? this.safeId(`_row_${item[primaryKey]}`)
- : null
+ const rowId = hasPkValue ? this.safeId(`_row_${item[primaryKey]}`) : null
+ const evtFactory = this.rowEvtFactory
const handlers = {}
if (hasRowClickHandler) {
- handlers['click'] = evt => {
- this.rowClicked(evt, item, rowIndex)
- }
- handlers['keydown'] = evt => {
- this.tbodyRowKeydown(evt, item, rowIndex)
- }
+ handlers.click = evtFactory(this.rowClicked, item, rowIndex)
+ handlers.keydown = evtFactory(this.tbodyRowKeydown, item, rowIndex)
}
// Selectable classes and attributes
@@ -278,50 +249,44 @@ export default {
// Add the item row
$rows.push(
h(
- 'tr',
+ BTr,
{
key: `__b-table-row-${rowKey}__`,
ref: 'itemRows',
refInFor: true,
class: [
- this.rowClasses(item),
+ isFunction(this.tbodyTrClass) ? this.tbodyTrClass(item, 'row') : this.tbodyTrClass,
selectableClasses,
- {
- 'b-table-has-details': rowShowDetails
- }
+ rowShowDetails ? 'b-table-has-details' : ''
],
+ props: { variant: item._rowVariant || null },
attrs: {
id: rowId,
tabindex: hasRowClickHandler ? '0' : null,
'data-pk': rowId ? String(item[primaryKey]) : null,
+ // Should this be `aria-details` instead?
'aria-describedby': detailsId,
'aria-owns': detailsId,
'aria-rowindex': ariaRowIndex,
- role: 'row',
...selectableAttrs
},
on: {
...handlers,
- // TODO: Instantiate the following handlers only if we have registered
- // listeners i.e. this.$listeners['row-middle-clicked'], etc.
- auxclick: evt => {
- if (evt.which === 2) {
- this.middleMouseRowClicked(evt, item, rowIndex)
- }
- },
- contextmenu: evt => {
- this.rowContextmenu(evt, item, rowIndex)
- },
- // Note: these events are not accessibility friendly!
- dblclick: evt => {
- this.rowDblClicked(evt, item, rowIndex)
- },
- mouseenter: evt => {
- this.rowHovered(evt, item, rowIndex)
- },
- mouseleave: evt => {
- this.rowUnhovered(evt, item, rowIndex)
- }
+ // TODO:
+ // Instantiate the following handlers only if we have registered
+ // listeners i.e. `this.$listeners['row-middle-clicked']`, etc.
+ //
+ // Could make all of this (including the above click/key handlers)
+ // the result of a factory function and/or make it a delegated event
+ // handler on the tbody (if we store the row index as a data-attribute
+ // on the TR as we can lookup the item data from the computedItems array
+ // or it could be a hidden prop (via attrs) on BTr instance)
+ auxclick: evtFactory(this.middleMouseRowClicked, item, rowIndex),
+ contextmenu: evtFactory(this.rowContextmenu, item, rowIndex),
+ // Note: These events are not accessibility friendly!
+ dblclick: evtFactory(this.rowDblClicked, item, rowIndex),
+ mouseenter: evtFactory(this.rowHovered, item, rowIndex),
+ mouseleave: evtFactory(this.rowUnhovered, item, rowIndex)
}
},
$tds
@@ -330,27 +295,22 @@ export default {
// Row Details slot
if (rowShowDetails) {
- const tdAttrs = {
- colspan: String(fields.length),
- role: 'cell'
- }
- const trAttrs = {
- id: detailsId,
- role: 'row'
+ const detailsScope = {
+ item: item,
+ index: rowIndex,
+ fields: fields,
+ toggleDetails: this.toggleDetailsFactory(hasDetailsSlot, item)
}
- // Render the details slot
- const $details = h('td', { attrs: tdAttrs }, [
- $detailsSlot({
- item: item,
- index: rowIndex,
- fields: fields,
- toggleDetails: toggleDetailsFn
- })
+
+ // Render the details slot in a TD
+ const $details = h(BTd, { props: { colspan: fields.length }, attrs: { id: detailsId } }, [
+ this.normalizeSlot(detailsSlotName, detailsScope)
])
// Add a hidden row to keep table row striping consistent when details showing
if (tableStriped) {
$rows.push(
+ // We don't use `BTr` here as we dont need the extra functionality
h('tr', {
key: `__b-table-details-${rowIndex}-stripe__`,
staticClass: 'd-none',
@@ -362,25 +322,26 @@ export default {
// Add the actual details row
$rows.push(
h(
- 'tr',
+ BTr,
{
key: `__b-table-details-${rowIndex}__`,
staticClass: 'b-table-details',
class: [
isFunction(this.tbodyTrClass)
- ? this.tbodyTrClass(item, 'row-details')
+ ? this.tbodyTrClass(item, detailsSlotName)
: this.tbodyTrClass
],
- attrs: trAttrs
+ props: { variant: item._rowVariant || null },
+ attrs: { id: detailsId }
},
[$details]
)
)
- } else if ($detailsSlot) {
+ } else if (hasDetailsSlot) {
// Only add the placeholder if a the table has a row-details slot defined (but not shown)
$rows.push(h())
if (tableStriped) {
- // add extra placeholder if table is striped
+ // Add extra placeholder if table is striped
$rows.push(h())
}
}
diff --git a/src/components/table/helpers/mixin-tbody.js b/src/components/table/helpers/mixin-tbody.js
index b6887121487..fe304ed8a49 100644
--- a/src/components/table/helpers/mixin-tbody.js
+++ b/src/components/table/helpers/mixin-tbody.js
@@ -1,26 +1,23 @@
+import { props as tbodyProps, BTbody } from '../tbody'
import tbodyRowMixin from './mixin-tbody-row'
+const props = {
+ tbodyClass: {
+ type: [String, Array, Object]
+ // default: undefined
+ },
+ ...tbodyProps
+}
+
export default {
mixins: [tbodyRowMixin],
- props: {
- tbodyClass: {
- type: [String, Array],
- default: null
- },
- tbodyTransitionProps: {
- type: Object
- // default: undefined
- },
- tbodyTransitionHandlers: {
- type: Object
- // default: undefined
- }
- },
+ props,
methods: {
renderTbody() {
// Render the tbody element and children
- const h = this.$createElement
const items = this.computedItems
+ // Shortcut to `createElement` (could use `this._c()` instead)
+ const h = this.$createElement
// Prepare the tbody rows
const $rows = []
@@ -49,26 +46,15 @@ export default {
$rows.push(this.renderBottomRow ? this.renderBottomRow() : h())
}
- // If tbody transition enabled
- const isTransGroup = this.tbodyTransitionProps || this.tbodyTransitionHandlers
- let tbodyProps = {}
- let tbodyOn = {}
- if (isTransGroup) {
- tbodyOn = this.tbodyTransitionHandlers || {}
- tbodyProps = {
- ...(this.tbodyTransitionProps || {}),
- tag: 'tbody'
- }
- }
-
// Assemble rows into the tbody
const $tbody = h(
- isTransGroup ? 'transition-group' : 'tbody',
+ BTbody,
{
- props: tbodyProps,
- on: tbodyOn,
- class: [this.tbodyClass],
- attrs: { role: 'rowgroup' }
+ class: this.tbodyClass || null,
+ props: {
+ tbodyTransitionProps: this.tbodyTransitionProps,
+ tbodyTransitionHandlers: this.tbodyTransitionHandlers
+ }
},
$rows
)
diff --git a/src/components/table/helpers/mixin-tfoot.js b/src/components/table/helpers/mixin-tfoot.js
index 97c86bec632..bb2ef94c097 100644
--- a/src/components/table/helpers/mixin-tfoot.js
+++ b/src/components/table/helpers/mixin-tfoot.js
@@ -19,12 +19,6 @@ export default {
default: null
}
},
- computed: {
- footClasses() {
- const variant = this.footVariant || this.headVariant || null
- return [variant ? `thead-${variant}` : '', this.tfootClass]
- }
- },
methods: {
renderTfoot() {
const h = this.$createElement
diff --git a/src/components/table/helpers/mixin-thead.js b/src/components/table/helpers/mixin-thead.js
index 50ac7c1556f..dd7c859f30f 100644
--- a/src/components/table/helpers/mixin-thead.js
+++ b/src/components/table/helpers/mixin-thead.js
@@ -4,42 +4,37 @@ import { getComponentConfig } from '../../../utils/config'
import { htmlOrText } from '../../../utils/html'
import filterEvent from './filter-event'
import textSelectionActive from './text-selection-active'
+import { BThead } from '../thead'
+import { BTfoot } from '../tfoot'
+import { BTr } from '../tr'
+import { BTh } from '../th'
export default {
props: {
headVariant: {
- type: String,
+ type: String, // 'light', 'dark' or null (or custom)
default: () => getComponentConfig('BTable', 'headVariant')
},
theadClass: {
- type: [String, Array, Object],
- default: null
+ type: [String, Array, Object]
+ // default: undefined
},
theadTrClass: {
- type: [String, Array, Object],
- default: null
- }
- },
- computed: {
- headClasses() {
- return [this.headVariant ? 'thead-' + this.headVariant : '', this.theadClass]
+ type: [String, Array, Object]
+ // default: undefined
}
},
methods: {
fieldClasses(field) {
- // header field (th) classes
- return [
- field.variant ? 'table-' + field.variant : '',
- field.class ? field.class : '',
- field.thClass ? field.thClass : ''
- ]
+ // Header field () classes
+ return [field.class ? field.class : '', field.thClass ? field.thClass : '']
},
headClicked(evt, field, isFoot) {
if (this.stopIfBusy && this.stopIfBusy(evt)) {
// If table is busy (via provider) then don't propagate
return
} else if (filterEvent(evt)) {
- // clicked on a non-disabled control so ignore
+ // Clicked on a non-disabled control so ignore
return
} else if (textSelectionActive(this.$el)) {
// User is selecting text, so ignore
@@ -60,7 +55,8 @@ export default {
return h()
}
- // Helper function to generate a field TH cell
+ // Helper function to generate a field cell
+ // TODO: This should be moved into it's own mixin
const makeCell = (field, colIndex) => {
let ariaLabel = null
if (!field.label.trim() && !field.headerTitle) {
@@ -87,53 +83,71 @@ export default {
const data = {
key: field.key,
class: [this.fieldClasses(field), sortClass],
+ props: {
+ variant: field.variant,
+ stickyColumn: field.stickyColumn
+ },
style: field.thStyle || {},
attrs: {
// We only add a tabindex of 0 if there is a head-clicked listener
tabindex: hasHeadClickListener ? '0' : null,
abbr: field.headerAbbr || null,
title: field.headerTitle || null,
- role: 'columnheader',
- scope: 'col',
'aria-colindex': String(colIndex + 1),
'aria-label': ariaLabel,
...sortAttrs
},
on: handlers
}
- const fieldScope = { label: field.label, column: field.key, field: field }
- const slot =
- isFoot && this.hasNormalizedSlot(`FOOT_${field.key}`)
- ? this.normalizeSlot(`FOOT_${field.key}`, fieldScope)
- : this.normalizeSlot(`HEAD_${field.key}`, fieldScope)
+ const fieldScope = { label: field.label, column: field.key, field, isFoot }
+ let slot
+ if (
+ isFoot &&
+ this.hasNormalizedSlot([`FOOT[${field.key}]`, 'FOOT[]', `FOOT_${field.key}`])
+ ) {
+ // TODO: `FOOT_${field.key}` is deprecated, to be removed in future release
+ slot = this.normalizeSlot(
+ [`FOOT[${field.key}]`, 'FOOT[]', `FOOT_${field.key}`],
+ fieldScope
+ )
+ } else {
+ // TODO: `HEAD_${field.key}` is deprecated, to be removed in future release
+ slot = this.normalizeSlot(
+ [`HEAD[${field.key}]`, 'HEAD[]', `HEAD_${field.key}`],
+ fieldScope
+ )
+ }
if (!slot) {
+ // need to check if this will work
data.domProps = htmlOrText(field.labelHtml)
}
- return h('th', data, slot || field.label)
+ return h(BTh, data, slot || field.label)
}
- // Generate the array of TH cells
+ // Generate the array of cells
const $cells = fields.map(makeCell).filter(th => th)
// Genrate the row(s)
const $trs = []
if (isFoot) {
- $trs.push(h('tr', { class: this.tfootTrClass, attrs: { role: 'row' } }, $cells))
+ $trs.push(h(BTr, { class: this.tfootTrClass }, $cells))
} else {
const scope = {
columns: fields.length,
fields: fields
}
$trs.push(this.normalizeSlot('thead-top', scope) || h())
- $trs.push(h('tr', { class: this.theadTrClass, attrs: { role: 'row' } }, $cells))
+ $trs.push(h(BTr, { class: this.theadTrClass }, $cells))
}
return h(
- isFoot ? 'tfoot' : 'thead',
+ isFoot ? BTfoot : BThead,
{
- key: isFoot ? 'tfoot' : 'thead',
- class: isFoot ? this.footClasses : this.headClasses,
- attrs: { role: 'rowgroup' }
+ key: isFoot ? 'bv-tfoot' : 'bv-thead',
+ class: (isFoot ? this.tfootClass : this.theadClass) || null,
+ props: isFoot
+ ? { footVariant: this.footVariant || this.headVariant || null }
+ : { headVariant: this.headVariant || null }
},
$trs
)
diff --git a/src/components/table/helpers/mixin-top-row.js b/src/components/table/helpers/mixin-top-row.js
index 9548dd30aec..ab226c9b615 100644
--- a/src/components/table/helpers/mixin-top-row.js
+++ b/src/components/table/helpers/mixin-top-row.js
@@ -1,4 +1,7 @@
import { isFunction } from '../../../utils/inspect'
+import { BTr } from '../tr'
+
+const slotName = 'top-row'
export default {
methods: {
@@ -6,24 +9,23 @@ export default {
const h = this.$createElement
// Add static Top Row slot (hidden in visibly stacked mode as we can't control the data-label)
- // If in always stacked mode, we don't bother rendering the row
- if (!this.hasNormalizedSlot('top-row') || this.isStacked === true) {
+ // If in *always* stacked mode, we don't bother rendering the row
+ if (!this.hasNormalizedSlot(slotName) || this.stacked === true || this.stacked === '') {
return h()
}
const fields = this.computedFields
return h(
- 'tr',
+ BTr,
{
- key: 'top-row',
+ key: 'b-top-row',
staticClass: 'b-table-top-row',
class: [
isFunction(this.tbodyTrClass) ? this.tbodyTrClass(null, 'row-top') : this.tbodyTrClass
- ],
- attrs: { role: 'row' }
+ ]
},
- [this.normalizeSlot('top-row', { columns: fields.length, fields: fields })]
+ [this.normalizeSlot(slotName, { columns: fields.length, fields: fields })]
)
}
}
diff --git a/src/components/table/helpers/sanitize-row.js b/src/components/table/helpers/sanitize-row.js
index 7b12d4a81f3..ea1828dff55 100644
--- a/src/components/table/helpers/sanitize-row.js
+++ b/src/components/table/helpers/sanitize-row.js
@@ -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
}, {})
diff --git a/src/components/table/helpers/stringify-record-values.js b/src/components/table/helpers/stringify-record-values.js
index dc481ca5583..39a736e13c1 100644
--- a/src/components/table/helpers/stringify-record-values.js
+++ b/src/components/table/helpers/stringify-record-values.js
@@ -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
diff --git a/src/components/table/helpers/table-cell.js b/src/components/table/helpers/table-cell.js
new file mode 100644
index 00000000000..de2b3eadacd
--- /dev/null
+++ b/src/components/table/helpers/table-cell.js
@@ -0,0 +1,185 @@
+import Vue from '../../../utils/vue'
+import toString from '../../../utils/to-string'
+import { isUndefinedOrNull } from '../../../utils/inspect'
+import normalizeSlotMixin from '../../../mixins/normalize-slot'
+
+const digitsRx = /^\d+$/
+
+// Parse a rowspan or colspan into a digit (or null if < 1 or NaN)
+const parseSpan = val => {
+ val = parseInt(val, 10)
+ return digitsRx.test(String(val)) && val > 0 ? val : null
+}
+
+/* istanbul ignore next */
+const spanValidator = val => isUndefinedOrNull(val) || parseSpan(val) > 0
+
+export const props = {
+ header: {
+ type: Boolean,
+ default: false
+ },
+ variant: {
+ type: String,
+ default: null
+ },
+ colspan: {
+ type: [Number, String],
+ default: null,
+ validator: spanValidator
+ },
+ rowspan: {
+ type: [Number, String],
+ default: null,
+ validator: spanValidator
+ },
+ stackedHeading: {
+ type: String,
+ default: null
+ },
+ stickyColumn: {
+ type: Boolean,
+ default: false
+ }
+}
+
+// @vue/component
+export const BTableCell = /*#__PURE__*/ Vue.extend({
+ name: 'BTableCell',
+ mixins: [normalizeSlotMixin],
+ inheritAttrs: false,
+ inject: {
+ // Injections for feature / attribute detection
+ bvTable: {
+ default: null
+ },
+ bvTableTbody: {
+ default: null
+ },
+ bvTableThead: {
+ default: null
+ },
+ bvTableTfoot: {
+ default: null
+ },
+ bvTableTr: {
+ default: null
+ }
+ },
+ props,
+ computed: {
+ isDark() {
+ return this.bvTable && this.bvTable.dark
+ },
+ isStacked() {
+ return this.bvTable && this.bvTable.isStacked
+ },
+ isStackedCell() {
+ // We only support stacked-heading in tbody in stacked mode
+ return this.isStacked && this.bvTableTbody
+ },
+ isResponsive() {
+ return this.bvTable && this.bvTable.isResponsive && !this.isStacked
+ },
+ isStickyHeader() {
+ // Needed to handle header background classes, due to lack of
+ // bg color inheritance with Bootstrap v4 tabl css
+ // Sticky headers only apply to cells in table `thead`
+ return (
+ !this.isStacked &&
+ this.bvTable &&
+ this.bvTableThead &&
+ this.bvTableTr &&
+ this.bvTable.stickyHeader
+ )
+ },
+ isStickyColumn() {
+ // Needed to handle header background classes, due to lack of
+ // background color inheritance with Bootstrap v4 table css.
+ // Sticky column cells are only available in responsive
+ // mode (horzontal scrolling) or when sticky header mode.
+ // Applies to cells in `thead`, `tbody` and `tfoot`
+ return (
+ (this.isResponsive || this.isStickyHeader) &&
+ this.stickyColumn &&
+ !this.isStacked &&
+ this.bvTable &&
+ this.bvTableTr
+ )
+ },
+ cellClasses() {
+ // We use computed props here for improved performance by caching
+ // the results of the string interpolation
+ let variant = this.variant
+ if (
+ (!variant && this.isStickyHeader && !this.bvTableThead.headVariant) ||
+ (!variant && this.isStickyColumn)
+ ) {
+ // Needed for stickyheader mode as Bootstrap v4 table cells do
+ // not inherit parent's background-color. Boo!
+ variant = this.bvTableTr.variant || this.bvTable.tableVariant || 'b-table-default'
+ }
+ return [
+ variant ? `${this.isDark ? 'bg' : 'table'}-${variant}` : null,
+ this.isStickyColumn ? 'b-table-sticky-column' : null
+ ]
+ },
+ computedColspan() {
+ return parseSpan(this.colspan)
+ },
+ computedRowspan() {
+ return parseSpan(this.rowspan)
+ },
+ cellAttrs() {
+ // We use computed props here for improved performance by caching
+ // the results of the object spread (Object.assign)
+ const headOrFoot = this.bvTableThead || this.bvTableTfoot
+ // Make sure col/rowspan's are > 0 or null
+ const colspan = this.computedColspan
+ const rowspan = this.computedRowspan
+ // Default role and scope
+ let role = 'cell'
+ let scope = null
+
+ // Compute role and scope
+ // We only add scopes with an explicit span of 1 or greater
+ if (headOrFoot) {
+ // Header or footer cells
+ role = 'columnheader'
+ scope = colspan > 0 ? 'colspan' : 'col'
+ } else if (this.header) {
+ // th's in tbody
+ role = 'rowheader'
+ scope = rowspan > 0 ? 'rowgroup' : 'row'
+ }
+
+ return {
+ colspan: colspan,
+ rowspan: rowspan,
+ role: role,
+ scope: scope,
+ // Allow users to override role/scope plus add other attributes
+ ...this.$attrs,
+ // Add in the stacked cell label data-attribute if in
+ // stacked mode (if a stacked heading label is provided)
+ 'data-label':
+ this.isStackedCell && !isUndefinedOrNull(this.stackedHeading)
+ ? toString(this.stackedHeading)
+ : null
+ }
+ }
+ },
+ render(h) {
+ const content = [this.normalizeSlot('default')]
+ return h(
+ this.header ? 'th' : 'td',
+ {
+ class: this.cellClasses,
+ attrs: this.cellAttrs,
+ // Transfer any native listeners
+ on: this.$listeners
+ },
+ [this.isStackedCell ? h('div', {}, [content]) : content]
+ )
+ }
+})
diff --git a/src/components/table/helpers/text-selection-active.js b/src/components/table/helpers/text-selection-active.js
index eff5edb3e62..219db24ab85 100644
--- a/src/components/table/helpers/text-selection-active.js
+++ b/src/components/table/helpers/text-selection-active.js
@@ -7,7 +7,7 @@ import { getSel, isElement } from '../../../utils/dom'
// contained within the element
const textSelectionActive = (el = document) => {
const sel = getSel()
- return sel && sel.toString() !== '' && sel.containsNode && isElement(el)
+ return sel && sel.toString().trim() !== '' && sel.containsNode && isElement(el)
? sel.containsNode(el, true)
: false
}
diff --git a/src/components/table/index.d.ts b/src/components/table/index.d.ts
index 8f850dc654c..353d7412608 100644
--- a/src/components/table/index.d.ts
+++ b/src/components/table/index.d.ts
@@ -4,8 +4,10 @@
import Vue, { VNode } from 'vue'
import { BvPlugin, BvComponent } from '../../'
-// Modal Plugin
+// Table Plugins
export declare const TablePlugin: BvPlugin
+export declare const TableLitePlugin: BvPlugin
+export declare const TableSimplePlugin: BvPlugin
export default TablePlugin
// Component: b-table
@@ -13,6 +15,10 @@ export declare class BTable extends BvComponent {
// Public methods
refresh: () => void
clearSelected: () => void
+ selectAllRows: () => void
+ isRowSelected: (index: number) => boolean
+ selectRow: (index: number) => void
+ unselectRow: (index: number) => void
// Props
id?: string
items: Array | BvTableProviderCallback
@@ -22,10 +28,14 @@ export declare class BTable extends BvComponent {
sortDesc?: boolean
sortDirection?: BvTableSortDirection
sortCompare?: BvTableSortCompareCallback
+ sortCompareLocale?: string | Array
+ sortCompareOptions?: BvTableLocaleCompareOptions
perPage?: number | string
currentPage?: number | string
filter?: string | Array | RegExp | object | any
filterFunction?: BvTableFilterCallback
+ filterIgnoredFields?: Array
+ filterIncludedFields?: Array
busy?: boolean
tbodyTrClass?: string | Array | object | BvTableTbodyTrClassCallback
}
@@ -40,6 +50,30 @@ export declare class BTableLite extends BvComponent {
tbodyTrClass?: string | Array | object | BvTableTbodyTrClassCallback
}
+// Component: b-table-simple
+export declare class BTableSimple extends BvComponent {
+ // Props
+ id?: string
+}
+
+// Component: b-tbody
+export declare class BTbody extends BvComponent {}
+
+// Component: b-thead
+export declare class BThead extends BvComponent {}
+
+// Component: b-tfoot
+export declare class BTfoot extends BvComponent {}
+
+// Component: b-tr
+export declare class BTr extends BvComponent {}
+
+// Component: b-th
+export declare class BTh extends BvComponent {}
+
+// Component: b-td
+export declare class BTd extends BvComponent {}
+
export type BvTableVariant =
| 'active'
| 'success'
@@ -59,7 +93,32 @@ export type BvTableTbodyTrClassCallback = ((item: any, type: string) => any)
export type BvTableFilterCallback = ((item: any, filter: any) => boolean)
-export type BvTableSortCompareCallback = ((a: any, b: any, field: string) => any)
+export type BvTableLocaleCompareOptionLocaleMatcher = 'lookup' | 'best fit'
+
+export type BvTableLocaleCompareOptionSensitivity = 'base' | 'accent' | 'case' | 'variant'
+
+export type BvTableLocaleCompareOptionCaseFirst = 'upper' | 'lower' | 'false'
+
+export type BvTableLocaleCompareOptionUsage = 'sort'
+
+export interface BvTableLocaleCompareOptions {
+ ignorePunctuation?: boolean
+ numeric?: boolean
+ localeMatcher?: BvTableLocaleCompareOptionLocaleMatcher
+ sensitivity?: BvTableLocaleCompareOptionSensitivity
+ caseFirst?: BvTableLocaleCompareOptionCaseFirst
+ usage?: BvTableLocaleCompareOptionUsage
+}
+
+export type BvTableSortCompareCallback = ((
+ a: any,
+ b: any,
+ field: string,
+ sortDesc?: boolean,
+ formatter?: BvTableFormatterCallback | undefined | null,
+ localeOptions?: BvTableLocaleCompareOptions,
+ locale?: string | Array | undefined | null
+) => number | boolean | null | undefined)
export interface BvTableCtxObject {
currentPage: number
@@ -71,8 +130,10 @@ export interface BvTableCtxObject {
[key: string]: any
}
+export type BvTableProviderPromiseResult = Array | null
+
export interface BvTableProviderCallback {
- (ctx: BvTableCtxObject): any
+ (ctx: BvTableCtxObject): Array