Skip to content

Commit ab7937e

Browse files
authored
feat(table): make some slots available either as scoped or unscoped (bootstrap-vue#2740)
1 parent dcb7939 commit ab7937e

File tree

4 files changed

+61
-31
lines changed

4 files changed

+61
-31
lines changed

src/components/table/README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,13 @@ grouping and styling of table columns. Note the styles available via `<col>` ele
717717
Refer to [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/colgroup) for details and
718718
usage of `<colgroup>`
719719

720+
Slot `table-colgroup` can be optionally scoped, receiving an object with the following properties:
721+
722+
| Property | Type | Description |
723+
| --------- | ------ | ---------------------------------------------------------------------------- |
724+
| `columns` | Number | The number of columns in the rendered table |
725+
| `fields` | Array | Array of field defintion objects (normalized to the array of objects format) |
726+
720727
## Table busy state
721728

722729
`<b-table>` provides a `busy` prop that will flag the table as busy, which you can set to `true`
@@ -751,6 +758,7 @@ the table's busy state is `true`. The slot will be placed in a `<tr>` element wi
751758

752759
<b-table :items="items" :busy="isBusy" class="mt-3" outlined>
753760
<div slot="table-busy" class="text-center text-danger my-2">
761+
<b-spinner class="align-middle" />
754762
<strong>Loading...</strong>
755763
</div>
756764
</b-table>
@@ -1045,7 +1053,8 @@ slot is provided, then the footer will use the `HEAD_` slot content.
10451053
</div>
10461054
```
10471055

1048-
The slot's scope variable (`data` in the above example) will have the following properties:
1056+
The slots can be optionally scoped (`data` in the above example), and will have the following
1057+
properties:
10491058

10501059
| Property | Type | Description |
10511060
| -------- | ------ | ------------------------------------------------------------- |
@@ -1110,6 +1119,14 @@ This slot is inserted before the header cells row, and is not encapsulated by `<
11101119
<!-- b-table-thead-top-slot.vue -->
11111120
```
11121121

1122+
Slot `thead-top` can be optionally scoped, receiving an object with the following properties:
1123+
1124+
| Property | Type | Description |
1125+
| --------- | ------ | ---------------------------------------------------------------------------- |
1126+
| `columns` | Number | The number of columns in the rendered table |
1127+
| `fields` | Array | Array of field defintion objects (normalized to the array of objects format) |
1128+
1129+
11131130
## Row select support
11141131

11151132
You can make rows selectable, by using the prop `selectable`.

src/components/table/package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@
197197
},
198198
{
199199
"name": "table-colgroup",
200-
"description": "Slot to place custom colgroup and col elements"
200+
"description": "Slot to place custom colgroup and col elements (optionally scoped: columns - number of columns, fields - array of field definition objects)"
201201
},
202202
{
203203
"name": "table-busy",
@@ -221,23 +221,23 @@
221221
},
222222
{
223223
"name": "empty",
224-
"description": "Content to display when no items are present in the `items` array"
224+
"description": "Content to display when no items are present in the `items` array (optionally scoped: see docs for details)"
225225
},
226226
{
227227
"name": "emptyfiltered",
228-
"description": "Content to display when no items are present in the filtered `items` array"
228+
"description": "Content to display when no items are present in the filtered `items` array (optionally scoped: see docs for details)"
229229
},
230230
{
231231
"name": "thead-top",
232-
"description": "Slot above the column headers in the `thead` element for user-supplied rows (ex: in the case of extra header labels / group labels). Scoped data: columns - number of TDs to provide, fields - fields object"
232+
"description": "Slot above the column headers in the `thead` element for user-supplied rows (optionally scoped: columns - number of TDs to provide, fields - array of field definition objects)"
233233
},
234234
{
235235
"name": "top-row",
236-
"description": "Fixed top row slot for user supplied TD cells. Scoped data: columns - number of TDs to provide, fields - fields object"
236+
"description": "Fixed top row slot for user supplied TD cells (Optionally scoped: columns - number of TDs to provide, fields - array of field definition objects)"
237237
},
238238
{
239239
"name": "bottom-row",
240-
"description": "Fixed bottom row slot for user supplied TD cells. Scoped data: columns - number of TDs to provide, fields - fields object"
240+
"description": "Fixed bottom row slot for user supplied TD cells (Optionally Scoped: columns - number of TDs to provide, fields - array of field definition objects)"
241241
}
242242
]
243243
}

src/components/table/table.js

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { closest, matches } from '../../utils/dom'
1111
import fieldToString from '../../utils/to-string'
1212
import idMixin from '../../mixins/id'
1313
import listenOnRootMixin from '../../mixins/listen-on-root'
14+
import normalizeSlotMixin from '../../mixins/normalize-slot'
1415

1516
// Object of item keys that should be ignored for headers and stringification and filter events
1617
const IGNORED_FIELD_KEYS = {
@@ -143,7 +144,7 @@ function filterEvent(evt) {
143144
// @vue/component
144145
export default {
145146
name: 'BTable',
146-
mixins: [idMixin, listenOnRootMixin],
147+
mixins: [idMixin, listenOnRootMixin, normalizeSlotMixin],
147148
// Don't place ATTRS on root element automatically, as table could be wrapped in responsive div
148149
inheritAttrs: false,
149150
props: {
@@ -1117,9 +1118,11 @@ export default {
11171118
// Check number of arguments provider function requested
11181119
// Provider not using callback (didn't request second argument), so we clear
11191120
// busy state as most likely there was an error in the provider function
1121+
/* istanbul ignore next */
11201122
warn(
11211123
"b-table provider function didn't request calback and did not return a promise or data"
11221124
)
1125+
/* istanbul ignore next */
11231126
this.localBusy = false
11241127
}
11251128
} catch (e) /* istanbul ignore next */ {
@@ -1156,14 +1159,13 @@ export default {
11561159
}
11571160

11581161
// Build the colgroup
1159-
const colgroup = $slots['table-colgroup']
1160-
? h('colgroup', { key: 'colgroup' }, $slots['table-colgroup'])
1161-
: h(false)
1162-
1163-
// Support scoped and unscoped slots when needed
1164-
const normalizeSlot = (slotName, slotScope = {}) => {
1165-
const slot = $scoped[slotName] || $slots[slotName]
1166-
return typeof slot === 'function' ? slot(slotScope) : slot
1162+
let colgroup = h(false)
1163+
if (this.hasNormalizedSlot('table-colgroup')) {
1164+
colgroup = h(
1165+
'colgroup',
1166+
{ key: 'colgroup' },
1167+
this.normalizeSlot('table-colgroup', { columns: fields.length, fields: fields })
1168+
)
11671169
}
11681170

11691171
// factory function for thead and tfoot cells (th's)
@@ -1215,12 +1217,13 @@ export default {
12151217
}
12161218
}
12171219
}
1220+
let fieldScope = { label: field.label, column: field.key, field: field }
12181221
let slot =
1219-
isFoot && $scoped[`FOOT_${field.key}`]
1220-
? $scoped[`FOOT_${field.key}`]
1221-
: $scoped[`HEAD_${field.key}`]
1222+
isFoot && this.hasNormalizedSlot(`FOOT_${field.key}`)
1223+
? this.normalizeSlot(`FOOT_${field.key}`, fieldScope)
1224+
: this.normalizeSlot(`HEAD_${field.key}`, fieldScope)
12221225
if (slot) {
1223-
slot = [slot({ label: field.label, column: field.key, field: field })]
1226+
slot = [slot]
12241227
} else {
12251228
data.domProps = htmlOrText(field.labelHtml, field.label)
12261229
}
@@ -1234,8 +1237,10 @@ export default {
12341237
// If in always stacked mode (this.isStacked === true), then we don't bother rendering the thead
12351238
const theadChildren = []
12361239

1237-
if ($scoped['thead-top']) {
1238-
theadChildren.push($scoped['thead-top']({ columns: fields.length, fields: fields }))
1240+
if (this.hasNormalizedSlot('thead-top')) {
1241+
theadChildren.push(
1242+
this.normalizeSlot('thead-top', { columns: fields.length, fields: fields })
1243+
)
12391244
} else {
12401245
theadChildren.push(h(false))
12411246
}
@@ -1259,7 +1264,7 @@ export default {
12591264

12601265
// Add static Top Row slot (hidden in visibly stacked mode as we can't control the data-label)
12611266
// If in always stacked mode, we don't bother rendering the row
1262-
if ($scoped['top-row'] && this.isStacked !== true) {
1267+
if (this.hasNormalizedSlot('top-row') && this.isStacked !== true) {
12631268
rows.push(
12641269
h(
12651270
'tr',
@@ -1272,7 +1277,7 @@ export default {
12721277
: this.tbodyTrClass
12731278
]
12741279
},
1275-
[$scoped['top-row']({ columns: fields.length, fields: fields })]
1280+
[this.normalizeSlot('top-row', { columns: fields.length, fields: fields })]
12761281
)
12771282
)
12781283
} else {
@@ -1363,9 +1368,9 @@ export default {
13631368
if (this.currentPage && this.perPage && this.perPage > 0) {
13641369
ariaRowIndex = String((this.currentPage - 1) * this.perPage + rowIndex + 1)
13651370
}
1366-
// Create a unique key based on the record content, to ensure that sub components are
1367-
// re-rendered rather than re-used, which can cause issues. If a primary key is not provided
1368-
// we concatinate the row number and stringified record (in case there are duplicate records).
1371+
// Create a unique :key to help ensure that sub components are re-rendered rather than
1372+
// re-used, which can cause issues. If a primary key is not provided we use the rendered
1373+
// rows index within the tbody.
13691374
// See: https://github.com/bootstrap-vue/bootstrap-vue/issues/2410
13701375
const primaryKey = this.primaryKey
13711376
const rowKey =
@@ -1501,7 +1506,7 @@ export default {
15011506
(!items || items.length === 0) &&
15021507
!($slots['table-busy'] && this.computedBusy)
15031508
) {
1504-
let empty = normalizeSlot(this.isFiltered ? 'emptyfiltered' : 'empty', {
1509+
let empty = this.normalizeSlot(this.isFiltered ? 'emptyfiltered' : 'empty', {
15051510
emptyFilteredHtml: this.emptyFilteredHtml,
15061511
emptyFilteredText: this.emptyFilteredText,
15071512
emptyHtml: this.emptyHtml,
@@ -1549,7 +1554,7 @@ export default {
15491554

15501555
// Static bottom row slot (hidden in visibly stacked mode as we can't control the data-label)
15511556
// If in always stacked mode, we don't bother rendering the row
1552-
if ($scoped['bottom-row'] && this.isStacked !== true) {
1557+
if (this.hasNormalizedSlot('bottom-row') && this.isStacked !== true) {
15531558
rows.push(
15541559
h(
15551560
'tr',
@@ -1562,7 +1567,7 @@ export default {
15621567
: this.tbodyTrClass
15631568
]
15641569
},
1565-
[$scoped['bottom-row']({ columns: fields.length, fields: fields })]
1570+
this.normalizeSlot('bottom-row', { columns: fields.length, fields: fields })
15661571
)
15671572
)
15681573
} else {

src/mixins/normalize-slot.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
import normalizeSlot from '../utils/normalize-slot'
2+
import { concat } from '../utils/array'
23

34
export default {
45
methods: {
6+
hasNormalizedSlot(name) {
7+
// Returns true if the either a $scopedSlot or $slot exists with the specified name
8+
return Boolean(this.$scopedSlots[name] || this.$slots[name])
9+
},
510
normalizeSlot(name, scope = {}) {
6-
return normalizeSlot(name, scope, this.$scopedSlots, this.$slots)
11+
// Returns an array of rendered vNodes if slot found.
12+
// Returns undefined if not found.
13+
const vNodes = normalizeSlot(name, scope, this.$scopedSlots, this.$slots)
14+
return vNodes ? concat(vNodes) : vNodes
715
}
816
}
917
}

0 commit comments

Comments
 (0)