vue-slottable 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +21 -0
- package/README.md +209 -0
- package/dist/style.css +1 -0
- package/dist/vue-slottable.mjs +81 -0
- package/dist/vue-slottable.umd.js +1 -0
- package/package.json +62 -0
- package/src/App.vue +200 -0
- package/src/components/SlotTable.vue +114 -0
- package/src/components/SlotTableColumn.vue +16 -0
- package/src/components/SlotTableColumnGroup.vue +20 -0
- package/src/index.js +3 -0
- package/src/main.js +4 -0
- package/src/types.d.ts +25 -0
package/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# vue-slottable
|
|
2
|
+
|
|
3
|
+
[](https://github.com/nickvdyck/vue-slot-table/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/vue-slottable)
|
|
5
|
+
[](./LICENSE.txt)
|
|
6
|
+
|
|
7
|
+
A flexible, slot-based Vue 3 table component with sticky columns, column groups, and scoped slots for custom cell rendering.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Slot-based columns** — define headers and cells with named slots
|
|
12
|
+
- **Sticky columns** — pin columns to the left or right edge
|
|
13
|
+
- **Column groups** — add grouped header rows with colspan
|
|
14
|
+
- **Row click events** — handle row interactions
|
|
15
|
+
- **Zero CSS opinions** — bring your own styles
|
|
16
|
+
- **Lightweight** — no dependencies beyond Vue 3
|
|
17
|
+
- **TypeScript declarations** included
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install vue-slottable
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Requires Vue 3.4+.
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```vue
|
|
30
|
+
<script setup>
|
|
31
|
+
import { SlotTable, SlotTableColumn } from 'vue-slottable'
|
|
32
|
+
|
|
33
|
+
const rows = [
|
|
34
|
+
{ name: 'Alice', age: 28 },
|
|
35
|
+
{ name: 'Bob', age: 34 },
|
|
36
|
+
]
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<template>
|
|
40
|
+
<SlotTable :rows="rows" table-class="my-table">
|
|
41
|
+
<SlotTableColumn>
|
|
42
|
+
<template #header>Name</template>
|
|
43
|
+
<template #cell="{ row }">{{ row.name }}</template>
|
|
44
|
+
</SlotTableColumn>
|
|
45
|
+
|
|
46
|
+
<SlotTableColumn>
|
|
47
|
+
<template #header>Age</template>
|
|
48
|
+
<template #cell="{ row }">{{ row.age }}</template>
|
|
49
|
+
</SlotTableColumn>
|
|
50
|
+
</SlotTable>
|
|
51
|
+
</template>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## API Reference
|
|
55
|
+
|
|
56
|
+
### `<SlotTable>`
|
|
57
|
+
|
|
58
|
+
The main table component. Renders a `<table>` element.
|
|
59
|
+
|
|
60
|
+
#### Props
|
|
61
|
+
|
|
62
|
+
| Prop | Type | Required | Default | Description |
|
|
63
|
+
|------|------|----------|---------|-------------|
|
|
64
|
+
| `rows` | `Array` | Yes | — | Array of row data objects |
|
|
65
|
+
| `table-class` | `String` | No | `''` | CSS class applied to the `<table>` element |
|
|
66
|
+
|
|
67
|
+
#### Events
|
|
68
|
+
|
|
69
|
+
| Event | Payload | Description |
|
|
70
|
+
|-------|---------|-------------|
|
|
71
|
+
| `row-click` | `rowIndex: number` | Emitted when a table row is clicked |
|
|
72
|
+
|
|
73
|
+
#### Slots
|
|
74
|
+
|
|
75
|
+
| Slot | Description |
|
|
76
|
+
|------|-------------|
|
|
77
|
+
| `default` | Accepts `<SlotTableColumn>` and `<SlotTableColumnGroup>` children |
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
### `<SlotTableColumn>`
|
|
82
|
+
|
|
83
|
+
Defines a single table column. This is a renderless component — it must be a direct child of `<SlotTable>`.
|
|
84
|
+
|
|
85
|
+
#### Props
|
|
86
|
+
|
|
87
|
+
| Prop | Type | Default | Description |
|
|
88
|
+
|------|------|---------|-------------|
|
|
89
|
+
| `sticky` | `String` | `''` | Set to `'left'` or `'right'` to make the column sticky |
|
|
90
|
+
|
|
91
|
+
#### Slots
|
|
92
|
+
|
|
93
|
+
| Slot | Scope | Description |
|
|
94
|
+
|------|-------|-------------|
|
|
95
|
+
| `header` | — | Content for the column's `<th>` |
|
|
96
|
+
| `cell` | `{ row, rowIndex, columnIndex }` | Content for each `<td>` in this column |
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
### `<SlotTableColumnGroup>`
|
|
101
|
+
|
|
102
|
+
Defines a grouped header row entry. Must be a direct child of `<SlotTable>`.
|
|
103
|
+
|
|
104
|
+
#### Props
|
|
105
|
+
|
|
106
|
+
| Prop | Type | Default | Description |
|
|
107
|
+
|------|------|---------|-------------|
|
|
108
|
+
| `sticky` | `String` | `''` | Set to `'left'` or `'right'` for sticky positioning |
|
|
109
|
+
| `colspan` | `Number` | — | Number of columns this group header spans |
|
|
110
|
+
|
|
111
|
+
#### Slots
|
|
112
|
+
|
|
113
|
+
| Slot | Description |
|
|
114
|
+
|------|-------------|
|
|
115
|
+
| `default` | Content for the group `<th>` |
|
|
116
|
+
|
|
117
|
+
## Examples
|
|
118
|
+
|
|
119
|
+
### Sticky Columns
|
|
120
|
+
|
|
121
|
+
```vue
|
|
122
|
+
<SlotTable :rows="rows">
|
|
123
|
+
<SlotTableColumn sticky="left">
|
|
124
|
+
<template #header>ID</template>
|
|
125
|
+
<template #cell="{ row }">{{ row.id }}</template>
|
|
126
|
+
</SlotTableColumn>
|
|
127
|
+
|
|
128
|
+
<SlotTableColumn>
|
|
129
|
+
<template #header>Description</template>
|
|
130
|
+
<template #cell="{ row }">{{ row.description }}</template>
|
|
131
|
+
</SlotTableColumn>
|
|
132
|
+
|
|
133
|
+
<SlotTableColumn sticky="right">
|
|
134
|
+
<template #header>Actions</template>
|
|
135
|
+
<template #cell="{ row }">
|
|
136
|
+
<button @click.stop="edit(row)">Edit</button>
|
|
137
|
+
</template>
|
|
138
|
+
</SlotTableColumn>
|
|
139
|
+
</SlotTable>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Column Groups
|
|
143
|
+
|
|
144
|
+
```vue
|
|
145
|
+
<SlotTable :rows="rows">
|
|
146
|
+
<SlotTableColumnGroup :colspan="2">Personal</SlotTableColumnGroup>
|
|
147
|
+
<SlotTableColumnGroup :colspan="1">Work</SlotTableColumnGroup>
|
|
148
|
+
|
|
149
|
+
<SlotTableColumn>
|
|
150
|
+
<template #header>Name</template>
|
|
151
|
+
<template #cell="{ row }">{{ row.name }}</template>
|
|
152
|
+
</SlotTableColumn>
|
|
153
|
+
|
|
154
|
+
<SlotTableColumn>
|
|
155
|
+
<template #header>Age</template>
|
|
156
|
+
<template #cell="{ row }">{{ row.age }}</template>
|
|
157
|
+
</SlotTableColumn>
|
|
158
|
+
|
|
159
|
+
<SlotTableColumn>
|
|
160
|
+
<template #header>Role</template>
|
|
161
|
+
<template #cell="{ row }">{{ row.role }}</template>
|
|
162
|
+
</SlotTableColumn>
|
|
163
|
+
</SlotTable>
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Row Click Events
|
|
167
|
+
|
|
168
|
+
```vue
|
|
169
|
+
<script setup>
|
|
170
|
+
function onRowClick(rowIndex) {
|
|
171
|
+
console.log('Clicked row:', rowIndex)
|
|
172
|
+
}
|
|
173
|
+
</script>
|
|
174
|
+
|
|
175
|
+
<template>
|
|
176
|
+
<SlotTable :rows="rows" @row-click="onRowClick">
|
|
177
|
+
<!-- columns... -->
|
|
178
|
+
</SlotTable>
|
|
179
|
+
</template>
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Custom Cell Rendering
|
|
183
|
+
|
|
184
|
+
```vue
|
|
185
|
+
<SlotTableColumn>
|
|
186
|
+
<template #header>Status</template>
|
|
187
|
+
<template #cell="{ row }">
|
|
188
|
+
<span :class="'badge-' + row.status">{{ row.status }}</span>
|
|
189
|
+
</template>
|
|
190
|
+
</SlotTableColumn>
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Development
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
npm install
|
|
197
|
+
npm run dev # Start dev server with demo app
|
|
198
|
+
npm test # Run tests
|
|
199
|
+
npm run lint # Lint
|
|
200
|
+
npm run build:lib # Build library bundles
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Contributing
|
|
204
|
+
|
|
205
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md).
|
|
206
|
+
|
|
207
|
+
## License
|
|
208
|
+
|
|
209
|
+
[MIT](./LICENSE.txt)
|
package/dist/style.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.sticky-left{position:sticky;left:0}.sticky-right{position:sticky;right:0}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { defineComponent as i, h as s } from "vue";
|
|
2
|
+
const w = i({
|
|
3
|
+
name: "SlotTableColumn",
|
|
4
|
+
props: {
|
|
5
|
+
sticky: {
|
|
6
|
+
type: String,
|
|
7
|
+
default: ""
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
setup() {
|
|
11
|
+
return () => null;
|
|
12
|
+
}
|
|
13
|
+
}), P = i({
|
|
14
|
+
name: "SlotTableColumnGroup",
|
|
15
|
+
props: {
|
|
16
|
+
sticky: {
|
|
17
|
+
type: String,
|
|
18
|
+
default: ""
|
|
19
|
+
},
|
|
20
|
+
colspan: {
|
|
21
|
+
type: Number,
|
|
22
|
+
default: void 0
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
setup() {
|
|
26
|
+
return () => null;
|
|
27
|
+
}
|
|
28
|
+
}), N = i({
|
|
29
|
+
name: "SlotTable",
|
|
30
|
+
props: {
|
|
31
|
+
rows: {
|
|
32
|
+
type: Array,
|
|
33
|
+
required: !0
|
|
34
|
+
},
|
|
35
|
+
tableClass: {
|
|
36
|
+
type: String,
|
|
37
|
+
default: ""
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
emits: ["row-click"],
|
|
41
|
+
setup(p, { slots: f, emit: m }) {
|
|
42
|
+
return () => {
|
|
43
|
+
const k = f.default ? f.default() : [], a = [], d = [];
|
|
44
|
+
k.forEach((t) => {
|
|
45
|
+
t.type === w ? a.push(t) : t.type === P && d.push(t);
|
|
46
|
+
});
|
|
47
|
+
const b = a.map((t) => {
|
|
48
|
+
const l = (t.props || {}).sticky || "", n = {
|
|
49
|
+
"sticky-left": l === "left",
|
|
50
|
+
"sticky-right": l === "right"
|
|
51
|
+
}, o = t.children && t.children.header, c = typeof o == "function" ? o() : null;
|
|
52
|
+
return s("th", { class: n }, c);
|
|
53
|
+
}), h = d.map((t) => {
|
|
54
|
+
const e = t.props || {}, l = e.sticky || "", n = e.colspan ? Number(e.colspan) : void 0, o = {
|
|
55
|
+
"sticky-left": l === "left",
|
|
56
|
+
"sticky-right": l === "right"
|
|
57
|
+
}, c = t.children && t.children.default, r = typeof c == "function" ? c() : null;
|
|
58
|
+
return s("th", { class: o, colspan: n }, r);
|
|
59
|
+
}), u = [];
|
|
60
|
+
h.length > 0 && u.push(s("tr", null, h)), u.push(s("tr", null, b));
|
|
61
|
+
const C = s("thead", null, u), S = p.rows.map((t, e) => {
|
|
62
|
+
const l = a.map((n, o) => {
|
|
63
|
+
const r = (n.props || {}).sticky || "", T = {
|
|
64
|
+
"sticky-left": r === "left",
|
|
65
|
+
"sticky-right": r === "right"
|
|
66
|
+
}, y = n.children && n.children.cell, _ = typeof y == "function" ? y({ row: t, rowIndex: e, columnIndex: o }) : null;
|
|
67
|
+
return s("td", { class: T }, _);
|
|
68
|
+
});
|
|
69
|
+
return s("tr", {
|
|
70
|
+
onClick: () => m("row-click", e)
|
|
71
|
+
}, l);
|
|
72
|
+
}), g = s("tbody", null, S);
|
|
73
|
+
return s("table", { class: p.tableClass }, [C, g]);
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
export {
|
|
78
|
+
N as SlotTable,
|
|
79
|
+
w as SlotTableColumn,
|
|
80
|
+
P as SlotTableColumnGroup
|
|
81
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(n,t){typeof exports=="object"&&typeof module<"u"?t(exports,require("vue")):typeof define=="function"&&define.amd?define(["exports","vue"],t):(n=typeof globalThis<"u"?globalThis:n||self,t(n.VueSlotTable={},n.Vue))})(this,function(n,t){"use strict";const p=t.defineComponent({name:"SlotTableColumn",props:{sticky:{type:String,default:""}},setup(){return()=>null}}),f=t.defineComponent({name:"SlotTableColumnGroup",props:{sticky:{type:String,default:""},colspan:{type:Number,default:void 0}},setup(){return()=>null}}),k=t.defineComponent({name:"SlotTable",props:{rows:{type:Array,required:!0},tableClass:{type:String,default:""}},emits:["row-click"],setup(d,{slots:h,emit:S}){return()=>{const C=h.default?h.default():[],u=[],y=[];C.forEach(e=>{e.type===p?u.push(e):e.type===f&&y.push(e)});const g=u.map(e=>{const l=(e.props||{}).sticky||"",o={"sticky-left":l==="left","sticky-right":l==="right"},c=e.children&&e.children.header,r=typeof c=="function"?c():null;return t.h("th",{class:o},r)}),m=y.map(e=>{const s=e.props||{},l=s.sticky||"",o=s.colspan?Number(s.colspan):void 0,c={"sticky-left":l==="left","sticky-right":l==="right"},r=e.children&&e.children.default,i=typeof r=="function"?r():null;return t.h("th",{class:c,colspan:o},i)}),a=[];m.length>0&&a.push(t.h("tr",null,m)),a.push(t.h("tr",null,g));const T=t.h("thead",null,a),P=d.rows.map((e,s)=>{const l=u.map((o,c)=>{const i=(o.props||{}).sticky||"",w={"sticky-left":i==="left","sticky-right":i==="right"},b=o.children&&o.children.cell,G=typeof b=="function"?b({row:e,rowIndex:s,columnIndex:c}):null;return t.h("td",{class:w},G)});return t.h("tr",{onClick:()=>S("row-click",s)},l)}),_=t.h("tbody",null,P);return t.h("table",{class:d.tableClass},[T,_])}}});n.SlotTable=k,n.SlotTableColumn=p,n.SlotTableColumnGroup=f,Object.defineProperty(n,Symbol.toStringTag,{value:"Module"})});
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vue-slottable",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A flexible, slot-based Vue 3 table component with sticky columns, column groups, and scoped slots for custom cell rendering.",
|
|
5
|
+
"main": "./dist/vue-slottable.umd.js",
|
|
6
|
+
"module": "./dist/vue-slottable.mjs",
|
|
7
|
+
"types": "./src/types.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/vue-slottable.mjs",
|
|
11
|
+
"require": "./dist/vue-slottable.umd.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"src"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"dev": "vite",
|
|
20
|
+
"build": "vite build",
|
|
21
|
+
"build:lib": "vite build --mode lib",
|
|
22
|
+
"preview": "vite preview",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"test:watch": "vitest",
|
|
25
|
+
"lint": "eslint src/ tests/",
|
|
26
|
+
"lint:fix": "eslint src/ tests/ --fix"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"vue": "^3.4.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@vitejs/plugin-vue": "^5.1.0",
|
|
33
|
+
"@vue/test-utils": "^2.4.0",
|
|
34
|
+
"eslint": "^8.57.0",
|
|
35
|
+
"eslint-plugin-vue": "^9.27.0",
|
|
36
|
+
"jsdom": "^24.1.0",
|
|
37
|
+
"vite": "^5.4.0",
|
|
38
|
+
"vitest": "^2.0.0",
|
|
39
|
+
"vue": "^3.4.0"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"vue",
|
|
43
|
+
"vue3",
|
|
44
|
+
"table",
|
|
45
|
+
"slot-table",
|
|
46
|
+
"slots",
|
|
47
|
+
"scoped-slots",
|
|
48
|
+
"sticky-columns",
|
|
49
|
+
"column-groups",
|
|
50
|
+
"data-table",
|
|
51
|
+
"component"
|
|
52
|
+
],
|
|
53
|
+
"license": "MIT",
|
|
54
|
+
"repository": {
|
|
55
|
+
"type": "git",
|
|
56
|
+
"url": "git+https://github.com/nickvdyck/vue-slot-table.git"
|
|
57
|
+
},
|
|
58
|
+
"homepage": "https://github.com/nickvdyck/vue-slot-table#readme",
|
|
59
|
+
"bugs": {
|
|
60
|
+
"url": "https://github.com/nickvdyck/vue-slot-table/issues"
|
|
61
|
+
}
|
|
62
|
+
}
|
package/src/App.vue
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import { SlotTable, SlotTableColumn, SlotTableColumnGroup } from './index.js'
|
|
4
|
+
|
|
5
|
+
const rows = ref([
|
|
6
|
+
{ id: 1, name: 'Alice Johnson', age: 28, role: 'Engineer', department: 'Frontend', salary: 95000 },
|
|
7
|
+
{ id: 2, name: 'Bob Smith', age: 34, role: 'Designer', department: 'UX', salary: 88000 },
|
|
8
|
+
{ id: 3, name: 'Carol Williams', age: 41, role: 'Manager', department: 'Engineering', salary: 120000 },
|
|
9
|
+
{ id: 4, name: 'David Brown', age: 25, role: 'Intern', department: 'Backend', salary: 45000 },
|
|
10
|
+
{ id: 5, name: 'Eva Martinez', age: 31, role: 'Engineer', department: 'Backend', salary: 102000 },
|
|
11
|
+
])
|
|
12
|
+
|
|
13
|
+
const lastClickedRow = ref(null)
|
|
14
|
+
|
|
15
|
+
function onRowClick(rowIndex) {
|
|
16
|
+
lastClickedRow.value = rowIndex
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function formatSalary(value) {
|
|
20
|
+
return `$${value.toLocaleString()}`
|
|
21
|
+
}
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<div class="demo">
|
|
26
|
+
<h1>Vue SlotTable Demo</h1>
|
|
27
|
+
<p class="subtitle">A flexible, slot-based table component for Vue 3</p>
|
|
28
|
+
|
|
29
|
+
<div class="card">
|
|
30
|
+
<h2>Employee Directory</h2>
|
|
31
|
+
<p v-if="lastClickedRow !== null" class="click-info">
|
|
32
|
+
Last clicked row: {{ lastClickedRow }} ({{ rows[lastClickedRow].name }})
|
|
33
|
+
</p>
|
|
34
|
+
|
|
35
|
+
<div class="table-wrapper">
|
|
36
|
+
<SlotTable :rows="rows" table-class="demo-table" @row-click="onRowClick">
|
|
37
|
+
<SlotTableColumnGroup sticky="left" :colspan="2">
|
|
38
|
+
Identity
|
|
39
|
+
</SlotTableColumnGroup>
|
|
40
|
+
<SlotTableColumnGroup :colspan="2">
|
|
41
|
+
Position
|
|
42
|
+
</SlotTableColumnGroup>
|
|
43
|
+
<SlotTableColumnGroup sticky="right" :colspan="1">
|
|
44
|
+
Compensation
|
|
45
|
+
</SlotTableColumnGroup>
|
|
46
|
+
|
|
47
|
+
<SlotTableColumn sticky="left">
|
|
48
|
+
<template #header>Name</template>
|
|
49
|
+
<template #cell="{ row }">
|
|
50
|
+
<strong>{{ row.name }}</strong>
|
|
51
|
+
</template>
|
|
52
|
+
</SlotTableColumn>
|
|
53
|
+
|
|
54
|
+
<SlotTableColumn sticky="left">
|
|
55
|
+
<template #header>Age</template>
|
|
56
|
+
<template #cell="{ row }">{{ row.age }}</template>
|
|
57
|
+
</SlotTableColumn>
|
|
58
|
+
|
|
59
|
+
<SlotTableColumn>
|
|
60
|
+
<template #header>Role</template>
|
|
61
|
+
<template #cell="{ row }">
|
|
62
|
+
<span class="badge" :class="'badge--' + row.role.toLowerCase()">
|
|
63
|
+
{{ row.role }}
|
|
64
|
+
</span>
|
|
65
|
+
</template>
|
|
66
|
+
</SlotTableColumn>
|
|
67
|
+
|
|
68
|
+
<SlotTableColumn>
|
|
69
|
+
<template #header>Department</template>
|
|
70
|
+
<template #cell="{ row }">{{ row.department }}</template>
|
|
71
|
+
</SlotTableColumn>
|
|
72
|
+
|
|
73
|
+
<SlotTableColumn sticky="right">
|
|
74
|
+
<template #header>Salary</template>
|
|
75
|
+
<template #cell="{ row }">
|
|
76
|
+
<span class="salary">{{ formatSalary(row.salary) }}</span>
|
|
77
|
+
</template>
|
|
78
|
+
</SlotTableColumn>
|
|
79
|
+
</SlotTable>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</template>
|
|
84
|
+
|
|
85
|
+
<style>
|
|
86
|
+
* {
|
|
87
|
+
box-sizing: border-box;
|
|
88
|
+
margin: 0;
|
|
89
|
+
padding: 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
body {
|
|
93
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
94
|
+
background: #f5f5f5;
|
|
95
|
+
color: #333;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.demo {
|
|
99
|
+
max-width: 960px;
|
|
100
|
+
margin: 0 auto;
|
|
101
|
+
padding: 2rem;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
h1 {
|
|
105
|
+
font-size: 2rem;
|
|
106
|
+
margin-bottom: 0.25rem;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.subtitle {
|
|
110
|
+
color: #666;
|
|
111
|
+
margin-bottom: 2rem;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.card {
|
|
115
|
+
background: white;
|
|
116
|
+
border-radius: 8px;
|
|
117
|
+
padding: 1.5rem;
|
|
118
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.card h2 {
|
|
122
|
+
margin-bottom: 0.75rem;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.click-info {
|
|
126
|
+
color: #0066cc;
|
|
127
|
+
font-size: 0.875rem;
|
|
128
|
+
margin-bottom: 0.75rem;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.table-wrapper {
|
|
132
|
+
overflow-x: auto;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.demo-table {
|
|
136
|
+
width: 100%;
|
|
137
|
+
border-collapse: collapse;
|
|
138
|
+
font-size: 0.9rem;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.demo-table th,
|
|
142
|
+
.demo-table td {
|
|
143
|
+
padding: 0.625rem 1rem;
|
|
144
|
+
text-align: left;
|
|
145
|
+
border-bottom: 1px solid #e0e0e0;
|
|
146
|
+
background: white;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.demo-table thead th {
|
|
150
|
+
font-weight: 600;
|
|
151
|
+
color: #555;
|
|
152
|
+
border-bottom: 2px solid #ccc;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.demo-table tbody tr {
|
|
156
|
+
cursor: pointer;
|
|
157
|
+
transition: background 0.15s;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.demo-table tbody tr:hover {
|
|
161
|
+
background: #f0f7ff;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.demo-table tbody tr:hover td {
|
|
165
|
+
background: #f0f7ff;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.badge {
|
|
169
|
+
display: inline-block;
|
|
170
|
+
padding: 0.2rem 0.6rem;
|
|
171
|
+
border-radius: 12px;
|
|
172
|
+
font-size: 0.8rem;
|
|
173
|
+
font-weight: 500;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.badge--engineer {
|
|
177
|
+
background: #e3f2fd;
|
|
178
|
+
color: #1565c0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.badge--designer {
|
|
182
|
+
background: #fce4ec;
|
|
183
|
+
color: #c62828;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.badge--manager {
|
|
187
|
+
background: #e8f5e9;
|
|
188
|
+
color: #2e7d32;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.badge--intern {
|
|
192
|
+
background: #fff3e0;
|
|
193
|
+
color: #e65100;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.salary {
|
|
197
|
+
font-variant-numeric: tabular-nums;
|
|
198
|
+
font-weight: 500;
|
|
199
|
+
}
|
|
200
|
+
</style>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { defineComponent, h } from 'vue'
|
|
3
|
+
import SlotTableColumn from './SlotTableColumn.vue'
|
|
4
|
+
import SlotTableColumnGroup from './SlotTableColumnGroup.vue'
|
|
5
|
+
|
|
6
|
+
export default defineComponent({
|
|
7
|
+
name: 'SlotTable',
|
|
8
|
+
props: {
|
|
9
|
+
rows: {
|
|
10
|
+
type: Array,
|
|
11
|
+
required: true,
|
|
12
|
+
},
|
|
13
|
+
tableClass: {
|
|
14
|
+
type: String,
|
|
15
|
+
default: '',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
emits: ['row-click'],
|
|
19
|
+
setup(props, { slots, emit }) {
|
|
20
|
+
return () => {
|
|
21
|
+
const children = slots.default ? slots.default() : []
|
|
22
|
+
|
|
23
|
+
const columns = []
|
|
24
|
+
const columnGroups = []
|
|
25
|
+
|
|
26
|
+
children.forEach((vnode) => {
|
|
27
|
+
if (vnode.type === SlotTableColumn) {
|
|
28
|
+
columns.push(vnode)
|
|
29
|
+
} else if (vnode.type === SlotTableColumnGroup) {
|
|
30
|
+
columnGroups.push(vnode)
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// Build header cells
|
|
35
|
+
const headerCells = columns.map((col) => {
|
|
36
|
+
const colProps = col.props || {}
|
|
37
|
+
const sticky = colProps.sticky || ''
|
|
38
|
+
const classes = {
|
|
39
|
+
'sticky-left': sticky === 'left',
|
|
40
|
+
'sticky-right': sticky === 'right',
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const headerSlot = col.children && col.children.header
|
|
44
|
+
const headerContent = typeof headerSlot === 'function' ? headerSlot() : null
|
|
45
|
+
|
|
46
|
+
return h('th', { class: classes }, headerContent)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// Build group header row
|
|
50
|
+
const groupHeaderCells = columnGroups.map((group) => {
|
|
51
|
+
const groupProps = group.props || {}
|
|
52
|
+
const sticky = groupProps.sticky || ''
|
|
53
|
+
const colspan = groupProps.colspan ? Number(groupProps.colspan) : undefined
|
|
54
|
+
const classes = {
|
|
55
|
+
'sticky-left': sticky === 'left',
|
|
56
|
+
'sticky-right': sticky === 'right',
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const defaultSlot = group.children && group.children.default
|
|
60
|
+
const content = typeof defaultSlot === 'function' ? defaultSlot() : null
|
|
61
|
+
|
|
62
|
+
return h('th', { class: classes, colspan }, content)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// Build thead
|
|
66
|
+
const headerRows = []
|
|
67
|
+
if (groupHeaderCells.length > 0) {
|
|
68
|
+
headerRows.push(h('tr', null, groupHeaderCells))
|
|
69
|
+
}
|
|
70
|
+
headerRows.push(h('tr', null, headerCells))
|
|
71
|
+
const thead = h('thead', null, headerRows)
|
|
72
|
+
|
|
73
|
+
// Build tbody
|
|
74
|
+
const bodyRows = props.rows.map((row, rowIndex) => {
|
|
75
|
+
const cells = columns.map((col, columnIndex) => {
|
|
76
|
+
const colProps = col.props || {}
|
|
77
|
+
const sticky = colProps.sticky || ''
|
|
78
|
+
const classes = {
|
|
79
|
+
'sticky-left': sticky === 'left',
|
|
80
|
+
'sticky-right': sticky === 'right',
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const cellSlot = col.children && col.children.cell
|
|
84
|
+
const cellContent = typeof cellSlot === 'function'
|
|
85
|
+
? cellSlot({ row, rowIndex, columnIndex })
|
|
86
|
+
: null
|
|
87
|
+
|
|
88
|
+
return h('td', { class: classes }, cellContent)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
return h('tr', {
|
|
92
|
+
onClick: () => emit('row-click', rowIndex),
|
|
93
|
+
}, cells)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
const tbody = h('tbody', null, bodyRows)
|
|
97
|
+
|
|
98
|
+
return h('table', { class: props.tableClass }, [thead, tbody])
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
})
|
|
102
|
+
</script>
|
|
103
|
+
|
|
104
|
+
<style>
|
|
105
|
+
.sticky-left {
|
|
106
|
+
position: sticky;
|
|
107
|
+
left: 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.sticky-right {
|
|
111
|
+
position: sticky;
|
|
112
|
+
right: 0;
|
|
113
|
+
}
|
|
114
|
+
</style>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { defineComponent } from 'vue'
|
|
3
|
+
|
|
4
|
+
export default defineComponent({
|
|
5
|
+
name: 'SlotTableColumnGroup',
|
|
6
|
+
props: {
|
|
7
|
+
sticky: {
|
|
8
|
+
type: String,
|
|
9
|
+
default: '',
|
|
10
|
+
},
|
|
11
|
+
colspan: {
|
|
12
|
+
type: Number,
|
|
13
|
+
default: undefined,
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
setup() {
|
|
17
|
+
return () => null
|
|
18
|
+
},
|
|
19
|
+
})
|
|
20
|
+
</script>
|
package/src/index.js
ADDED
package/src/main.js
ADDED
package/src/types.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { DefineComponent } from 'vue'
|
|
2
|
+
|
|
3
|
+
export interface CellSlotScope<T = any> {
|
|
4
|
+
row: T
|
|
5
|
+
rowIndex: number
|
|
6
|
+
columnIndex: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface SlotTableProps {
|
|
10
|
+
rows: any[]
|
|
11
|
+
tableClass?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SlotTableColumnProps {
|
|
15
|
+
sticky?: '' | 'left' | 'right'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SlotTableColumnGroupProps {
|
|
19
|
+
sticky?: '' | 'left' | 'right'
|
|
20
|
+
colspan?: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export declare const SlotTable: DefineComponent<SlotTableProps>
|
|
24
|
+
export declare const SlotTableColumn: DefineComponent<SlotTableColumnProps>
|
|
25
|
+
export declare const SlotTableColumnGroup: DefineComponent<SlotTableColumnGroupProps>
|