• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

humanspeak / svelte-headless-table / 12913869200

22 Jan 2025 05:36PM UTC coverage: 34.206% (-25.6%) from 59.766%
12913869200

push

github

web-flow
Merge pull request #9 from humanspeak/feature-version

Feature: 5.x

404 of 538 branches covered (75.09%)

Branch coverage included in aggregate %.

2129 of 3542 new or added lines in 41 files covered. (60.11%)

1 existing line in 1 file now uncovered.

2639 of 8358 relevant lines covered (31.57%)

17.47 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.27
/src/lib/createViewModel.ts
1
import { derived, readable, writable, type Readable, type Writable } from 'svelte/store'
1✔
2
import { BodyRow, getBodyRows, getColumnedBodyRows } from './bodyRows.js'
1✔
3
import { FlatColumn, getFlatColumns, type Column } from './columns.js'
1✔
4
import type { Table } from './createTable.js'
1✔
5
import { getHeaderRows, HeaderRow } from './headerRows.js'
1✔
6
import type {
1✔
7
    AnyPlugins,
1✔
8
    DeriveFlatColumnsFn,
1✔
9
    DeriveRowsFn,
1✔
10
    DeriveFn,
1✔
11
    PluginStates
1✔
12
} from './types/TablePlugin.js'
1✔
13
import { finalizeAttributes } from './utils/attributes.js'
1✔
14
import { nonUndefined } from './utils/filter.js'
1✔
15

1✔
16
// eslint-disable-next-line @typescript-eslint/no-unused-vars
1✔
17
export type TableAttributes<Item, Plugins extends AnyPlugins = AnyPlugins> = Record<
1✔
18
    string,
1✔
19
    unknown
1✔
20
> & {
1✔
21
    role: 'table'
1✔
22
}
1✔
23

1✔
24
// eslint-disable-next-line @typescript-eslint/no-unused-vars
1✔
25
export type TableHeadAttributes<Item, Plugins extends AnyPlugins = AnyPlugins> = Record<
1✔
26
    string,
1✔
27
    unknown
1✔
28
>
1✔
29

1✔
30
// eslint-disable-next-line @typescript-eslint/no-unused-vars
1✔
31
export type TableBodyAttributes<Item, Plugins extends AnyPlugins = AnyPlugins> = Record<
1✔
32
    string,
1✔
33
    unknown
1✔
34
> & {
1✔
35
    role: 'rowgroup'
1✔
36
}
1✔
37

1✔
38
export interface TableViewModel<Item, Plugins extends AnyPlugins = AnyPlugins> {
1✔
39
    flatColumns: FlatColumn<Item, Plugins>[]
1✔
40
    tableAttrs: Readable<TableAttributes<Item, Plugins>>
1✔
41
    tableHeadAttrs: Readable<TableHeadAttributes<Item, Plugins>>
1✔
42
    tableBodyAttrs: Readable<TableBodyAttributes<Item, Plugins>>
1✔
43
    visibleColumns: Readable<FlatColumn<Item, Plugins>[]>
1✔
44
    headerRows: Readable<HeaderRow<Item, Plugins>[]>
1✔
45
    originalRows: Readable<BodyRow<Item, Plugins>[]>
1✔
46
    rows: Readable<BodyRow<Item, Plugins>[]>
1✔
47
    pageRows: Readable<BodyRow<Item, Plugins>[]>
1✔
48
    pluginStates: PluginStates<Plugins>
1✔
49
}
1✔
50

1✔
51
export type ReadOrWritable<T> = Readable<T> | Writable<T>
1✔
52
export interface PluginInitTableState<Item, Plugins extends AnyPlugins = AnyPlugins>
1✔
53
    extends Omit<TableViewModel<Item, Plugins>, 'pluginStates'> {
1✔
54
    data: ReadOrWritable<Item[]>
1✔
55
    columns: Column<Item, Plugins>[]
1✔
56
}
1✔
57

1✔
58
export interface TableState<Item, Plugins extends AnyPlugins = AnyPlugins>
1✔
59
    extends TableViewModel<Item, Plugins> {
1✔
60
    data: ReadOrWritable<Item[]>
1✔
61
    columns: Column<Item, Plugins>[]
1✔
62
}
1✔
63

1✔
64
export interface CreateViewModelOptions<Item> {
1✔
65
    rowDataId?: (item: Item, index: number) => string
1✔
66
}
1✔
67

