Skip to content

Commit 9a4fe24

Browse files
authored
feat(b-table): programmatic row selection (closes bootstrap-vue#3064, bootstrap-vue#3370) (bootstrap-vue#3844)
1 parent ed99f9c commit 9a4fe24

File tree

5 files changed

+488
-64
lines changed

5 files changed

+488
-64
lines changed

src/components/table/README.md

Lines changed: 66 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1403,6 +1403,25 @@ When a table is `selectable` and the user clicks on a row, `<b-table>` will emit
14031403
event, passing a single argument which is the complete list of selected items. **Treat this argument
14041404
as read-only.**
14051405

1406+
Rows can also be programmatically selected and unselected via the following exposed methods on the
1407+
`<b-table>` instance (i.e. via a reference to the table instance via `this.$refs`):
1408+
1409+
| Method | Description |
1410+
| ---------------------- | ---------------------------------------------------------------------------------------------------- |
1411+
| `selectRow(index)` | Selects a row with the given `index` number. |
1412+
| `unselectRow(index)` | Unselects a row with the given `index` number. |
1413+
| `selectAllRows()` | Selects all rows in the table, except in `single` mode in which case only the first row is selected. |
1414+
| `clearSelected()` | Unselects all rows. |
1415+
| `isRowSelected(index)` | Returns `true` if the row with the given `index` is selected, otherwise it returns `false`. |
1416+
1417+
Programmatic selection notes:
1418+
1419+
- `index` the zero-based index of the table's **visible rows**, after filtering, sorting, and
1420+
pagination have been applied.
1421+
- In `single` mode, `selectRow(index)` will unselect any previous selected row.
1422+
- Attempting to `selectRow(index)` or `unselectRow(index)` on a non-existent row will be ignored.
1423+
- The table must be `selectable` for any of these methods to have effect.
1424+
14061425
```html
14071426
<template>
14081427
<div>
@@ -1411,30 +1430,37 @@ as read-only.**
14111430
</b-form-group>
14121431

14131432
<b-table
1433+
ref="selectableTable"
14141434
selectable
14151435
:select-mode="selectMode"
1416-
selectedVariant="success"
1436+
selected-variant="success"
14171437
:items="items"
14181438
:fields="fields"
1419-
@row-selected="rowSelected"
1439+
@row-selected="onRowSelected"
14201440
responsive="sm"
14211441
>
1422-
<!-- We use colgroup to set some widths for styling only -->
1423-
<template slot="table-colgroup">
1424-
<col style="width: 75px;">
1425-
<col style="width: 125px;">
1426-
<col style="width: 75px;">
1427-
<col>
1428-
<col>
1429-
</template>
14301442
<!-- Example scoped slot for select state illustrative purposes -->
14311443
<template slot="[selected]" slot-scope="{ rowSelected }">
1432-
<span v-if="rowSelected">☑</span>
1433-
<span v-else>☐</span>
1444+
<template v-if="rowSelected">
1445+
<span aria-hidden="true">&check;</span>
1446+
<span class="sr-only">Selected</span>
1447+
</template>
1448+
<template v-else>
1449+
<span aria-hidden="true">&nbsp;</span>
1450+
<span class="sr-only">Not selected</span>
1451+
</template>
14341452
</template>
14351453
</b-table>
1436-
1437-
{{ selected }}
1454+
<p>
1455+
<b-button size="sm" @click="selectAllRows">Select all</b-button>
1456+
<b-button size="sm" @click="clearSelected">Clear selected</b-button>
1457+
<b-button size="sm" @click="selectThirdRow">Select 3rd row</b-button>
1458+
<b-button size="sm" @click="unselectThirdRow">Unselect 3rd row</b-button>
1459+
</p>
1460+
<p>
1461+
Selected Rows:<br>
1462+
{{ selected }}
1463+
</p>
14381464
</div>
14391465
</template>
14401466

@@ -1455,8 +1481,22 @@ as read-only.**
14551481
}
14561482
},
14571483
methods: {
1458-
rowSelected(items) {
1484+
onRowSelected(items) {
14591485
this.selected = items
1486+
},
1487+
selectAllRows() {
1488+
this.$refs.selectableTable.selectAllRows()
1489+
},
1490+
clearSelected() {
1491+
this.$refs.selectableTable.clearSelected()
1492+
},
1493+
selectThirdRow() {
1494+
// Rows are indexed from 0, so the third row is index 2
1495+
this.$refs.selectableTable.selectRow(2)
1496+
},
1497+
unselectThirdRow() {
1498+
// Rows are indexed from 0, so the third row is index 2
1499+
this.$refs.selectableTable.unselectRow(2)
14601500
}
14611501
}
14621502
}
@@ -1465,7 +1505,7 @@ as read-only.**
14651505
<!-- b-table-selectable.vue -->
14661506
```
14671507

1468-
When table is selectable, it will have class `b-table-selectable`, and one of the following three
1508+
When a table is selectable, it will have class `b-table-selectable`, and one of the following three
14691509
classes (depending on which mode is in use), on the `<table>` element:
14701510

14711511
- `b-table-select-single`
@@ -1475,13 +1515,22 @@ classes (depending on which mode is in use), on the `<table>` element:
14751515
When at least one row is selected the class `b-table-selecting` will be active on the `<table>`
14761516
element.
14771517

1518+
Use the prop `selected-variant` to apply a Bootstrap theme color to the selected row(s). Note, due
1519+
to the order that the table variants are defined in Bootstrap's CSS, any row-variant's may take
1520+
precedence over the `selected-variant`. You can set `selected-variant` to an empty string if you
1521+
will be using other means to convey that a row is selected (such as a scoped field slot in the above
1522+
example).
1523+
14781524
**Notes:**
14791525

14801526
- Paging, filtering, or sorting will clear the selection. The `row-selected` event will be emitted
14811527
with an empty array if needed.
14821528
- Selected rows will have a class of `b-row-selected` added to them.
14831529
- When the table is in `selectable` mode, all data item `<tr>` elements will be in the document tab
1484-
sequence (`tabindex="0"`) for accessibility reasons.
1530+
sequence (`tabindex="0"`) for [accessibility](#accessibility) reasons, and will have the attribute
1531+
`aria-selected` set to either `'true'` or `'false'` depending on the selected state of the row.
1532+
- When a table is `selectable`, the table will have the attribute `aria-multiselect` set to either
1533+
`'false'` for `single` mode, and `'true'` for either `multi` or `range` modes.
14851534

14861535
### Table body transition support
14871536

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

Lines changed: 86 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import looseEqual from '../../../utils/loose-equal'
2+
import range from '../../../utils/range'
23
import { isArray, arrayIncludes } from '../../../utils/array'
34
import { getComponentConfig } from '../../../utils/config'
5+
import { isNumber } from '../../../utils/inspect'
46
import sanitizeRow from './sanitize-row'
57

68
export default {
@@ -11,7 +13,8 @@ export default {
1113
},
1214
selectMode: {
1315
type: String,
14-
default: 'multi'
16+
default: 'multi',
17+
validator: val => arrayIncludes(['range', 'multi', 'single'], val)
1518
},
1619
selectedVariant: {
1720
type: String,
@@ -25,36 +28,42 @@ export default {
2528
}
2629
},
2730
computed: {
31+
isSelectable() {
32+
return this.selectable && this.selectMode
33+
},
34+
selectableHasSelection() {
35+
return (
36+
this.isSelectable &&
37+
this.selectedRows &&
38+
this.selectedRows.length > 0 &&
39+
this.selectedRows.some(Boolean)
40+
)
41+
},
42+
selectableIsMultiSelect() {
43+
return this.isSelectable && arrayIncludes(['range', 'multi'], this.selectMode)
44+
},
2845
selectableTableClasses() {
29-
const selectable = this.selectable
30-
const isSelecting = selectable && this.selectedRows && this.selectedRows.some(Boolean)
3146
return {
32-
'b-table-selectable': selectable,
33-
[`b-table-select-${this.selectMode}`]: selectable,
34-
'b-table-selecting': isSelecting
47+
'b-table-selectable': this.isSelectable,
48+
[`b-table-select-${this.selectMode}`]: this.isSelectable,
49+
'b-table-selecting': this.selectableHasSelection
3550
}
3651
},
3752
selectableTableAttrs() {
3853
return {
39-
'aria-multiselectable': this.selectableIsMultiSelect
40-
}
41-
},
42-
selectableIsMultiSelect() {
43-
if (this.selectable) {
44-
return arrayIncludes(['range', 'multi'], this.selectMode) ? 'true' : 'false'
45-
} else {
46-
return null
54+
'aria-multiselectable': !this.isSelectable
55+
? null
56+
: this.selectableIsMultiSelect
57+
? 'true'
58+
: 'false'
4759
}
4860
}
4961
},
5062
watch: {
5163
computedItems(newVal, oldVal) {
5264
// Reset for selectable
53-
// TODO: Should selectedLastClicked be reset here?
54-
// As changes to _showDetails would trigger it to reset
55-
this.selectedLastRow = -1
5665
let equal = false
57-
if (this.selectable && this.selectedRows.length > 0) {
66+
if (this.isSelectable && this.selectedRows.length > 0) {
5867
// Quick check against array length
5968
equal = isArray(newVal) && isArray(oldVal) && newVal.length === oldVal.length
6069
for (let i = 0; equal && i < newVal.length; i++) {
@@ -74,9 +83,9 @@ export default {
7483
this.clearSelected()
7584
},
7685
selectedRows(selectedRows, oldVal) {
77-
if (this.selectable && !looseEqual(selectedRows, oldVal)) {
86+
if (this.isSelectable && !looseEqual(selectedRows, oldVal)) {
7887
const items = []
79-
// forEach skips over non-existant indicies (on sparse arrays)
88+
// `.forEach()` skips over non-existent indices (on sparse arrays)
8089
selectedRows.forEach((v, idx) => {
8190
if (v) {
8291
items.push(this.computedItems[idx])
@@ -88,35 +97,67 @@ export default {
8897
},
8998
beforeMount() {
9099
// Set up handlers
91-
if (this.selectable) {
100+
if (this.isSelectable) {
92101
this.setSelectionHandlers(true)
93102
}
94103
},
95104
methods: {
96-
isRowSelected(idx) {
97-
return Boolean(this.selectedRows[idx])
105+
// Public methods
106+
selectRow(index) {
107+
// Select a particular row (indexed based on computedItems)
108+
if (
109+
this.isSelectable &&
110+
isNumber(index) &&
111+
index >= 0 &&
112+
index < this.computedItems.length &&
113+
!this.isRowSelected(index)
114+
) {
115+
const selectedRows = this.selectableIsMultiSelect ? this.selectedRows.slice() : []
116+
selectedRows[index] = true
117+
this.selectedLastClicked = -1
118+
this.selectedRows = selectedRows
119+
}
98120
},
99-
selectableRowClasses(idx) {
100-
const rowSelected = this.isRowSelected(idx)
101-
const base = this.dark ? 'bg' : 'table'
102-
const variant = this.selectedVariant
103-
return {
104-
'b-table-row-selected': this.selectable && rowSelected,
105-
[`${base}-${variant}`]: this.selectable && rowSelected && variant
121+
unselectRow(index) {
122+
// Un-select a particular row (indexed based on `computedItems`)
123+
if (this.isSelectable && isNumber(index) && this.isRowSelected(index)) {
124+
const selectedRows = this.selectedRows.slice()
125+
selectedRows[index] = false
126+
this.selectedLastClicked = -1
127+
this.selectedRows = selectedRows
106128
}
107129
},
108-
selectableRowAttrs(idx) {
109-
return {
110-
'aria-selected': !this.selectable ? null : this.isRowSelected(idx) ? 'true' : 'false'
130+
selectAllRows() {
131+
const length = this.computedItems.length
132+
if (this.isSelectable && length > 0) {
133+
this.selectedLastClicked = -1
134+
this.selectedRows = this.selectableIsMultiSelect ? range(length).map(i => true) : [true]
111135
}
112136
},
137+
isRowSelected(index) {
138+
// Determine if a row is selected (indexed based on `computedItems`)
139+
return Boolean(isNumber(index) && this.selectedRows[index])
140+
},
113141
clearSelected() {
114-
const hasSelection = this.selectedRows.reduce((prev, v) => {
115-
return prev || v
116-
}, false)
117-
if (hasSelection) {
118-
this.selectedLastClicked = -1
119-
this.selectedRows = []
142+
// Clear any active selected row(s)
143+
this.selectedLastClicked = -1
144+
this.selectedRows = []
145+
},
146+
// Internal private methods
147+
selectableRowClasses(index) {
148+
if (this.isSelectable && this.isRowSelected(index)) {
149+
const variant = this.selectedVariant
150+
return {
151+
'b-table-row-selected': true,
152+
[`${this.dark ? 'bg' : 'table'}-${variant}`]: variant
153+
}
154+
} else {
155+
return {}
156+
}
157+
},
158+
selectableRowAttrs(index) {
159+
return {
160+
'aria-selected': !this.isSelectable ? null : this.isRowSelected(index) ? 'true' : 'false'
120161
}
121162
},
122163
setSelectionHandlers(on) {
@@ -129,20 +170,20 @@ export default {
129170
},
130171
selectionHandler(item, index, evt) {
131172
/* istanbul ignore if: should never happen */
132-
if (!this.selectable) {
173+
if (!this.isSelectable) {
133174
// Don't do anything if table is not in selectable mode
134175
/* istanbul ignore next: should never happen */
135176
this.clearSelected()
136177
/* istanbul ignore next: should never happen */
137178
return
138179
}
180+
const selectMode = this.selectMode
139181
let selectedRows = this.selectedRows.slice()
140182
let selected = !selectedRows[index]
141-
const mode = this.selectMode
142-
// Note 'multi' mode needs no special handling
143-
if (mode === 'single') {
183+
// Note 'multi' mode needs no special event handling
184+
if (selectMode === 'single') {
144185
selectedRows = []
145-
} else if (mode === 'range') {
186+
} else if (selectMode === 'range') {
146187
if (this.selectedLastRow > -1 && evt.shiftKey) {
147188
// range
148189
for (
@@ -155,7 +196,7 @@ export default {
155196
selected = true
156197
} else {
157198
if (!(evt.ctrlKey || evt.metaKey)) {
158-
// clear range selection if any
199+
// Clear range selection if any
159200
selectedRows = []
160201
selected = true
161202
}

src/components/table/helpers/mixin-tbody-row.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ export default {
182182
}
183183
if (this.selectedRows) {
184184
// Add in rowSelected scope property if selectable rows supported
185-
slotScope.rowSelected = Boolean(this.selectedRows[rowIndex])
185+
slotScope.rowSelected = this.isRowSelected(rowIndex)
186186
}
187187
// TODO:
188188
// Using `field.key` as scoped slot name is deprecated, to be removed in future release
@@ -203,7 +203,7 @@ export default {
203203
const tableStriped = this.striped
204204
const hasDetailsSlot = this.hasNormalizedSlot(detailsSlotName)
205205
const rowShowDetails = Boolean(item._showDetails && hasDetailsSlot)
206-
const hasRowClickHandler = this.$listeners['row-clicked'] || this.selectable
206+
const hasRowClickHandler = this.$listeners['row-clicked'] || this.isSelectable
207207

208208
// We can return more than one TR if rowDetails enabled
209209
const $rows = []

src/components/table/index.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ export declare class BTable extends BvComponent {
1515
// Public methods
1616
refresh: () => void
1717
clearSelected: () => void
18+
selectAllRows: () => void
19+
isRowSelected: (index: number) => boolean
20+
selectRow: (index: number) => void
21+
unselectRow: (index: number) => void
1822
// Props
1923
id?: string
2024
items: Array<any> | BvTableProviderCallback

0 commit comments

Comments
 (0)