` element with
+class `b-table-busy-slot`, which has one single `` with a `colspan` set to the number of fields.
+
+**Example of `table-busy` slot usage:**
+```html
+
+
+ Toggle Busy State
+
+
+ Loading...
+
+
+
+
+
+
+
+```
+
+Also see the [**Using Items Provider Functions**](#using-items-provider-functions) below for additional
+informaton on the `busy` state.
+
+**Note:** All click related and hover events, and sort-changed events will __not__ be
+ emitted when the table is in the `busy` state.
+
+
## Custom Data Rendering
Custom rendering for each data field in a row is possible using either
[scoped slots](http://vuejs.org/v2/guide/components.html#Scoped-Slots)
@@ -1226,22 +1293,23 @@ function myProvider (ctx) {
}
```
-`` automatically tracks/controls it's `busy` state, however it provides
-a `busy` prop that can be used either to override inner `busy`state, or to monitor
-``'s current busy state in your application using the 2-way `.sync` modifier.
+### Automated table busy state
+`` automatically tracks/controls it's `busy` state when items provider functions are
+used, however it also provides a `busy` prop that can be used either to override the inner `busy`
+state, or to monitor ``'s current busy state in your application using the 2-way `.sync` modifier.
-**Note:** in order to allow `` fully track it's `busy` state, custom items
+**Note:** in order to allow `` fully track it's `busy` state, the custom items
provider function should handle errors from data sources and return an empty
array to ``.
-`` provides a `busy` prop that will flag the table as busy, which you can
-set to `true` just before your async fetch, and then set it to `false` once you have
-your data, and just before you send it to the table for display. Example:
-
+**Example: usage of busy state**
```html
-
+
+
```
-
```js
data () {
return {
@@ -1250,8 +1318,8 @@ data () {
}
methods: {
myProvider (ctx) {
- // Here we don't set isBusy prop, so busy state will be handled by table itself
- // this.isBusy = true
+ // Here we don't set isBusy prop, so busy state will be handled by table itself
+ // this.isBusy = true
let promise = axios.get('/some/url')
return promise.then((data) => {
@@ -1276,6 +1344,7 @@ __not__ be called/refreshed until the `busy` state has been set to `false`.
emitted when in the `busy` state (either set automatically during provider update,
or when manually set).
+
### Provider Paging, Filtering, and Sorting
By default, the items provider function is responsible for **all paging, filtering, and sorting**
of the data, before passing it to `b-table` for display.
diff --git a/src/components/table/package.json b/src/components/table/package.json
index a54af6cef23..c684895dc83 100755
--- a/src/components/table/package.json
+++ b/src/components/table/package.json
@@ -157,6 +157,10 @@
"name": "table-colgroup",
"description": "Slot to place custom colgroup and col elements"
},
+ {
+ "name": "table-busy",
+ "description": "Optional slot to place loading message when table is in the busy state"
+ },
{
"name": "[field]",
"description": "Scoped slot for custom data rendering of field data. See docs for scoped data"
diff --git a/src/components/table/table-busy.spec.js b/src/components/table/table-busy.spec.js
new file mode 100644
index 00000000000..53bf984c1d8
--- /dev/null
+++ b/src/components/table/table-busy.spec.js
@@ -0,0 +1,117 @@
+import Table from './table'
+import { mount } from '@vue/test-utils'
+
+const testItems = [
+ { a: 1, b: 2, c: 3 },
+ { a: 5, b: 5, c: 6 },
+ { a: 7, b: 8, c: 9 }
+]
+
+describe('b-table busy state', async () => {
+ it('default should have attribute aria-busy=false', async () => {
+ const wrapper = mount(Table, {
+ propsData: {
+ items: testItems
+ }
+ })
+ expect(wrapper.attributes('aria-busy')).toBeDefined()
+ expect(wrapper.attributes('aria-busy')).toEqual('false')
+ })
+
+ it('default should have item rows rendered', async () => {
+ const wrapper = mount(Table, {
+ propsData: {
+ items: testItems
+ }
+ })
+ expect(wrapper.find('tbody').exists()).toBe(true)
+ expect(wrapper.find('tbody').findAll('tr').exists()).toBe(true)
+ expect(wrapper.find('tbody').findAll('tr').length).toBe(testItems.length)
+ })
+
+ it('should have attribute aria-busy=true when prop busy=true', async () => {
+ const wrapper = mount(Table, {
+ propsData: {
+ busy: true,
+ items: testItems
+ }
+ })
+ expect(wrapper.attributes('aria-busy')).toBeDefined()
+ expect(wrapper.attributes('aria-busy')).toEqual('true')
+ })
+
+ it('should have attribute aria-busy=true when data localBusy=true', async () => {
+ const wrapper = mount(Table, {
+ propsData: {
+ items: testItems
+ }
+ })
+ expect(wrapper.attributes('aria-busy')).toBeDefined()
+ expect(wrapper.attributes('aria-busy')).toEqual('false')
+
+ wrapper.setData({
+ localBusy: true
+ })
+
+ expect(wrapper.attributes('aria-busy')).toBeDefined()
+ expect(wrapper.attributes('aria-busy')).toEqual('true')
+ })
+
+ it('should emit update:busy event when data localBusy is toggled', async () => {
+ const wrapper = mount(Table, {
+ propsData: {
+ items: testItems
+ }
+ })
+ expect(wrapper.emitted('update:busy')).not.toBeDefined()
+
+ wrapper.setData({
+ localBusy: true
+ })
+
+ expect(wrapper.emitted('update:busy')).toBeDefined()
+ expect(wrapper.emitted('update:busy')[0][0]).toEqual(true)
+ })
+
+ it('should render table-busy slot when prop busy=true and slot provided', async () => {
+ const wrapper = mount(Table, {
+ propsData: {
+ busy: false,
+ items: testItems
+ },
+ slots: {
+ // Note slot data needs to be wrapped in an element.
+ // https://github.com/vue/vue-test-utils/issues:992
+ // Will be fixed in v1.0.0-beta.26
+ 'table-busy': 'busy slot content'
+ }
+ })
+ expect(wrapper.attributes('aria-busy')).toBeDefined()
+ expect(wrapper.attributes('aria-busy')).toEqual('false')
+ expect(wrapper.find('tbody').exists()).toBe(true)
+ expect(wrapper.find('tbody').findAll('tr').exists()).toBe(true)
+ expect(wrapper.find('tbody').findAll('tr').length).toBe(testItems.length)
+
+ wrapper.setProps({
+ busy: true
+ })
+
+ expect(wrapper.attributes('aria-busy')).toBeDefined()
+ expect(wrapper.attributes('aria-busy')).toEqual('true')
+ expect(wrapper.find('tbody').exists()).toBe(true)
+ expect(wrapper.find('tbody').findAll('tr').exists()).toBe(true)
+ expect(wrapper.find('tbody').findAll('tr').length).toBe(1)
+ expect(wrapper.find('tbody').text()).toContain('busy slot content')
+ expect(wrapper.find('tbody').find('tr').classes()).toContain('b-table-busy-slot')
+
+ wrapper.setProps({
+ busy: false
+ })
+
+ expect(wrapper.attributes('aria-busy')).toBeDefined()
+ expect(wrapper.attributes('aria-busy')).toEqual('false')
+ expect(wrapper.find('tbody').exists()).toBe(true)
+ expect(wrapper.find('tbody').findAll('tr').exists()).toBe(true)
+ expect(wrapper.find('tbody').findAll('tr').length).toBe(testItems.length)
+ })
+})
diff --git a/src/components/table/table.js b/src/components/table/table.js
index 0c3acda9933..ef495e7730e 100644
--- a/src/components/table/table.js
+++ b/src/components/table/table.js
@@ -222,122 +222,146 @@ export default {
rows.push(h(false))
}
- // Add the item data rows
- items.forEach((item, rowIndex) => {
- const detailsSlot = $scoped['row-details']
- const rowShowDetails = Boolean(item._showDetails && detailsSlot)
- // 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)
- }
+ // Add the item data rows or the busy slot
+ if ($slots['table-busy'] && this.computedBusy) {
+ // Show the busy slot
+ const trAttrs = {
+ role: this.isStacked ? 'row' : null
}
- // For each item data field in row
- const tds = fields.map((field, colIndex) => {
- const formatted = this.getFormattedValue(item, field)
- const data = {
- key: `row-${rowIndex}-cell-${colIndex}`,
- class: this.tdClasses(field, item),
- attrs: this.tdAttrs(field, item, colIndex),
- domProps: {}
- }
- let childNodes
- if ($scoped[field.key]) {
- childNodes = [
- $scoped[field.key]({
- item: item,
- index: rowIndex,
- field: field,
- unformatted: _get(item, field.key, ''),
- value: formatted,
- toggleDetails: toggleDetailsFn,
- detailsShowing: Boolean(item._showDetails)
- })
- ]
- if (this.isStacked) {
- // We wrap in a DIV to ensure rendered as a single cell when visually stacked!
- childNodes = [h('div', {}, [childNodes])]
- }
- } else {
- if (this.isStacked) {
- // We wrap in a DIV to ensure rendered as a single cell when visually stacked!
- childNodes = [h('div', formatted)]
- } else {
- // Non stacked
- childNodes = formatted
- }
- }
- // Render either a td or th cell
- return h(field.isRowHeader ? 'th' : 'td', data, childNodes)
- })
- // Calculate the row number in the dataset (indexed from 1)
- let ariaRowIndex = null
- if (this.currentPage && this.perPage && this.perPage > 0) {
- ariaRowIndex = String((this.currentPage - 1) * this.perPage + rowIndex + 1)
+ const tdAttrs = {
+ colspan: String(fields.length),
+ role: this.isStacked ? 'cell' : null
}
- // Assemble and add the row
rows.push(
h(
'tr',
{
- key: `row-${rowIndex}`,
- class: [
- this.rowClasses(item),
- { 'b-table-has-details': rowShowDetails }
- ],
- attrs: {
- 'aria-describedby': detailsId,
- 'aria-owns': detailsId,
- 'aria-rowindex': ariaRowIndex,
- role: this.isStacked ? 'row' : null
- },
- on: {
- click: evt => { this.rowClicked(evt, item, rowIndex) },
- contextmenu: evt => { this.rowContextmenu(evt, item, rowIndex) },
- dblclick: evt => { this.rowDblClicked(evt, item, rowIndex) },
- mouseenter: evt => { this.rowHovered(evt, item, rowIndex) },
- mouseleave: evt => { this.rowUnhovered(evt, item, rowIndex) }
- }
+ key: 'table-busy-slot',
+ staticClass: 'b-table-busy-slot',
+ class: [typeof this.tbodyTrClass === 'function' ? this.tbodyTrClass(null, 'table-busy') : this.tbodyTrClass],
+ attrs: trAttrs
},
- tds
+ [h('td', { attrs: tdAttrs }, [$slots['table-busy']])]
)
)
- // Row Details slot
- if (rowShowDetails) {
- const tdAttrs = { colspan: String(fields.length) }
- const trAttrs = { id: detailsId }
- if (this.isStacked) {
- tdAttrs['role'] = 'cell'
- trAttrs['role'] = 'row'
+ } else {
+ // Show the rows
+ items.forEach((item, rowIndex) => {
+ const detailsSlot = $scoped['row-details']
+ const rowShowDetails = Boolean(item._showDetails && detailsSlot)
+ // 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)
+ }
}
- const details = h('td', { attrs: tdAttrs }, [
- detailsSlot({
- item: item,
- index: rowIndex,
- fields: fields,
- toggleDetails: toggleDetailsFn
- })
- ])
+ // For each item data field in row
+ const tds = fields.map((field, colIndex) => {
+ const formatted = this.getFormattedValue(item, field)
+ const data = {
+ key: `row-${rowIndex}-cell-${colIndex}`,
+ class: this.tdClasses(field, item),
+ attrs: this.tdAttrs(field, item, colIndex),
+ domProps: {}
+ }
+ let childNodes
+ if ($scoped[field.key]) {
+ childNodes = [
+ $scoped[field.key]({
+ item: item,
+ index: rowIndex,
+ field: field,
+ unformatted: _get(item, field.key, ''),
+ value: formatted,
+ toggleDetails: toggleDetailsFn,
+ detailsShowing: Boolean(item._showDetails)
+ })
+ ]
+ if (this.isStacked) {
+ // We wrap in a DIV to ensure rendered as a single cell when visually stacked!
+ childNodes = [h('div', {}, [childNodes])]
+ }
+ } else {
+ if (this.isStacked) {
+ // We wrap in a DIV to ensure rendered as a single cell when visually stacked!
+ childNodes = [h('div', formatted)]
+ } else {
+ // Non stacked
+ childNodes = formatted
+ }
+ }
+ // Render either a td or th cell
+ return h(field.isRowHeader ? 'th' : 'td', data, childNodes)
+ })
+ // Calculate the row number in the dataset (indexed from 1)
+ let ariaRowIndex = null
+ if (this.currentPage && this.perPage && this.perPage > 0) {
+ ariaRowIndex = String((this.currentPage - 1) * this.perPage + rowIndex + 1)
+ }
+ // Assemble and add the row
rows.push(
h(
'tr',
{
- key: `details-${rowIndex}`,
- staticClass: 'b-table-details',
- class: [typeof this.tbodyTrClass === 'function' ? this.tbodyTrClass(item, 'row-details') : this.tbodyTrClass],
- attrs: trAttrs
+ key: `row-${rowIndex}`,
+ class: [
+ this.rowClasses(item),
+ { 'b-table-has-details': rowShowDetails }
+ ],
+ attrs: {
+ 'aria-describedby': detailsId,
+ 'aria-owns': detailsId,
+ 'aria-rowindex': ariaRowIndex,
+ role: this.isStacked ? 'row' : null
+ },
+ on: {
+ click: evt => { this.rowClicked(evt, item, rowIndex) },
+ contextmenu: evt => { this.rowContextmenu(evt, item, rowIndex) },
+ dblclick: evt => { this.rowDblClicked(evt, item, rowIndex) },
+ mouseenter: evt => { this.rowHovered(evt, item, rowIndex) },
+ mouseleave: evt => { this.rowUnhovered(evt, item, rowIndex) }
+ }
},
- [details]
+ tds
)
)
- } else if (detailsSlot) {
- // Only add the placeholder if a the table has a row-details slot defined (but not shown)
- rows.push(h(false))
- }
- })
+ // Row Details slot
+ if (rowShowDetails) {
+ const tdAttrs = { colspan: String(fields.length) }
+ const trAttrs = { id: detailsId }
+ if (this.isStacked) {
+ tdAttrs['role'] = 'cell'
+ trAttrs['role'] = 'row'
+ }
+ const details = h('td', { attrs: tdAttrs }, [
+ detailsSlot({
+ item: item,
+ index: rowIndex,
+ fields: fields,
+ toggleDetails: toggleDetailsFn
+ })
+ ])
+ rows.push(
+ h(
+ 'tr',
+ {
+ key: `details-${rowIndex}`,
+ staticClass: 'b-table-details',
+ class: [typeof this.tbodyTrClass === 'function' ? this.tbodyTrClass(item, 'row-details') : this.tbodyTrClass],
+ attrs: trAttrs
+ },
+ [details]
+ )
+ )
+ } else if (detailsSlot) {
+ // Only add the placeholder if a the table has a row-details slot defined (but not shown)
+ rows.push(h(false))
+ }
+ })
+ }
// Empty Items / Empty Filtered Row slot
if (this.showEmpty && (!items || items.length === 0)) {
|