1✔
68
export const createViewModel = <Item, Plugins extends AnyPlugins = AnyPlugins>(
1✔
69
    table: Table<Item, Plugins>,
13✔
70
    columns: Column<Item, Plugins>[],
13✔
71
    { rowDataId }: CreateViewModelOptions<Item> = {}
13✔
72
): TableViewModel<Item, Plugins> => {
13✔
73
    const { data, plugins } = table
13✔
74

13✔
75
    const $flatColumns = getFlatColumns(columns)
13✔
76
    const flatColumns = readable($flatColumns)
13✔
77

13✔
78
    const originalRows = derived([data, flatColumns], ([$data, $flatColumns]) => {
13✔
79
        return getBodyRows($data, $flatColumns, { rowDataId })
24✔
80
    })
13✔
81

13✔
82
    // _stores need to be defined first to pass into plugins for initialization.
13✔
83
    const _visibleColumns = writable<FlatColumn<Item, Plugins>[]>([])
13✔
84
    const _headerRows = writable<HeaderRow<Item, Plugins>[]>()
13✔
85
    const _rows = writable<BodyRow<Item, Plugins>[]>([])
13✔
86
    const _pageRows = writable<BodyRow<Item, Plugins>[]>([])
13✔
87
    const _tableAttrs = writable<TableAttributes<Item>>({
13✔
88
        role: 'table' as const
13✔
89
    })
13✔
90
    const _tableHeadAttrs = writable<TableHeadAttributes<Item>>({})
13✔
91
    const _tableBodyAttrs = writable<TableBodyAttributes<Item>>({
13✔
92
        role: 'rowgroup' as const
13✔
93
    })
13✔
94
    const pluginInitTableState: PluginInitTableState<Item, Plugins> = {
13✔
95
        data,
13✔
96
        columns,
13✔
97
        flatColumns: $flatColumns,
13✔
98
        tableAttrs: _tableAttrs,
13✔
99
        tableHeadAttrs: _tableHeadAttrs,
13✔
100
        tableBodyAttrs: _tableBodyAttrs,
13✔
101
        visibleColumns: _visibleColumns,
13✔
102
        headerRows: _headerRows,
13✔
103
        originalRows,
13✔
104
        rows: _rows,
13✔
105
        pageRows: _pageRows
13✔
106
    }
13✔
107

13✔
108
    const pluginInstances = Object.fromEntries(
13✔
109
        Object.entries(plugins).map(([pluginName, plugin]) => {
13✔
110
            const columnOptions = Object.fromEntries(
24✔
111
                $flatColumns
24✔
112
                    .map((c) => {
24✔
113
                        const option = c.plugins?.[pluginName]
45✔
114
                        if (option === undefined) return undefined
45✔
115
                        return [c.id, option] as const
1✔
116
                    })
24✔
117
                    .filter(nonUndefined)
24✔
118
            )
24✔
119
            return [
24✔
120
                pluginName,
24✔
121
                // eslint-disable-next-line @typescript-eslint/no-explicit-any
24✔
122
                plugin({ pluginName, tableState: pluginInitTableState as any, columnOptions })
24✔
123
            ]
24✔
124
        })
13✔
125
    ) as {
13✔
126
        [K in keyof Plugins]: ReturnType<Plugins[K]>
13✔
127
    }
13✔
128

13✔
129
    const pluginStates = Object.fromEntries(
13✔
130
        Object.entries(pluginInstances).map(([key, pluginInstance]) => [
13✔
131
            key,
24✔
132
            pluginInstance.pluginState
24✔
133
        ])
13✔
134
    ) as PluginStates<Plugins>
13✔
135

13✔
136
    const tableState: TableState<Item, Plugins> = {
13✔
137
        data,
13✔
138
        columns,
13✔
139
        flatColumns: $flatColumns,
13✔
140
        tableAttrs: _tableAttrs,
13✔
141
        tableHeadAttrs: _tableHeadAttrs,
13✔
142
        tableBodyAttrs: _tableBodyAttrs,
13✔
143
        visibleColumns: _visibleColumns,
13✔
144
        headerRows: _headerRows,
13✔
145
        originalRows,
13✔
146
        rows: _rows,
13✔
147
        pageRows: _pageRows,
13✔
148
        pluginStates
13✔
149
    }
13✔
150

13✔
151
    const deriveTableAttrsFns: DeriveFn<TableAttributes<Item>>[] = Object.values(pluginInstances)
13✔
152
        .map((pluginInstance) => pluginInstance.deriveTableAttrs)
13✔
153
        .filter(nonUndefined)
13✔
154
    let tableAttrs = readable<TableAttributes<Item>>({
13✔
155
        role: 'table'
13✔
156
    })
13✔
157
    deriveTableAttrsFns.forEach((fn) => {
13✔
NEW
158
        tableAttrs = fn(tableAttrs)
×
159
    })
13✔
160
    const finalizedTableAttrs = derived(tableAttrs, ($tableAttrs) => {
13✔
NEW
161
        const $finalizedAttrs = finalizeAttributes($tableAttrs) as TableAttributes<Item>
×
NEW
162
        _tableAttrs.set($finalizedAttrs)
×
NEW
163
        return $finalizedAttrs
×
164
    })
13✔
165

13✔
166
    const deriveTableHeadAttrsFns: DeriveFn<TableHeadAttributes<Item>>[] = Object.values(
13✔
167
        pluginInstances
13✔
168
    )
13✔
169
        .map((pluginInstance) => pluginInstance.deriveTableBodyAttrs)
13✔
170
        .filter(nonUndefined)
13✔
171
    let tableHeadAttrs = readable<TableHeadAttributes<Item>>({})
13✔
172
    deriveTableHeadAttrsFns.forEach((fn) => {
13✔
NEW
173
        tableHeadAttrs = fn(tableHeadAttrs)
×
174
    })
13✔
175
    const finalizedTableHeadAttrs = derived(tableHeadAttrs, ($tableHeadAttrs) => {
13✔
NEW
176
        const $finalizedAttrs = finalizeAttributes($tableHeadAttrs) as TableHeadAttributes<Item>
×
NEW
177
        _tableHeadAttrs.set($finalizedAttrs)
×
NEW
178
        return $finalizedAttrs
×
179
    })
13✔
180

13✔
181
    const deriveTableBodyAttrsFns: DeriveFn<TableBodyAttributes<Item>>[] = Object.values(
13✔
182
        pluginInstances
13✔
183
    )
13✔
184
        .map((pluginInstance) => pluginInstance.deriveTableBodyAttrs)
13✔
185
        .filter(nonUndefined)
13✔
186
    let tableBodyAttrs = readable<TableBodyAttributes<Item>>({
13✔
187
        role: 'rowgroup'
13✔
188
    })
13✔
189
    deriveTableBodyAttrsFns.forEach((fn) => {
13✔
NEW
190
        tableBodyAttrs = fn(tableBodyAttrs)
×
191
    })
13✔
192
    const finalizedTableBodyAttrs = derived(tableBodyAttrs, ($tableBodyAttrs) => {
13✔
NEW
193
        const $finalizedAttrs = finalizeAttributes($tableBodyAttrs) as TableBodyAttributes<Item>
×
NEW
194
        _tableBodyAttrs.set($finalizedAttrs)
×
NEW
195
        return $finalizedAttrs
×
196
    })
13✔
197

13✔
198
    const deriveFlatColumnsFns: DeriveFlatColumnsFn<Item>[] = Object.values(pluginInstances)
13✔
199
        .map((pluginInstance) => pluginInstance.deriveFlatColumns)
13✔
200
        .filter(nonUndefined)
13✔
201

13✔
202
    let visibleColumns = flatColumns
13✔
203
    deriveFlatColumnsFns.forEach((fn) => {
13✔
NEW
204
        // Variance of generic type here is unstable. Not sure how to fix.
×
NEW
205
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
×
NEW
206
        visibleColumns = fn(visibleColumns as any) as any
×
207
    })
13✔
208

13✔
209
    const injectedColumns = derived(visibleColumns, ($visibleColumns) => {
13✔
210
        _visibleColumns.set($visibleColumns)
24✔
211
        return $visibleColumns
24✔
212
    })
13✔
213

13✔
214
    const columnedRows = derived(
13✔
215
        [originalRows, injectedColumns],
13✔
216
        ([$originalRows, $injectedColumns]) => {
13✔
217
            return getColumnedBodyRows(
24✔
218
                $originalRows,
24✔
219
                $injectedColumns.map((c) => c.id)
24✔
220
            )
24✔
221
        }
24✔
222
    )
13✔
223

13✔
224
    const deriveRowsFns: DeriveRowsFn<Item>[] = Object.values(pluginInstances)
13✔
225
        .map((pluginInstance) => pluginInstance.deriveRows)
13✔
226
        .filter(nonUndefined)
13✔
227

13✔
228
    let rows = columnedRows
13✔
229
    deriveRowsFns.forEach((fn) => {
13✔
230
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
16✔
231
        rows = fn(rows as any) as any
16✔
232
    })
13✔
233

13✔
234
    const injectedRows = derived(rows, ($rows) => {
13✔
235
        // Inject state.
24✔
236
        $rows.forEach((row) => {
24✔
237
            row.injectState(tableState)
110✔
238
            row.cells.forEach((cell) => {
110✔
239
                cell.injectState(tableState)
208✔
240
            })
110✔
241
        })
24✔
242
        // Apply plugin component hooks.
24✔
243
        Object.entries(pluginInstances).forEach(([pluginName, pluginInstance]) => {
24✔
244
            $rows.forEach((row) => {
53✔
245
                if (pluginInstance.hooks?.['tbody.tr'] !== undefined) {
250✔
246
                    row.applyHook(pluginName, pluginInstance.hooks['tbody.tr'](row))
85✔
247
                }
85✔
248
                row.cells.forEach((cell) => {
250✔
249
                    if (pluginInstance.hooks?.['tbody.tr.td'] !== undefined) {
488✔
250
                        cell.applyHook(pluginName, pluginInstance.hooks['tbody.tr.td'](cell))
38✔
251
                    }
38✔
252
                })
250✔
253
            })
53✔
254
        })
24✔
255
        _rows.set($rows)
24✔
256
        return $rows
24✔
257
    })
13✔
258

13✔
259
    const derivePageRowsFns: DeriveRowsFn<Item>[] = Object.values(pluginInstances)
13✔
260
        .map((pluginInstance) => pluginInstance.derivePageRows)
13✔
261
        .filter(nonUndefined)
13✔
262

13✔
263
    // Must derive from `injectedRows` instead of `rows` to ensure that `_rows` is set.
13✔
264
    let pageRows = injectedRows
13✔
265
    derivePageRowsFns.forEach((fn) => {
13✔
266
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
2✔
267
        pageRows = fn(pageRows as any) as any
2✔
268
    })
13✔
269

13✔
270
    const injectedPageRows = derived(pageRows, ($pageRows) => {
13✔
271
        // Inject state.
9✔
272
        $pageRows.forEach((row) => {
9✔
273
            row.injectState(tableState)
18✔
274
            row.cells.forEach((cell) => {
18✔
275
                cell.injectState(tableState)
36✔
276
            })
18✔
277
        })
9✔
278
        // Apply plugin component hooks.
9✔
279
        Object.entries(pluginInstances).forEach(([pluginName, pluginInstance]) => {
9✔
280
            $pageRows.forEach((row) => {
27✔
281
                if (pluginInstance.hooks?.['tbody.tr'] !== undefined) {
54✔
282
                    row.applyHook(pluginName, pluginInstance.hooks['tbody.tr'](row))
18✔
283
                }
18✔
284
                row.cells.forEach((cell) => {
54✔
285
                    if (pluginInstance.hooks?.['tbody.tr.td'] !== undefined) {
108!
NEW
286
                        cell.applyHook(pluginName, pluginInstance.hooks['tbody.tr.td'](cell))
×
NEW
287
                    }
×
288
                })
54✔
289
            })
27✔
290
        })
9✔
291
        _pageRows.set($pageRows)
9✔
292
        return $pageRows
9✔
293
    })
13✔
294

13✔
295
    const headerRows = derived(injectedColumns, ($injectedColumns) => {
13✔
NEW
296
        const $headerRows = getHeaderRows(
×
NEW
297
            columns,
×
NEW
298
            $injectedColumns.map((c) => c.id)
×
NEW
299
        )
×
NEW
300
        // Inject state.
×
NEW
301
        $headerRows.forEach((row) => {
×
NEW
302
            row.injectState(tableState)
×
NEW
303
            row.cells.forEach((cell) => {
×
NEW
304
                cell.injectState(tableState)
×
NEW
305
            })
×
NEW
306
        })
×
NEW
307
        // Apply plugin component hooks.
×
NEW
308
        Object.entries(pluginInstances).forEach(([pluginName, pluginInstance]) => {
×
NEW
309
            $headerRows.forEach((row) => {
×
NEW
310
                if (pluginInstance.hooks?.['thead.tr'] !== undefined) {
×
NEW
311
                    row.applyHook(pluginName, pluginInstance.hooks['thead.tr'](row))
×
NEW
312
                }
×
NEW
313
                row.cells.forEach((cell) => {
×
NEW
314
                    if (pluginInstance.hooks?.['thead.tr.th'] !== undefined) {
×
NEW
315
                        cell.applyHook(pluginName, pluginInstance.hooks['thead.tr.th'](cell))
×
NEW
316
                    }
×
NEW
317
                })
×
NEW
318
            })
×
NEW
319
        })
×
NEW
320
        _headerRows.set($headerRows)
×
NEW
321
        return $headerRows
×
322
    })
13✔
323

13✔
324
    return {
13✔
325
        tableAttrs: finalizedTableAttrs,
13✔
326
        tableHeadAttrs: finalizedTableHeadAttrs,
13✔
327
        tableBodyAttrs: finalizedTableBodyAttrs,
13✔
328
        visibleColumns: injectedColumns,
13✔
329
        flatColumns: $flatColumns,
13✔
330
        headerRows,
13✔
331
        originalRows,
13✔
332
        rows: injectedRows,
13✔
333
        pageRows: injectedPageRows,
13✔
334
        pluginStates
13✔
335
    }
13✔
336
}
13✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc