@zoobzio/table 0.0.0-stage → 0.0.1
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/.dist/index.d.mts +328 -0
- package/.dist/index.mjs +322 -0
- package/LICENSE +21 -0
- package/README.md +78 -2
- package/package.json +39 -3
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { Entry, Filter, Service as Service$1 } from "@zoobzio/query";
|
|
2
|
+
import { Change as Change$1, Changers as Changers$1, Changes as Changes$1, Listener as Listener$1, Middleware as Middleware$1, Options as Options$1 } from "@zoobzio/kit";
|
|
3
|
+
/**
|
|
4
|
+
* Column data type — drives cell rendering and sort behavior.
|
|
5
|
+
*/
|
|
6
|
+
export type ColumnType = "text" | "number" | "date" | "datetime" | "boolean" | "enum" | "currency" | "filesize" | "url" | "image" | "action";
|
|
7
|
+
export interface DataTableColumn<T> {
|
|
8
|
+
key: keyof T;
|
|
9
|
+
label: string;
|
|
10
|
+
type?: ColumnType;
|
|
11
|
+
align?: "left" | "center" | "right";
|
|
12
|
+
sortable?: boolean;
|
|
13
|
+
sortKey?: string;
|
|
14
|
+
enumValues?: string[];
|
|
15
|
+
/**
|
|
16
|
+
* Whether the column appears in the search autocomplete. Defaults by
|
|
17
|
+
* type: everything except `action` and `image` is filterable.
|
|
18
|
+
*/
|
|
19
|
+
filterable?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A committed column filter, as the query holds it. Keys are strings,
|
|
23
|
+
* matching the `sortField` / `columnOrder` convention; `op` is present for
|
|
24
|
+
* operator columns (date/datetime: before/after/on, number/currency:
|
|
25
|
+
* over/under/is).
|
|
26
|
+
*/
|
|
27
|
+
export type TableFilter = Filter;
|
|
28
|
+
export type SortDirection = "asc" | "desc";
|
|
29
|
+
/**
|
|
30
|
+
* The serializable half of a row/bulk action: how it renders. The behavior
|
|
31
|
+
* half is a handler registered under the same key in `Actions`.
|
|
32
|
+
*/
|
|
33
|
+
export interface ActionDescriptor {
|
|
34
|
+
label: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Row action — the live shape the service exposes: descriptor joined with
|
|
38
|
+
* its handler, under the config key. `runAction(key, row)` dispatches by
|
|
39
|
+
* key.
|
|
40
|
+
*/
|
|
41
|
+
export interface RowAction<T> {
|
|
42
|
+
key: string;
|
|
43
|
+
label: string;
|
|
44
|
+
action: (row: T) => void;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Bulk action — the live shape the service exposes: descriptor joined with
|
|
48
|
+
* its handler, under the config key. `runBulkAction(key)` dispatches by key
|
|
49
|
+
* over the service's selection.
|
|
50
|
+
*/
|
|
51
|
+
export interface BulkAction {
|
|
52
|
+
key: string;
|
|
53
|
+
label: string;
|
|
54
|
+
action: (selected: Set<string>) => void;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Parameters passed to the fetch action. The table owns paging and sort;
|
|
58
|
+
* its query owns the search: `query` is the committed free text, `filters`
|
|
59
|
+
* the committed column filters.
|
|
60
|
+
*/
|
|
61
|
+
export interface DataTableFetchParams {
|
|
62
|
+
page: number;
|
|
63
|
+
pageSize: number;
|
|
64
|
+
sortField: string | null;
|
|
65
|
+
sortDirection: SortDirection;
|
|
66
|
+
query: string;
|
|
67
|
+
filters: TableFilter[];
|
|
68
|
+
}
|
|
69
|
+
export interface DataTableFetchResult<T> {
|
|
70
|
+
data: T[];
|
|
71
|
+
total: number;
|
|
72
|
+
pageCount: number;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Config the consumer provides — pure serializable data. Actions are
|
|
76
|
+
* descriptor records: the key is the action's identity, shared with the
|
|
77
|
+
* handler record in `Actions`.
|
|
78
|
+
*/
|
|
79
|
+
export type Config<T> = {
|
|
80
|
+
columns: DataTableColumn<T>[];
|
|
81
|
+
rowKey: keyof T;
|
|
82
|
+
actions?: Record<string, ActionDescriptor>;
|
|
83
|
+
bulkActions?: Record<string, ActionDescriptor>;
|
|
84
|
+
pinnedColumns?: (keyof T)[];
|
|
85
|
+
defaultColumnOrder?: (keyof T)[];
|
|
86
|
+
/**
|
|
87
|
+
* Whether a UI renders the toolbar search. Defaults to `true`. A hint for
|
|
88
|
+
* the view layer only — the search surface on the service (`query`,
|
|
89
|
+
* `filters`, mutators, fetch params) stays live for imperative use.
|
|
90
|
+
*/
|
|
91
|
+
searchable?: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* The BCP 47 locale cells format in. Defaults to `en-US`.
|
|
94
|
+
*/
|
|
95
|
+
locale?: string;
|
|
96
|
+
/**
|
|
97
|
+
* The ISO 4217 code currency cells format in. Defaults to `USD`.
|
|
98
|
+
*/
|
|
99
|
+
currency?: string;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* The state container the service reads and writes: paging, sort,
|
|
103
|
+
* selection, column order, and the query's slice. The container can be a
|
|
104
|
+
* plain object or a reactive proxy. The service replaces each member as a
|
|
105
|
+
* whole, so a proxy tracks every change. `entries` and `draft` belong to
|
|
106
|
+
* the table's `query` machine, its only writer.
|
|
107
|
+
*/
|
|
108
|
+
export type State<T> = {
|
|
109
|
+
data: T[];
|
|
110
|
+
loading: boolean;
|
|
111
|
+
initialized: boolean;
|
|
112
|
+
page: number;
|
|
113
|
+
pageSize: number;
|
|
114
|
+
total: number;
|
|
115
|
+
pageCount: number;
|
|
116
|
+
sortField: string | null;
|
|
117
|
+
sortDirection: SortDirection;
|
|
118
|
+
selected: Set<string>;
|
|
119
|
+
columnOrder: string[];
|
|
120
|
+
entries: Entry[];
|
|
121
|
+
draft: string;
|
|
122
|
+
};
|
|
123
|
+
export type StateKey = keyof State<unknown>;
|
|
124
|
+
/**
|
|
125
|
+
* The kit's state mechanisms bound to this service's container: per-field
|
|
126
|
+
* middleware, the read/write options, and the change objects.
|
|
127
|
+
*/
|
|
128
|
+
export type Middleware<T> = Middleware$1<State<T>>;
|
|
129
|
+
export type Options<T> = Options$1<State<T>>;
|
|
130
|
+
export type Changes<T> = Changes$1<State<T>>;
|
|
131
|
+
export type Change<T, K extends StateKey = StateKey> = Change$1<State<T>, K>;
|
|
132
|
+
export type Changers<T> = Changers$1<State<T>>;
|
|
133
|
+
/**
|
|
134
|
+
* The consumer-supplied behavior: the fetch mechanism plus handlers keyed
|
|
135
|
+
* like the config's descriptor records.
|
|
136
|
+
*/
|
|
137
|
+
export type Actions<T> = {
|
|
138
|
+
fetch: (params: DataTableFetchParams, service: Service<T>) => Promise<DataTableFetchResult<T>>;
|
|
139
|
+
actions?: Record<string, (row: T) => void>;
|
|
140
|
+
bulkActions?: Record<string, (selected: Set<string>) => void>;
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* The events of a table service, keyed by name. Each value is the argument
|
|
144
|
+
* list of a listener. The service emits after the write lands, so a listener
|
|
145
|
+
* reads the new state.
|
|
146
|
+
*
|
|
147
|
+
* - `change`: any field of the container was written.
|
|
148
|
+
* - `updated`: a fetch settled, success or failure. `total` is the stored total.
|
|
149
|
+
* - `filtered`: the query's entries changed.
|
|
150
|
+
*/
|
|
151
|
+
export type Events<T> = {
|
|
152
|
+
change: [change: Change<T>];
|
|
153
|
+
updated: [event: {
|
|
154
|
+
id: string;
|
|
155
|
+
total: number;
|
|
156
|
+
}];
|
|
157
|
+
filtered: [event: {
|
|
158
|
+
id: string;
|
|
159
|
+
query: string;
|
|
160
|
+
filters: TableFilter[];
|
|
161
|
+
}];
|
|
162
|
+
};
|
|
163
|
+
export type Event<T> = keyof Events<T>;
|
|
164
|
+
export type Listener<T, E extends Event<T>> = Listener$1<Events<T>, E>;
|
|
165
|
+
export type Service<T> = {
|
|
166
|
+
readonly id: string;
|
|
167
|
+
readonly config: Config<T>;
|
|
168
|
+
readonly columns: DataTableColumn<T>[];
|
|
169
|
+
readonly rowKey: keyof T;
|
|
170
|
+
readonly actions: RowAction<T>[];
|
|
171
|
+
readonly bulkActions: BulkAction[];
|
|
172
|
+
readonly pinnedColumns: (keyof T)[];
|
|
173
|
+
readonly searchable: boolean;
|
|
174
|
+
readonly locale: string;
|
|
175
|
+
readonly currency: string;
|
|
176
|
+
/**
|
|
177
|
+
* The query machine over the filterable columns: one field per column,
|
|
178
|
+
* with the operators of its type and the values of an enum or boolean
|
|
179
|
+
* column. A change to its entries refetches from the first page.
|
|
180
|
+
*/
|
|
181
|
+
readonly query: Service$1;
|
|
182
|
+
readonly data: T[];
|
|
183
|
+
readonly loading: boolean;
|
|
184
|
+
readonly initialized: boolean;
|
|
185
|
+
readonly page: number;
|
|
186
|
+
readonly pageSize: number;
|
|
187
|
+
readonly total: number;
|
|
188
|
+
readonly pageCount: number;
|
|
189
|
+
readonly sortField: string | null;
|
|
190
|
+
readonly sortDirection: SortDirection;
|
|
191
|
+
readonly selected: Set<string>;
|
|
192
|
+
readonly columnOrder: string[];
|
|
193
|
+
readonly visibleColumns: DataTableColumn<T>[];
|
|
194
|
+
readonly filterableColumns: DataTableColumn<T>[];
|
|
195
|
+
readonly isAllSelected: boolean;
|
|
196
|
+
readonly isIndeterminate: boolean;
|
|
197
|
+
readonly selectAllState: boolean | "indeterminate";
|
|
198
|
+
readonly colSpan: number;
|
|
199
|
+
readonly hasSelection: boolean;
|
|
200
|
+
readonly isSelectable: boolean;
|
|
201
|
+
readonly hasActions: boolean;
|
|
202
|
+
goToPage(page: number): void;
|
|
203
|
+
setPageSize(size: number): void;
|
|
204
|
+
sortBy(field: string): void;
|
|
205
|
+
sortFieldFor(col: DataTableColumn<T>): string;
|
|
206
|
+
isSorted(col: DataTableColumn<T>): boolean;
|
|
207
|
+
keyOf(row: T): string;
|
|
208
|
+
toggleRow(key: string): void;
|
|
209
|
+
toggleAll(): void;
|
|
210
|
+
clearSelection(): void;
|
|
211
|
+
isRowSelected(row: T): boolean;
|
|
212
|
+
toggleColumn(key: keyof T): void;
|
|
213
|
+
setVisibleColumns(keys: Iterable<string>): void;
|
|
214
|
+
moveColumn(key: string, toKey: string): void;
|
|
215
|
+
reorderColumns(order: string[]): void;
|
|
216
|
+
resetColumns(): void;
|
|
217
|
+
isColumnPinned(key: keyof T): boolean;
|
|
218
|
+
isColumnVisible(key: keyof T): boolean;
|
|
219
|
+
runAction(key: string, row: T): void;
|
|
220
|
+
runBulkAction(key: string): void;
|
|
221
|
+
init(): Promise<boolean>;
|
|
222
|
+
fetch(): Promise<void>;
|
|
223
|
+
/**
|
|
224
|
+
* Subscribes a listener to an event. Listeners run in subscription order.
|
|
225
|
+
* An error in a listener propagates to the caller of the write. Returns a
|
|
226
|
+
* function that removes the listener.
|
|
227
|
+
*/
|
|
228
|
+
on<E extends Event<T>>(event: E, listener: Listener<T, E>): () => void;
|
|
229
|
+
};
|
|
230
|
+
export declare const TABLE_DEFAULT_PAGE_SIZE = 25;
|
|
231
|
+
export declare const TABLE_DEFAULT_SORT_DIRECTION: SortDirection;
|
|
232
|
+
export declare const TABLE_DEFAULT_LOCALE = "en-US";
|
|
233
|
+
export declare const TABLE_DEFAULT_CURRENCY = "USD";
|
|
234
|
+
export declare const TABLE_DATE_OPERATORS: readonly ["before", "after", "on"];
|
|
235
|
+
export declare const TABLE_NUMBER_OPERATORS: readonly ["over", "under", "is"];
|
|
236
|
+
/**
|
|
237
|
+
* The operators a column type accepts on a filter. A type absent here has
|
|
238
|
+
* no operator stage: its filters carry no `op`.
|
|
239
|
+
*/
|
|
240
|
+
export declare const TABLE_OPERATORS: Partial<Record<ColumnType, readonly string[]>>;
|
|
241
|
+
/**
|
|
242
|
+
* The operator a filter takes when the caller names none, by column type.
|
|
243
|
+
*/
|
|
244
|
+
export declare const TABLE_DEFAULT_OPERATORS: Partial<Record<ColumnType, string>>;
|
|
245
|
+
/**
|
|
246
|
+
* The fields of the container, for key-driven iteration independent of a
|
|
247
|
+
* container's runtime keys.
|
|
248
|
+
*/
|
|
249
|
+
export declare const TABLE_STATE_FIELDS: Record<StateKey, null>;
|
|
250
|
+
/**
|
|
251
|
+
* Builds the initial state container from config defaults. The result is a
|
|
252
|
+
* plain object. Wrap it in a reactive proxy to track reads and writes.
|
|
253
|
+
*/
|
|
254
|
+
export declare const createTableState: <T>(config: Config<T>) => State<T>;
|
|
255
|
+
/**
|
|
256
|
+
* The headless table machine. Owns paging, sort, selection, column order,
|
|
257
|
+
* and the query over its columns; delegates data loading to `wiring.fetch`.
|
|
258
|
+
*
|
|
259
|
+
* The service reads and writes the `state` container through the `options`
|
|
260
|
+
* middleware. The container can be a plain object or a reactive proxy; the
|
|
261
|
+
* service replaces each member as a whole. Each write emits `change`, and
|
|
262
|
+
* the domain methods emit `updated` and `filtered`. `on` subscribes.
|
|
263
|
+
*/
|
|
264
|
+
export declare class TableService<T> implements Service<T> {
|
|
265
|
+
readonly id: string;
|
|
266
|
+
readonly config: Config<T>;
|
|
267
|
+
private readonly wiring;
|
|
268
|
+
readonly columns: DataTableColumn<T>[];
|
|
269
|
+
readonly rowKey: keyof T;
|
|
270
|
+
readonly actions: Service<T>["actions"];
|
|
271
|
+
readonly bulkActions: Service<T>["bulkActions"];
|
|
272
|
+
readonly pinnedColumns: (keyof T)[];
|
|
273
|
+
readonly query: Service$1;
|
|
274
|
+
private readonly state;
|
|
275
|
+
private readonly emitter;
|
|
276
|
+
private readonly columnMap;
|
|
277
|
+
private readonly definitionIndex;
|
|
278
|
+
private readonly pinnedSet;
|
|
279
|
+
private readonly defaultColumnKeys;
|
|
280
|
+
constructor(id: string, config: Config<T>, state: State<T>, wiring: Actions<T>, options?: Options<T>);
|
|
281
|
+
on<E extends Event<T>>(event: E, listener: Listener<T, E>): () => void;
|
|
282
|
+
get data(): T[];
|
|
283
|
+
get loading(): boolean;
|
|
284
|
+
get initialized(): boolean;
|
|
285
|
+
get page(): number;
|
|
286
|
+
get pageSize(): number;
|
|
287
|
+
get total(): number;
|
|
288
|
+
get pageCount(): number;
|
|
289
|
+
get sortField(): string | null;
|
|
290
|
+
get sortDirection(): SortDirection;
|
|
291
|
+
get selected(): Set<string>;
|
|
292
|
+
get columnOrder(): string[];
|
|
293
|
+
get searchable(): boolean;
|
|
294
|
+
get locale(): string;
|
|
295
|
+
get currency(): string;
|
|
296
|
+
get filterableColumns(): DataTableColumn<T>[];
|
|
297
|
+
get visibleColumns(): DataTableColumn<T>[];
|
|
298
|
+
get isAllSelected(): boolean;
|
|
299
|
+
get isIndeterminate(): boolean;
|
|
300
|
+
get selectAllState(): boolean | "indeterminate";
|
|
301
|
+
get colSpan(): number;
|
|
302
|
+
get hasSelection(): boolean;
|
|
303
|
+
get isSelectable(): boolean;
|
|
304
|
+
get hasActions(): boolean;
|
|
305
|
+
keyOf(row: T): string;
|
|
306
|
+
goToPage(page: number): void;
|
|
307
|
+
setPageSize(size: number): void;
|
|
308
|
+
sortBy(field: string): void;
|
|
309
|
+
sortFieldFor(col: DataTableColumn<T>): string;
|
|
310
|
+
isSorted(col: DataTableColumn<T>): boolean;
|
|
311
|
+
toggleRow(key: string): void;
|
|
312
|
+
toggleAll(): void;
|
|
313
|
+
clearSelection(): void;
|
|
314
|
+
isRowSelected(row: T): boolean;
|
|
315
|
+
isColumnPinned(key: keyof T): boolean;
|
|
316
|
+
isColumnVisible(key: keyof T): boolean;
|
|
317
|
+
toggleColumn(key: keyof T): void;
|
|
318
|
+
setVisibleColumns(keys: Iterable<string>): void;
|
|
319
|
+
moveColumn(key: string, toKey: string): void;
|
|
320
|
+
reorderColumns(order: string[]): void;
|
|
321
|
+
private insertAtDefinition;
|
|
322
|
+
runAction(key: string, row: T): void;
|
|
323
|
+
runBulkAction(key: string): void;
|
|
324
|
+
resetColumns(): void;
|
|
325
|
+
private refilter;
|
|
326
|
+
init(): Promise<boolean>;
|
|
327
|
+
fetch(): Promise<void>;
|
|
328
|
+
}
|
package/.dist/index.mjs
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { entries } from "objectively";
|
|
2
|
+
import { QueryService } from "@zoobzio/query";
|
|
3
|
+
import { makeEmitter, makeProxy } from "@zoobzio/kit";
|
|
4
|
+
const TABLE_DEFAULT_PAGE_SIZE = 25;
|
|
5
|
+
const TABLE_DEFAULT_SORT_DIRECTION = "asc";
|
|
6
|
+
const TABLE_DEFAULT_LOCALE = "en-US";
|
|
7
|
+
const TABLE_DEFAULT_CURRENCY = "USD";
|
|
8
|
+
const TABLE_DATE_OPERATORS = [
|
|
9
|
+
"before",
|
|
10
|
+
"after",
|
|
11
|
+
"on"
|
|
12
|
+
];
|
|
13
|
+
const TABLE_NUMBER_OPERATORS = [
|
|
14
|
+
"over",
|
|
15
|
+
"under",
|
|
16
|
+
"is"
|
|
17
|
+
];
|
|
18
|
+
const TABLE_OPERATORS = {
|
|
19
|
+
date: TABLE_DATE_OPERATORS,
|
|
20
|
+
datetime: TABLE_DATE_OPERATORS,
|
|
21
|
+
number: TABLE_NUMBER_OPERATORS,
|
|
22
|
+
currency: TABLE_NUMBER_OPERATORS,
|
|
23
|
+
filesize: TABLE_NUMBER_OPERATORS
|
|
24
|
+
};
|
|
25
|
+
const TABLE_DEFAULT_OPERATORS = {
|
|
26
|
+
date: "on",
|
|
27
|
+
datetime: "on",
|
|
28
|
+
number: "is",
|
|
29
|
+
currency: "is",
|
|
30
|
+
filesize: "is"
|
|
31
|
+
};
|
|
32
|
+
const TABLE_STATE_FIELDS = {
|
|
33
|
+
data: null,
|
|
34
|
+
loading: null,
|
|
35
|
+
initialized: null,
|
|
36
|
+
page: null,
|
|
37
|
+
pageSize: null,
|
|
38
|
+
total: null,
|
|
39
|
+
pageCount: null,
|
|
40
|
+
sortField: null,
|
|
41
|
+
sortDirection: null,
|
|
42
|
+
selected: null,
|
|
43
|
+
columnOrder: null,
|
|
44
|
+
entries: null,
|
|
45
|
+
draft: null
|
|
46
|
+
};
|
|
47
|
+
const createTableState = (config) => ({
|
|
48
|
+
data: [],
|
|
49
|
+
loading: false,
|
|
50
|
+
initialized: false,
|
|
51
|
+
page: 1,
|
|
52
|
+
pageSize: 25,
|
|
53
|
+
total: 0,
|
|
54
|
+
pageCount: 0,
|
|
55
|
+
sortField: null,
|
|
56
|
+
sortDirection: "asc",
|
|
57
|
+
selected: /* @__PURE__ */ new Set(),
|
|
58
|
+
columnOrder: (config.defaultColumnOrder ?? config.columns.map((c) => c.key)).map(String),
|
|
59
|
+
entries: [],
|
|
60
|
+
draft: ""
|
|
61
|
+
});
|
|
62
|
+
const fields = (columns) => Object.fromEntries(columns.map((c) => {
|
|
63
|
+
const operators = c.type ? TABLE_OPERATORS[c.type] : void 0;
|
|
64
|
+
const defaultOperator = c.type ? TABLE_DEFAULT_OPERATORS[c.type] : void 0;
|
|
65
|
+
const values = c.type === "boolean" ? ["true", "false"] : c.enumValues;
|
|
66
|
+
return [String(c.key), {
|
|
67
|
+
label: c.label,
|
|
68
|
+
...operators && { operators },
|
|
69
|
+
...defaultOperator && { defaultOperator },
|
|
70
|
+
...values && { values }
|
|
71
|
+
}];
|
|
72
|
+
}));
|
|
73
|
+
var TableService = class {
|
|
74
|
+
id;
|
|
75
|
+
config;
|
|
76
|
+
wiring;
|
|
77
|
+
columns;
|
|
78
|
+
rowKey;
|
|
79
|
+
actions;
|
|
80
|
+
bulkActions;
|
|
81
|
+
pinnedColumns;
|
|
82
|
+
query;
|
|
83
|
+
state;
|
|
84
|
+
emitter = makeEmitter();
|
|
85
|
+
columnMap;
|
|
86
|
+
definitionIndex;
|
|
87
|
+
pinnedSet;
|
|
88
|
+
defaultColumnKeys;
|
|
89
|
+
constructor(id, config, state, wiring, options = {}) {
|
|
90
|
+
this.id = id;
|
|
91
|
+
this.config = config;
|
|
92
|
+
this.wiring = wiring;
|
|
93
|
+
this.state = makeProxy(state, TABLE_STATE_FIELDS, options, this.emitter);
|
|
94
|
+
this.columns = config.columns;
|
|
95
|
+
this.rowKey = config.rowKey;
|
|
96
|
+
this.actions = entries(config.actions ?? {}).map(([key, descriptor]) => ({
|
|
97
|
+
...descriptor,
|
|
98
|
+
key,
|
|
99
|
+
action: (row) => this.runAction(key, row)
|
|
100
|
+
}));
|
|
101
|
+
this.bulkActions = entries(config.bulkActions ?? {}).map(([key, descriptor]) => ({
|
|
102
|
+
...descriptor,
|
|
103
|
+
key,
|
|
104
|
+
action: (selected) => wiring.bulkActions?.[key]?.(selected)
|
|
105
|
+
}));
|
|
106
|
+
this.pinnedColumns = config.pinnedColumns ?? [];
|
|
107
|
+
this.columnMap = new Map(config.columns.map((c) => [String(c.key), c]));
|
|
108
|
+
this.definitionIndex = new Map(config.columns.map((c, i) => [String(c.key), i]));
|
|
109
|
+
this.pinnedSet = new Set(this.pinnedColumns.map(String));
|
|
110
|
+
this.defaultColumnKeys = (config.defaultColumnOrder ?? config.columns.map((c) => c.key)).map(String);
|
|
111
|
+
this.query = new QueryService(`${id}-query`, { fields: fields(this.filterableColumns) }, this.state);
|
|
112
|
+
this.query.on("changed", () => this.refilter());
|
|
113
|
+
}
|
|
114
|
+
on(event, listener) {
|
|
115
|
+
return this.emitter.on(event, listener);
|
|
116
|
+
}
|
|
117
|
+
get data() {
|
|
118
|
+
return this.state.data;
|
|
119
|
+
}
|
|
120
|
+
get loading() {
|
|
121
|
+
return this.state.loading;
|
|
122
|
+
}
|
|
123
|
+
get initialized() {
|
|
124
|
+
return this.state.initialized;
|
|
125
|
+
}
|
|
126
|
+
get page() {
|
|
127
|
+
return this.state.page;
|
|
128
|
+
}
|
|
129
|
+
get pageSize() {
|
|
130
|
+
return this.state.pageSize;
|
|
131
|
+
}
|
|
132
|
+
get total() {
|
|
133
|
+
return this.state.total;
|
|
134
|
+
}
|
|
135
|
+
get pageCount() {
|
|
136
|
+
return this.state.pageCount;
|
|
137
|
+
}
|
|
138
|
+
get sortField() {
|
|
139
|
+
return this.state.sortField;
|
|
140
|
+
}
|
|
141
|
+
get sortDirection() {
|
|
142
|
+
return this.state.sortDirection;
|
|
143
|
+
}
|
|
144
|
+
get selected() {
|
|
145
|
+
return this.state.selected;
|
|
146
|
+
}
|
|
147
|
+
get columnOrder() {
|
|
148
|
+
return this.state.columnOrder;
|
|
149
|
+
}
|
|
150
|
+
get searchable() {
|
|
151
|
+
return this.config.searchable ?? true;
|
|
152
|
+
}
|
|
153
|
+
get locale() {
|
|
154
|
+
return this.config.locale ?? "en-US";
|
|
155
|
+
}
|
|
156
|
+
get currency() {
|
|
157
|
+
return this.config.currency ?? "USD";
|
|
158
|
+
}
|
|
159
|
+
get filterableColumns() {
|
|
160
|
+
return this.columns.filter((c) => c.filterable ?? (c.type !== "action" && c.type !== "image"));
|
|
161
|
+
}
|
|
162
|
+
get visibleColumns() {
|
|
163
|
+
return this.columnOrder.map((key) => this.columnMap.get(key)).filter((c) => c !== void 0);
|
|
164
|
+
}
|
|
165
|
+
get isAllSelected() {
|
|
166
|
+
if (!this.data.length) return false;
|
|
167
|
+
return this.data.every((row) => this.selected.has(this.keyOf(row)));
|
|
168
|
+
}
|
|
169
|
+
get isIndeterminate() {
|
|
170
|
+
if (!this.selected.size) return false;
|
|
171
|
+
return !this.isAllSelected;
|
|
172
|
+
}
|
|
173
|
+
get selectAllState() {
|
|
174
|
+
if (this.isIndeterminate) return "indeterminate";
|
|
175
|
+
return this.isAllSelected;
|
|
176
|
+
}
|
|
177
|
+
get colSpan() {
|
|
178
|
+
return this.visibleColumns.length + (this.hasActions ? 1 : 0) + (this.isSelectable ? 1 : 0);
|
|
179
|
+
}
|
|
180
|
+
get hasSelection() {
|
|
181
|
+
return this.selected.size > 0;
|
|
182
|
+
}
|
|
183
|
+
get isSelectable() {
|
|
184
|
+
return this.bulkActions.length > 0;
|
|
185
|
+
}
|
|
186
|
+
get hasActions() {
|
|
187
|
+
return this.actions.length > 0;
|
|
188
|
+
}
|
|
189
|
+
keyOf(row) {
|
|
190
|
+
return String(row[this.config.rowKey]);
|
|
191
|
+
}
|
|
192
|
+
goToPage(page) {
|
|
193
|
+
if (page < 1 || page > this.pageCount) return;
|
|
194
|
+
this.state.page = page;
|
|
195
|
+
this.fetch();
|
|
196
|
+
}
|
|
197
|
+
setPageSize(size) {
|
|
198
|
+
this.state.pageSize = size;
|
|
199
|
+
this.state.page = 1;
|
|
200
|
+
this.fetch();
|
|
201
|
+
}
|
|
202
|
+
sortBy(field) {
|
|
203
|
+
if (this.sortField === field) this.state.sortDirection = this.sortDirection === "asc" ? "desc" : "asc";
|
|
204
|
+
else {
|
|
205
|
+
this.state.sortField = field;
|
|
206
|
+
this.state.sortDirection = "asc";
|
|
207
|
+
}
|
|
208
|
+
this.state.page = 1;
|
|
209
|
+
this.fetch();
|
|
210
|
+
}
|
|
211
|
+
sortFieldFor(col) {
|
|
212
|
+
return col.sortKey ?? String(col.key);
|
|
213
|
+
}
|
|
214
|
+
isSorted(col) {
|
|
215
|
+
return this.sortField === this.sortFieldFor(col);
|
|
216
|
+
}
|
|
217
|
+
toggleRow(key) {
|
|
218
|
+
const next = new Set(this.selected);
|
|
219
|
+
if (next.has(key)) next.delete(key);
|
|
220
|
+
else next.add(key);
|
|
221
|
+
this.state.selected = next;
|
|
222
|
+
}
|
|
223
|
+
toggleAll() {
|
|
224
|
+
if (this.isAllSelected) this.state.selected = /* @__PURE__ */ new Set();
|
|
225
|
+
else this.state.selected = new Set(this.data.map((row) => this.keyOf(row)));
|
|
226
|
+
}
|
|
227
|
+
clearSelection() {
|
|
228
|
+
this.state.selected = /* @__PURE__ */ new Set();
|
|
229
|
+
}
|
|
230
|
+
isRowSelected(row) {
|
|
231
|
+
return this.selected.has(this.keyOf(row));
|
|
232
|
+
}
|
|
233
|
+
isColumnPinned(key) {
|
|
234
|
+
return this.pinnedSet.has(String(key));
|
|
235
|
+
}
|
|
236
|
+
isColumnVisible(key) {
|
|
237
|
+
return this.columnOrder.includes(String(key));
|
|
238
|
+
}
|
|
239
|
+
toggleColumn(key) {
|
|
240
|
+
const k = String(key);
|
|
241
|
+
if (this.pinnedSet.has(k)) return;
|
|
242
|
+
if (this.columnOrder.includes(k)) this.state.columnOrder = this.columnOrder.filter((c) => c !== k);
|
|
243
|
+
else this.state.columnOrder = this.insertAtDefinition(this.columnOrder, k);
|
|
244
|
+
}
|
|
245
|
+
setVisibleColumns(keys) {
|
|
246
|
+
const wanted = new Set(keys);
|
|
247
|
+
for (const k of this.pinnedSet) wanted.add(k);
|
|
248
|
+
let order = this.columnOrder.filter((k) => wanted.has(k));
|
|
249
|
+
for (const k of wanted) {
|
|
250
|
+
if (!this.columnMap.has(k) || order.includes(k)) continue;
|
|
251
|
+
order = this.insertAtDefinition(order, k);
|
|
252
|
+
}
|
|
253
|
+
this.state.columnOrder = order;
|
|
254
|
+
}
|
|
255
|
+
moveColumn(key, toKey) {
|
|
256
|
+
if (key === toKey) return;
|
|
257
|
+
const order = [...this.columnOrder];
|
|
258
|
+
const from = order.indexOf(key);
|
|
259
|
+
const to = order.indexOf(toKey);
|
|
260
|
+
if (from === -1 || to === -1) return;
|
|
261
|
+
order.splice(from, 1);
|
|
262
|
+
order.splice(to, 0, key);
|
|
263
|
+
this.state.columnOrder = order;
|
|
264
|
+
}
|
|
265
|
+
reorderColumns(order) {
|
|
266
|
+
this.state.columnOrder = order;
|
|
267
|
+
}
|
|
268
|
+
insertAtDefinition(order, key) {
|
|
269
|
+
const index = this.definitionIndex.get(key) ?? Infinity;
|
|
270
|
+
const at = order.findIndex((k) => (this.definitionIndex.get(k) ?? Infinity) > index);
|
|
271
|
+
const next = [...order];
|
|
272
|
+
next.splice(at === -1 ? next.length : at, 0, key);
|
|
273
|
+
return next;
|
|
274
|
+
}
|
|
275
|
+
runAction(key, row) {
|
|
276
|
+
this.wiring.actions?.[key]?.(row);
|
|
277
|
+
}
|
|
278
|
+
runBulkAction(key) {
|
|
279
|
+
this.wiring.bulkActions?.[key]?.(this.selected);
|
|
280
|
+
}
|
|
281
|
+
resetColumns() {
|
|
282
|
+
this.state.columnOrder = this.defaultColumnKeys;
|
|
283
|
+
}
|
|
284
|
+
refilter() {
|
|
285
|
+
this.state.page = 1;
|
|
286
|
+
this.fetch();
|
|
287
|
+
this.emitter.emit("filtered", {
|
|
288
|
+
id: this.id,
|
|
289
|
+
query: this.query.text,
|
|
290
|
+
filters: this.query.filters
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
async init() {
|
|
294
|
+
if (this.initialized) return true;
|
|
295
|
+
this.state.initialized = true;
|
|
296
|
+
await this.fetch();
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
async fetch() {
|
|
300
|
+
this.state.loading = true;
|
|
301
|
+
try {
|
|
302
|
+
const result = await this.wiring.fetch({
|
|
303
|
+
page: this.page,
|
|
304
|
+
pageSize: this.pageSize,
|
|
305
|
+
sortField: this.sortField,
|
|
306
|
+
sortDirection: this.sortDirection,
|
|
307
|
+
query: this.query.text,
|
|
308
|
+
filters: this.query.filters
|
|
309
|
+
}, this);
|
|
310
|
+
this.state.data = result.data;
|
|
311
|
+
this.state.total = result.total;
|
|
312
|
+
this.state.pageCount = result.pageCount;
|
|
313
|
+
} finally {
|
|
314
|
+
this.state.loading = false;
|
|
315
|
+
this.emitter.emit("updated", {
|
|
316
|
+
id: this.id,
|
|
317
|
+
total: this.total
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
export { TABLE_DATE_OPERATORS, TABLE_DEFAULT_CURRENCY, TABLE_DEFAULT_LOCALE, TABLE_DEFAULT_OPERATORS, TABLE_DEFAULT_PAGE_SIZE, TABLE_DEFAULT_SORT_DIRECTION, TABLE_NUMBER_OPERATORS, TABLE_OPERATORS, TABLE_STATE_FIELDS, TableService, createTableState };
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Alexander Thorwaldson
|
|
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
CHANGED
|
@@ -1,3 +1,79 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @zoobzio/table
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Headless data table service. Pure TypeScript, no framework dependency.
|
|
4
|
+
|
|
5
|
+
Owns: paging, sort, row selection, column order, and the query over its
|
|
6
|
+
columns (`@zoobzio/query`).
|
|
7
|
+
Delegates: data loading (`fetch`) and state storage (the container).
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { TableService, createTableState } from "@zoobzio/table";
|
|
13
|
+
|
|
14
|
+
const config = { columns: [{ key: "name", label: "Name" }], rowKey: "id" };
|
|
15
|
+
|
|
16
|
+
const table = new TableService("users", config, createTableState(config), {
|
|
17
|
+
fetch: async (params) => api.users(params),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
await table.init();
|
|
21
|
+
table.sortBy("name");
|
|
22
|
+
table.query.add({ kind: "filter", key: "status", value: "active" });
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Query
|
|
26
|
+
|
|
27
|
+
`table.query` is a query service over the filterable columns: one field per
|
|
28
|
+
column, with the operators and default operator of its type and the values
|
|
29
|
+
of an enum or boolean column. Any view drives it: `add`, `remove`, `clear`,
|
|
30
|
+
or the draft a text view composes through. A change to its entries resets
|
|
31
|
+
the page, refetches, and emits `filtered`. The fetch params carry its `text`
|
|
32
|
+
as `query` and its `filters`.
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
table.query.add({ kind: "filter", key: "status", value: "Active" });
|
|
36
|
+
table.query.setDraft("Amount: over: 40");
|
|
37
|
+
table.query.submit();
|
|
38
|
+
table.query.filters; // [{ key: "status", value: "Active" }, { key: "amount", op: "over", value: "40" }]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## The state container
|
|
42
|
+
|
|
43
|
+
The service reads and writes the `state` container: paging, sort, selection,
|
|
44
|
+
column order, and the query's entries and draft. The container can be a
|
|
45
|
+
plain object or a reactive proxy. The service replaces each member as a
|
|
46
|
+
whole, so a proxy tracks every change.
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
createTableState(config); // plain: tests, Node
|
|
50
|
+
reactive(createTableState(config)); // Vue
|
|
51
|
+
useState(`table-${id}`, () => createTableState(config)).value; // Nuxt, shared by id
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Middleware
|
|
55
|
+
|
|
56
|
+
`options.get.state.<field>` and `options.set.state.<field>` receive the value
|
|
57
|
+
of the field and return the value the service uses. A missing slot passes the
|
|
58
|
+
value on.
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
new TableService(id, config, state, wiring, {
|
|
62
|
+
set: { state: { query: (query) => query.trim() } },
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Events
|
|
67
|
+
|
|
68
|
+
`on(event, listener)` subscribes. Listeners run in subscription order, after
|
|
69
|
+
the write lands. The return value removes the listener.
|
|
70
|
+
|
|
71
|
+
- `change`: a field of the container was written. `{ field, before, after }`, narrowed by `field`.
|
|
72
|
+
- `updated`: a fetch settled. `{ id, total }`.
|
|
73
|
+
- `filtered`: the query's entries changed. `{ id, query, filters }`.
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
const off = table.on("change", (change) => {
|
|
77
|
+
if (change.field === "page") track(change.after);
|
|
78
|
+
});
|
|
79
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zoobzio/table",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Headless data table service: paging, sort, selection, column order, and a query over the columns. No framework dependency.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Alex Thorwaldson",
|
|
8
|
+
"email": "alex@zoobz.io",
|
|
9
|
+
"url": "https://zoobz.io"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/zoobzio/foundation.git",
|
|
14
|
+
"directory": "packages/table"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
".dist"
|
|
18
|
+
],
|
|
19
|
+
"type": "module",
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"main": "./.dist/index.mjs",
|
|
22
|
+
"types": "./.dist/index.d.mts",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./.dist/index.d.mts",
|
|
26
|
+
"import": "./.dist/index.mjs"
|
|
27
|
+
},
|
|
28
|
+
"./package.json": "./package.json"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"objectively": "^0.3.0",
|
|
32
|
+
"@zoobzio/kit": "0.0.1",
|
|
33
|
+
"@zoobzio/query": "0.0.1"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "obuild",
|
|
37
|
+
"stub": "obuild --stub",
|
|
38
|
+
"lint": "oxlint",
|
|
39
|
+
"typecheck": "tsc --noEmit",
|
|
40
|
+
"test": "vitest run"
|
|
41
|
+
}
|
|
6
42
|
}
|