@katlux/providers 0.1.0-beta.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.
@@ -0,0 +1,52 @@
1
+ import { ref, type Ref } from 'vue'
2
+ import { ADataProvider } from './ADataProvider'
3
+ import type { TDataRow, IDataFilter, IDataProviderOptions } from '../types'
4
+ import { useFilterLogic } from '@katlux/toolkit/composables/useFilterLogic'
5
+ import { useSortLogic } from '@katlux/toolkit/composables/useSortLogic'
6
+
7
+ const { evaluateFilter, filterData } = useFilterLogic()
8
+ const { sortData } = useSortLogic()
9
+
10
+
11
+ export class CFlatClientDataProvider extends ADataProvider {
12
+ allData: Ref<TDataRow[]> = ref([])
13
+ filteredData: Ref<TDataRow[]> = ref([])
14
+
15
+ constructor(options?: IDataProviderOptions) {
16
+ super(options)
17
+ }
18
+
19
+ setData(data: TDataRow[]) {
20
+ this.allData.value = [...data]
21
+ this.loadPageData()
22
+ }
23
+
24
+ async loadPageData(): Promise<void> {
25
+
26
+ // Apply filters
27
+ if (!this.filter.value) {
28
+ this.filteredData.value = this.allData.value
29
+ } else {
30
+ const { results } = filterData(this.allData.value, this.filter.value)
31
+ this.filteredData.value = results
32
+ }
33
+ this.rowCount.value = this.filteredData.value.length
34
+
35
+ // Apply sort
36
+ if (this.sortList.value.length > 0)
37
+ this.filteredData.value = sortData(this.sortList.value, this.filteredData.value)
38
+ // Apply pagination (pageSize <= 0 means no pagination)
39
+ if (this.pageSize.value <= 0) {
40
+ this.pageData.value = this.filteredData.value
41
+ } else {
42
+ const startIndex = (this.currentPage.value - 1) * this.pageSize.value
43
+ const endIndex = startIndex + this.pageSize.value
44
+ this.pageData.value = this.filteredData.value.slice(startIndex, endIndex)
45
+ }
46
+ }
47
+
48
+ refreshData(): void {
49
+
50
+ }
51
+
52
+ }
@@ -0,0 +1,104 @@
1
+ import { ref, type Ref } from 'vue'
2
+ import { ADataProvider } from './ADataProvider'
3
+ import type { TDataRow, IDataResult, TPageDataHandler, IDataFilter, IDataSort, IDataProviderOptions } from '../types'
4
+
5
+ export class CFlatServerDataProvider extends ADataProvider {
6
+
7
+ pageDataHandler: Ref<TPageDataHandler> = ref((currentPage: number, pageSize: number, filter: IDataFilter, sortList: IDataSort[]): IDataResult | Promise<IDataResult> => {
8
+ return {
9
+ rows: [],
10
+ rowCount: 0
11
+ };
12
+ })
13
+ contextKey: Ref<string> = ref("")
14
+ initialLoad: Ref<boolean> = ref(true)
15
+
16
+ constructor(options?: IDataProviderOptions) {
17
+ super(options)
18
+ }
19
+
20
+ setContextKey(key: string) {
21
+ this.contextKey.value = key
22
+ }
23
+
24
+ async setPageDataHandler(handler: TPageDataHandler) {
25
+ this.pageDataHandler.value = handler
26
+ await this.loadPageData()
27
+ }
28
+
29
+ async loadPageData(options?: { disableCache?: boolean }): Promise<void> {
30
+
31
+
32
+ if (!this.pageDataHandler.value || (!this.SSR.value && import.meta.server)) return;
33
+
34
+ // Stable key generation for Nuxt hydration
35
+ const stableStringify = (val: any) => JSON.stringify(val === undefined ? "" : val)
36
+ const key = [
37
+ this.contextKey.value,
38
+ this.currentPage.value,
39
+ this.pageSize.value,
40
+ stableStringify(this.filter.value),
41
+ stableStringify(this.sortList.value)
42
+ ].join('-')
43
+
44
+ const fetcher = async () => await this.pageDataHandler.value(this.currentPage.value, this.pageSize.value, this.filter.value as IDataFilter, this.sortList.value as IDataSort[], options)
45
+
46
+
47
+ // Check for existing data from SSR payload first (Only if SSR is enabled)
48
+ // Skip if disableCache is true
49
+ if (!options?.disableCache && this.SSR.value && this.contextKey.value && this.initialLoad.value) {
50
+ const { data } = useNuxtData(key)
51
+ if (data.value) {
52
+ const result = data.value as IDataResult
53
+ this.pageData.value = result.rows
54
+ this.rowCount.value = result.rowCount
55
+ this.initialLoad.value = false
56
+ this.loading.value = false
57
+ return
58
+ }
59
+ }
60
+
61
+ // SSR: use useAsyncData when SSR is enabled (works on both server and client)
62
+ if (this.SSR.value && this.contextKey.value) {
63
+ this.loading.value = true
64
+ const { data } = await useAsyncData(key, fetcher)
65
+ if (data.value) {
66
+ const result = data.value as IDataResult
67
+ this.pageData.value = result.rows
68
+ this.rowCount.value = result.rowCount
69
+ this.initialLoad.value = false
70
+ this.loading.value = false
71
+ return
72
+ }
73
+ }
74
+
75
+ // For non-SSR client-side, handle asynchronously without blocking
76
+ if (!this.SSR.value) {
77
+ // Call fetcher and handle promise asynchronously after Nuxt is ready (hydrated)
78
+ // This avoids hydration mismatches on initial load
79
+ onNuxtReady(() => {
80
+ this.loading.value = true
81
+ fetcher().then((returnedData) => {
82
+ this.pageData.value = returnedData.rows
83
+ this.rowCount.value = returnedData.rowCount
84
+ this.initialLoad.value = false
85
+ this.loading.value = false
86
+ }).catch(() => {
87
+ this.initialLoad.value = false
88
+ this.loading.value = false
89
+ })
90
+ })
91
+ return
92
+ }
93
+
94
+ // SSR client-side or fallback: await the request
95
+ this.loading.value = true
96
+ const returnedData: IDataResult = await fetcher()
97
+ this.pageData.value = returnedData.rows
98
+ this.rowCount.value = returnedData.rowCount
99
+ this.initialLoad.value = false
100
+ this.loading.value = false
101
+
102
+ }
103
+
104
+ }
@@ -0,0 +1,335 @@
1
+ import { ref, computed, watch, type Ref, type ComputedRef } from 'vue'
2
+ import { CFlatClientDataProvider } from './CFlatClientDataProvider'
3
+ import { useFilterLogic } from '@katlux/toolkit/composables/useFilterLogic'
4
+ import type {
5
+ TDataRow,
6
+ ITreeNode,
7
+ ITreeNodeStatic,
8
+ TreePaginationMode,
9
+ ITreeDataProviderOptions
10
+ } from '../types'
11
+
12
+ const { evaluateFilter, filterData } = useFilterLogic()
13
+
14
+
15
+ export class CTreeClientDataProvider extends CFlatClientDataProvider {
16
+ // Tree configuration
17
+ parentKey: string = 'parentId'
18
+ idKey: string = 'id'
19
+ expandedByDefault: boolean = false
20
+ paginateBy: TreePaginationMode = 'all'
21
+
22
+ // Tree state
23
+ expandedNodes: Ref<Set<string | number>> = ref(new Set())
24
+
25
+ // Internal: static tree structure (only rebuilds when data changes)
26
+ private _staticNodes: ComputedRef<ITreeNodeStatic[]>
27
+ private _nodeMap: ComputedRef<Map<string | number, ITreeNodeStatic>>
28
+ private _sortedNodeIds: ComputedRef<(string | number)[]>
29
+
30
+ constructor(options?: ITreeDataProviderOptions) {
31
+ super(options)
32
+ if (options?.parentKey) this.parentKey = options.parentKey
33
+ if (options?.idKey) this.idKey = options.idKey
34
+ if (options?.expandedByDefault) this.expandedByDefault = options.expandedByDefault
35
+ if (options?.paginateBy) this.paginateBy = options.paginateBy
36
+
37
+ // Static tree structure - only rebuilds when allData changes
38
+ this._staticNodes = computed(() => this.buildStaticNodes())
39
+ this._nodeMap = computed(() => new Map(this._staticNodes.value.map(n => [n.id, n])))
40
+ this._sortedNodeIds = computed(() => this.computeSortedIds())
41
+
42
+ // Watch for changes that require pageData update
43
+ // Note: currentPage and pageSize changes are handled by parent ADataProvider watchers
44
+ // which call loadPageData() -> updatePageData()
45
+ watch([this.expandedNodes, this._staticNodes], () => {
46
+ this.updatePageData()
47
+ }, { immediate: true })
48
+ }
49
+
50
+ private buildStaticNodes(): ITreeNodeStatic[] {
51
+ const allData = this.allData.value || []
52
+ const nodeMap = new Map<string | number, ITreeNodeStatic>()
53
+ const childrenMap = new Map<string | number, TDataRow[]>()
54
+
55
+ // First pass: create nodes and map children
56
+ allData.forEach((item: TDataRow) => {
57
+ const id = item[this.idKey]
58
+ const parentId = item[this.parentKey] || null
59
+
60
+ nodeMap.set(id, {
61
+ data: item,
62
+ id,
63
+ parentId,
64
+ depth: 0,
65
+ hasChildren: false,
66
+ })
67
+
68
+ if (parentId !== null) {
69
+ if (!childrenMap.has(parentId)) {
70
+ childrenMap.set(parentId, [])
71
+ }
72
+ childrenMap.get(parentId)!.push(item)
73
+ }
74
+ })
75
+
76
+ // Second pass: set hasChildren and calculate depth
77
+ const calculateDepth = (nodeId: string | number, depth: number = 0): void => {
78
+ const node = nodeMap.get(nodeId)
79
+ if (!node) return
80
+
81
+ node.depth = depth
82
+ const children = childrenMap.get(nodeId)
83
+ if (children && children.length > 0) {
84
+ node.hasChildren = true
85
+ children.forEach((child: TDataRow) => {
86
+ calculateDepth(child[this.idKey], depth + 1)
87
+ })
88
+ }
89
+ }
90
+
91
+ nodeMap.forEach((node) => {
92
+ if (node.parentId === null) {
93
+ calculateDepth(node.id, 0)
94
+ }
95
+ })
96
+
97
+ return Array.from(nodeMap.values())
98
+ }
99
+
100
+ // Pre-compute sorted order once (only when data changes)
101
+ private computeSortedIds(): (string | number)[] {
102
+ const nodes = this._staticNodes.value
103
+ const nodeMap = new Map(nodes.map(n => [n.id, n]))
104
+
105
+ // Build tree order using DFS
106
+ const sortedIds: (string | number)[] = []
107
+ const rootNodes = nodes.filter(n => n.parentId === null)
108
+
109
+ // Get children map
110
+ const childrenMap = new Map<string | number, ITreeNodeStatic[]>()
111
+ nodes.forEach(n => {
112
+ if (n.parentId !== null) {
113
+ if (!childrenMap.has(n.parentId)) {
114
+ childrenMap.set(n.parentId, [])
115
+ }
116
+ childrenMap.get(n.parentId)!.push(n)
117
+ }
118
+ })
119
+
120
+ const collectNodes = (node: ITreeNodeStatic): void => {
121
+ sortedIds.push(node.id)
122
+ const children = childrenMap.get(node.id) || []
123
+ children.forEach(child => collectNodes(child))
124
+ }
125
+
126
+ rootNodes.forEach(root => collectNodes(root))
127
+ return sortedIds
128
+ }
129
+
130
+ // Fast visibility check and page data update
131
+ private updatePageData(): void {
132
+ const nodeMap = this._nodeMap.value
133
+ const sortedIds = this._sortedNodeIds.value
134
+ const visibleNodes: ITreeNode[] = []
135
+
136
+ if (this.filter.value && this.filter.value.active) {
137
+ // FILTER LOGIC
138
+ // Use shared filterData with treeOptions
139
+ // Static nodes are already in flat format needed
140
+ const { results: filtered, expandedNodes: expandedResults } = filterData(
141
+ this._staticNodes.value,
142
+ this.filter.value,
143
+ {
144
+ treeOptions: {
145
+ idKey: this.idKey,
146
+ parentKey: this.parentKey
147
+ }
148
+ }
149
+ )
150
+
151
+ // We need to update isExpanded based on the result
152
+ const expandedSet = expandedResults || new Set()
153
+ for (const node of (filtered as ITreeNodeStatic[])) {
154
+ visibleNodes.push({
155
+ ...node,
156
+ isExpanded: expandedSet.has(node.id)
157
+ })
158
+ }
159
+
160
+ } else {
161
+ // STANDARD TREE VISIBILITY LOGIC
162
+ for (const id of sortedIds) {
163
+ const staticNode = nodeMap.get(id)
164
+ if (!staticNode) continue
165
+
166
+ // Check if node is visible (all ancestors expanded)
167
+ if (this.isNodeVisible(staticNode, nodeMap)) {
168
+ const isExpanded = this.expandedByDefault
169
+ ? !this.expandedNodes.value.has(id)
170
+ : this.expandedNodes.value.has(id)
171
+
172
+ visibleNodes.push({
173
+ ...staticNode,
174
+ isExpanded
175
+ })
176
+ }
177
+ }
178
+ }
179
+
180
+ // Apply pagination
181
+ const paginatedNodes = this.paginateNodes(visibleNodes)
182
+
183
+ // Update pageData for KDatatable
184
+ this.pageData.value = paginatedNodes.map(node => ({
185
+ ...node.data,
186
+ __treeNode: node
187
+ }))
188
+ }
189
+
190
+ private isNodeVisible(node: ITreeNodeStatic, nodeMap: Map<string | number, ITreeNodeStatic>): boolean {
191
+ if (node.parentId === null) return true
192
+
193
+ let currentId: string | number | null = node.parentId
194
+ while (currentId !== null) {
195
+ const parent = nodeMap.get(currentId)
196
+ if (!parent) return false
197
+
198
+ const parentExpanded = this.expandedByDefault
199
+ ? !this.expandedNodes.value.has(currentId)
200
+ : this.expandedNodes.value.has(currentId)
201
+
202
+ if (!parentExpanded) return false
203
+ currentId = parent.parentId
204
+ }
205
+ return true
206
+ }
207
+
208
+ private paginateNodes(visible: ITreeNode[]): ITreeNode[] {
209
+ const pageSize = this.pageSize.value
210
+ const currentPage = this.currentPage.value
211
+
212
+ // pageSize <= 0 means no pagination
213
+ if (pageSize <= 0) {
214
+ this.rowCount.value = visible.length
215
+ return visible
216
+ }
217
+
218
+ if (this.paginateBy === 'root') {
219
+ const rootNodes = visible.filter(n => n.parentId === null)
220
+ const startIndex = (currentPage - 1) * pageSize
221
+ const endIndex = startIndex + pageSize
222
+ const paginatedRoots = rootNodes.slice(startIndex, endIndex)
223
+ const paginatedRootIds = new Set(paginatedRoots.map(n => n.id))
224
+
225
+ const nodeMap = new Map(visible.map(n => [n.id, n]))
226
+ const getRootId = (node: ITreeNode): string | number => {
227
+ let current = node
228
+ while (current.parentId !== null) {
229
+ const parent = nodeMap.get(current.parentId as string | number)
230
+ if (!parent) break
231
+ current = parent
232
+ }
233
+ return current.id
234
+ }
235
+
236
+ const result = visible.filter(node => paginatedRootIds.has(getRootId(node)))
237
+ this.rowCount.value = rootNodes.length
238
+ return result
239
+ } else {
240
+ const startIndex = (currentPage - 1) * pageSize
241
+ const endIndex = startIndex + pageSize
242
+ this.rowCount.value = visible.length
243
+ return visible.slice(startIndex, endIndex)
244
+ }
245
+ }
246
+
247
+ // Override loadPageData
248
+ override async loadPageData(): Promise<void> {
249
+ this.updatePageData()
250
+ }
251
+
252
+ // Public API
253
+ get treeNodes(): ITreeNode[] {
254
+ // Build full tree nodes with expansion state
255
+ return this._staticNodes.value.map(node => ({
256
+ ...node,
257
+ isExpanded: this.expandedByDefault
258
+ ? !this.expandedNodes.value.has(node.id)
259
+ : this.expandedNodes.value.has(node.id)
260
+ }))
261
+ }
262
+
263
+ get visibleRows(): ITreeNode[] {
264
+ // Rebuild visible list
265
+ const nodeMap = this._nodeMap.value
266
+ const sortedIds = this._sortedNodeIds.value
267
+ const visibleNodes: ITreeNode[] = []
268
+
269
+ for (const id of sortedIds) {
270
+ const staticNode = nodeMap.get(id)
271
+ if (!staticNode) continue
272
+
273
+ if (this.isNodeVisible(staticNode, nodeMap)) {
274
+ visibleNodes.push({
275
+ ...staticNode,
276
+ isExpanded: this.expandedByDefault
277
+ ? !this.expandedNodes.value.has(id)
278
+ : this.expandedNodes.value.has(id)
279
+ })
280
+ }
281
+ }
282
+
283
+ return this.paginateNodes(visibleNodes)
284
+ }
285
+
286
+ toggleNode(nodeId: string | number): void {
287
+ if (this.expandedNodes.value.has(nodeId)) {
288
+ this.expandedNodes.value.delete(nodeId)
289
+ } else {
290
+ this.expandedNodes.value.add(nodeId)
291
+ }
292
+ // Trigger reactivity
293
+ this.expandedNodes.value = new Set(this.expandedNodes.value)
294
+ }
295
+
296
+ expand(nodeId: string | number): void {
297
+ this.expandedNodes.value.add(nodeId)
298
+ this.expandedNodes.value = new Set(this.expandedNodes.value)
299
+ }
300
+
301
+ collapse(nodeId: string | number): void {
302
+ this.expandedNodes.value.delete(nodeId)
303
+ this.expandedNodes.value = new Set(this.expandedNodes.value)
304
+ }
305
+
306
+ expandAll(): void {
307
+ const allIds = this._staticNodes.value.filter(n => n.hasChildren).map(n => n.id)
308
+ this.expandedNodes.value = new Set(allIds)
309
+ }
310
+
311
+ collapseAll(): void {
312
+ this.expandedNodes.value = new Set()
313
+ }
314
+
315
+ getNode(nodeId: string | number): ITreeNode | undefined {
316
+ const staticNode = this._nodeMap.value.get(nodeId)
317
+ if (!staticNode) return undefined
318
+ return {
319
+ ...staticNode,
320
+ isExpanded: this.expandedByDefault
321
+ ? !this.expandedNodes.value.has(nodeId)
322
+ : this.expandedNodes.value.has(nodeId)
323
+ }
324
+ }
325
+
326
+ getChildren(nodeId: string | number): ITreeNode[] {
327
+ return this.treeNodes.filter(n => n.parentId === nodeId)
328
+ }
329
+
330
+ getParent(nodeId: string | number): ITreeNode | undefined {
331
+ const node = this.getNode(nodeId)
332
+ if (!node || node.parentId === null) return undefined
333
+ return this.getNode(node.parentId)
334
+ }
335
+ }
@@ -0,0 +1,207 @@
1
+ import { ref, watch, type Ref } from 'vue'
2
+ import { ADataProvider } from './ADataProvider'
3
+ import type {
4
+ IDataResult,
5
+ IDataFilter,
6
+ IDataSort,
7
+ ITreeDataProviderOptions,
8
+ TreePaginationMode
9
+ } from '../types'
10
+
11
+ /**
12
+ * Tree-specific page data handler that includes tree parameters
13
+ * Server handles tree building, visibility, and pagination
14
+ */
15
+ export type TTreePageDataHandler = (
16
+ currentPage: number,
17
+ pageSize: number,
18
+ filter: IDataFilter,
19
+ sortList: IDataSort[],
20
+ treeParams: {
21
+ expandedNodes: (string | number)[],
22
+ parentKey: string,
23
+ idKey: string,
24
+ paginateBy: TreePaginationMode,
25
+ expandedByDefault: boolean
26
+ },
27
+ options?: { disableCache?: boolean }
28
+ ) => IDataResult | Promise<IDataResult>;
29
+
30
+ /**
31
+ * Server-side Tree Data Provider
32
+ *
33
+ * Key difference from CTreeClientDataProvider:
34
+ * - Tree structure, visibility, and pagination are handled SERVER-SIDE
35
+ * - Client only sends expanded state to server
36
+ * - Server returns already-paginated visible rows with __treeNode metadata
37
+ * - No allData - server handles everything
38
+ */
39
+ export class CTreeServerDataProvider extends ADataProvider {
40
+ // Tree configuration (sent to server)
41
+ parentKey: string = 'parentId'
42
+ idKey: string = 'id'
43
+ expandedByDefault: boolean = false
44
+ paginateBy: TreePaginationMode = 'all'
45
+
46
+ // Tree state - toggled nodes (sent to server for visibility calculation)
47
+ expandedNodes: Ref<Set<string | number>> = ref(new Set())
48
+
49
+ // Server-side data handler
50
+ pageDataHandler: Ref<TTreePageDataHandler | null> = ref(null)
51
+ contextKey: Ref<string> = ref("")
52
+ initialLoad: Ref<boolean> = ref(true)
53
+
54
+ constructor(options?: ITreeDataProviderOptions) {
55
+ super(options)
56
+ if (options?.parentKey) this.parentKey = options.parentKey
57
+ if (options?.idKey) this.idKey = options.idKey
58
+ if (options?.expandedByDefault) this.expandedByDefault = options.expandedByDefault
59
+ if (options?.paginateBy) this.paginateBy = options.paginateBy
60
+
61
+ // Watch for changes that require server refetch
62
+ watch([this.expandedNodes, this.currentPage, this.pageSize], () => {
63
+ if (this.pageDataHandler.value && !this.initialLoad.value) {
64
+ this.loadPageData()
65
+ }
66
+ })
67
+ }
68
+
69
+ setContextKey(key: string) {
70
+ this.contextKey.value = key
71
+ }
72
+
73
+ async setPageDataHandler(handler: TTreePageDataHandler) {
74
+ this.pageDataHandler.value = handler
75
+ await this.loadPageData()
76
+ }
77
+
78
+ /**
79
+ * Load page data from server
80
+ * Server handles tree building, visibility, and pagination
81
+ */
82
+ async loadPageData(options?: { disableCache?: boolean }): Promise<void> {
83
+ if (!this.pageDataHandler.value || (!this.SSR.value && import.meta.server)) return;
84
+
85
+ const expandedArray = Array.from(this.expandedNodes.value)
86
+ const stableStringify = (val: any) => JSON.stringify(val === undefined ? "" : val)
87
+ const key = [
88
+ this.contextKey.value,
89
+ this.currentPage.value,
90
+ this.pageSize.value,
91
+ stableStringify(this.filter.value),
92
+ stableStringify(this.sortList.value),
93
+ stableStringify(expandedArray)
94
+ ].join('-')
95
+
96
+ const treeParams = {
97
+ expandedNodes: expandedArray,
98
+ parentKey: this.parentKey,
99
+ idKey: this.idKey,
100
+ paginateBy: this.paginateBy,
101
+ expandedByDefault: this.expandedByDefault
102
+ }
103
+
104
+ const fetcher = async () => await this.pageDataHandler.value!(
105
+ this.currentPage.value,
106
+ this.pageSize.value,
107
+ this.filter.value as IDataFilter,
108
+ this.sortList.value as IDataSort[],
109
+ treeParams,
110
+ options
111
+ )
112
+
113
+ // Check for existing data from SSR payload first
114
+ // Skip if disableCache is true
115
+ if (!options?.disableCache && this.SSR.value && this.contextKey.value && this.initialLoad.value) {
116
+ const { data } = useNuxtData(key)
117
+ if (data.value) {
118
+ const result = data.value as IDataResult
119
+ this.pageData.value = result.rows
120
+ this.rowCount.value = result.rowCount
121
+ this.initialLoad.value = false
122
+ this.loading.value = false
123
+ return
124
+ }
125
+ }
126
+
127
+ // SSR: use useAsyncData
128
+ if (this.SSR.value && this.contextKey.value) {
129
+ this.loading.value = true
130
+
131
+ // If cache is disabled, we bypass useAsyncData's automatic caching if possible,
132
+ // but useAsyncData is primary for hydration.
133
+ // For specific disableCache request in SSR mode, we might want to just fetch directly
134
+ // OR rely on the fact that the underlying handler will disable cache.
135
+ // However, useAsyncData itself caches based on key.
136
+ // If disableCache is true, we should probably append a random key or manage it otherwise?
137
+ // Actually, if disableCache is true, we likely shouldn't use useAsyncData at all if we want to force refresh?
138
+ // But sticking to standard pattern:
139
+ const { data } = await useAsyncData(key, fetcher)
140
+ if (data.value) {
141
+ const result = data.value as IDataResult
142
+ this.pageData.value = result.rows
143
+ this.rowCount.value = result.rowCount
144
+ this.initialLoad.value = false
145
+ this.loading.value = false
146
+ return
147
+ }
148
+ }
149
+
150
+ // Non-SSR client-side
151
+ if (!this.SSR.value) {
152
+ onNuxtReady(() => {
153
+ this.loading.value = true
154
+ fetcher().then((returnedData) => {
155
+ this.pageData.value = returnedData.rows
156
+ this.rowCount.value = returnedData.rowCount
157
+ this.initialLoad.value = false
158
+ this.loading.value = false
159
+ }).catch(() => {
160
+ this.initialLoad.value = false
161
+ this.loading.value = false
162
+ })
163
+ })
164
+ return
165
+ }
166
+
167
+ // Fallback
168
+ this.loading.value = true
169
+ const returnedData: IDataResult = await fetcher()
170
+ this.pageData.value = returnedData.rows
171
+ this.rowCount.value = returnedData.rowCount
172
+ this.initialLoad.value = false
173
+ this.loading.value = false
174
+ }
175
+
176
+ // ============================================
177
+ // Expand/Collapse API
178
+ // ============================================
179
+
180
+ toggleNode(nodeId: string | number): void {
181
+ if (this.expandedNodes.value.has(nodeId)) {
182
+ this.expandedNodes.value.delete(nodeId)
183
+ } else {
184
+ this.expandedNodes.value.add(nodeId)
185
+ }
186
+ this.expandedNodes.value = new Set(this.expandedNodes.value)
187
+ }
188
+
189
+ expand(nodeId: string | number): void {
190
+ this.expandedNodes.value.add(nodeId)
191
+ this.expandedNodes.value = new Set(this.expandedNodes.value)
192
+ }
193
+
194
+ collapse(nodeId: string | number): void {
195
+ this.expandedNodes.value.delete(nodeId)
196
+ this.expandedNodes.value = new Set(this.expandedNodes.value)
197
+ }
198
+
199
+ collapseAll(): void {
200
+ this.expandedNodes.value = new Set()
201
+ }
202
+
203
+ isExpanded(nodeId: string | number): boolean {
204
+ const isToggled = this.expandedNodes.value.has(nodeId)
205
+ return this.expandedByDefault ? !isToggled : isToggled
206
+ }
207
+ }