@myrmidon/paged-data-browsers 5.2.1 → 5.2.2
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/README.md
CHANGED
|
@@ -1,59 +1,149 @@
|
|
|
1
1
|
# Paged Data Browsers
|
|
2
2
|
|
|
3
|
-
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
- [Node Component](#node-component)
|
|
14
|
-
- [History](#history)
|
|
15
|
-
- [5.2.0](#520)
|
|
16
|
-
- [5.1.3](#513)
|
|
17
|
-
- [5.1.2](#512)
|
|
18
|
-
- [5.1.1](#511)
|
|
19
|
-
- [5.1.0](#510)
|
|
20
|
-
- [5.0.2](#502)
|
|
21
|
-
- [5.0.1](#501)
|
|
22
|
-
- [5.0.0](#500)
|
|
23
|
-
- [4.0.2](#402)
|
|
24
|
-
- [4.0.1](#401)
|
|
25
|
-
- [4.0.0](#400)
|
|
26
|
-
- [3.0.0](#300)
|
|
27
|
-
- [2.0.5](#205)
|
|
28
|
-
- [2.0.4](#204)
|
|
29
|
-
- [2.0.3](#203)
|
|
30
|
-
- [2.0.2](#202)
|
|
31
|
-
- [2.0.1](#201)
|
|
32
|
-
- [2.0.0](#200)
|
|
33
|
-
- [1.1.0](#110)
|
|
34
|
-
|
|
35
|
-
This library provides simple components to display filtered and paged data from some service.
|
|
36
|
-
|
|
37
|
-
There are currently two components, one for displaying a flat list of data, and another to display hierarchically structured data, combining a tree view with paging.
|
|
38
|
-
|
|
39
|
-
Also, there is a LRU cache service and a compact pager used in the paged tree component.
|
|
3
|
+
- 📦 `@myrmidon/paged-data-browsers`
|
|
4
|
+
|
|
5
|
+
This library provides components to display filtered and paged data from some service:
|
|
6
|
+
|
|
7
|
+
- a flat paged list of data.
|
|
8
|
+
- a paged tree, to display hierarchically structured data, combining a tree view with paging.
|
|
9
|
+
- a compact pager component for the tree.
|
|
10
|
+
- a LRU cache service.
|
|
11
|
+
|
|
12
|
+
---
|
|
40
13
|
|
|
41
14
|
## Paged List
|
|
42
15
|
|
|
43
|
-
Paged list
|
|
16
|
+
Paged list is provided by a single utility class, [PagedListStore](src/lib/services/paged-list.store.ts). This templated class provides:
|
|
44
17
|
|
|
45
18
|
- a filter object of type `F`;
|
|
46
19
|
- a list element object of type `E`.
|
|
47
20
|
|
|
48
|
-
|
|
21
|
+
You provide:
|
|
49
22
|
|
|
50
|
-
- a filter class to represent
|
|
23
|
+
- a filter class to represent the filter for list elements (for `F`).
|
|
51
24
|
- an element item class (for `E`).
|
|
52
|
-
- a service to fetch data, implementing interface [PagedListStoreService<F, E>](src/lib/services/paged-list.store.ts).
|
|
25
|
+
- a service to fetch data, implementing interface [PagedListStoreService<F, E>](src/lib/services/paged-list.store.ts).
|
|
26
|
+
|
|
27
|
+
>In this workspace demo app, a [local service](../../../src/app/services/mock-paged-list-store.service.ts) provides mock data.
|
|
28
|
+
|
|
29
|
+
### Using Paged List
|
|
30
|
+
|
|
31
|
+
▶️ (1) create a **filter dumb component** which just gets the filter and emits it when changes.
|
|
32
|
+
|
|
33
|
+
- [paged list filter code example](../../../src/app/components/paged-list-filter/paged-list-filter.component.ts)
|
|
34
|
+
- [paged list filter template example](../../../src/app/components/paged-list-filter/paged-list-filter.component.html)
|
|
35
|
+
- [paged list filter styles example](../../../src/app/components/paged-list-filter/paged-list-filter.component.css)
|
|
36
|
+
|
|
37
|
+
▶️ (2) to persist list state, either **create a service wrapper** or an implementation of `PagedListStoreService<F,E>` for the store. This way the store will be used as a singleton with your data service.
|
|
38
|
+
|
|
39
|
+
- If you only need a service wrapper with no additional logic, just wrap it and expose the store as a property (see [paged list browser service example](src/app/services/paged-list-browser.service.ts)):
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
/**
|
|
43
|
+
* Wrapper for a PagedListStore<F, E> instance.
|
|
44
|
+
* This singleton service ensures that the corresponding component
|
|
45
|
+
* preserves its state when navigating away and back.
|
|
46
|
+
*/
|
|
47
|
+
@Injectable({
|
|
48
|
+
providedIn: 'root',
|
|
49
|
+
})
|
|
50
|
+
export class PagedListBrowserService {
|
|
51
|
+
public readonly store: PagedListStore<__F__Filter, __E__>;
|
|
52
|
+
|
|
53
|
+
constructor(service: __S__Service) {
|
|
54
|
+
this.store = new PagedListStore<__F__Filter, __E__>(service);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- If instead you have additional data and logic in your component, make your service implement `PagedListStoreService<F,E>` and expose wrapped functionality from the store; in addition, it will also include other data (e.g. lookup data) and their services, to fully back the browser component UI.
|
|
60
|
+
|
|
61
|
+
If you implement this interface, you must provide a `loadPage` method which will be used by the list browser infrastructure to load the requested page; while other methods will just wrap essential functions of the store.
|
|
62
|
+
|
|
63
|
+
For instance, here is how you can implement `loadPage` and wrap store functionality in a `DocumentRepository implements PagedListStoreService<DocumentFilter, Document>` singleton service:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
// implement interface
|
|
67
|
+
private _store: PagedListStore<DocumentFilter, Document>;
|
|
68
|
+
|
|
69
|
+
constructor(private _docService: DocumentService) {
|
|
70
|
+
this._store = new PagedListStore<DocumentFilter, Document>(this);
|
|
71
|
+
this._loading$ = new BehaviorSubject<boolean>(false);
|
|
72
|
+
// ... etc.
|
|
73
|
+
this._store.reset();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public loadPage(
|
|
77
|
+
pageNumber: number,
|
|
78
|
+
pageSize: number,
|
|
79
|
+
filter: DocumentFilter
|
|
80
|
+
): Observable<DataPage<Document>> {
|
|
81
|
+
this._loading$.next(true);
|
|
82
|
+
return this._docService.getDocuments(filter, pageNumber, pageSize).pipe(
|
|
83
|
+
tap({
|
|
84
|
+
next: () => this._loading$.next(false),
|
|
85
|
+
error: () => this._loading$.next(false),
|
|
86
|
+
})
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// wrap store functions
|
|
91
|
+
public async reset(): Promise<void> {
|
|
92
|
+
this._loading$.next(true);
|
|
93
|
+
try {
|
|
94
|
+
await this._store.reset();
|
|
95
|
+
} catch (error) {
|
|
96
|
+
throw error;
|
|
97
|
+
} finally {
|
|
98
|
+
this._loading$.next(false);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public async setFilter(filter: DocumentFilter): Promise<void> {
|
|
103
|
+
this._loading$.next(true);
|
|
104
|
+
try {
|
|
105
|
+
await this._store.setFilter(filter);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
throw error;
|
|
108
|
+
} finally {
|
|
109
|
+
this._loading$.next(false);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public getFilter(): DocumentFilter {
|
|
114
|
+
return this._store.getFilter();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public async setPage(pageNumber: number, pageSize: number): Promise<void> {
|
|
118
|
+
this._loading$.next(true);
|
|
119
|
+
try {
|
|
120
|
+
await this._store.setPage(pageNumber, pageSize);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
throw error;
|
|
123
|
+
} finally {
|
|
124
|
+
this._loading$.next(false);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
▶️ (3) **create your browser component** which gets the service at (2) injected, and exposes from its wrapped store the filter and page observables, handling filter and page changes:
|
|
130
|
+
|
|
131
|
+
- [paged list browser code example](src/app/components/paged-list-browser/paged-list-browser.component.ts)
|
|
132
|
+
- [paged list browser template example](src/app/components/paged-list-browser/paged-list-browser.component.html)
|
|
133
|
+
- [paged list browser styles example](src/app/components/paged-list-browser/paged-list-browser.component.css)
|
|
134
|
+
|
|
135
|
+
---
|
|
53
136
|
|
|
54
137
|
## Paged Tree
|
|
55
138
|
|
|
56
|
-
|
|
139
|
+
A paged tree is a tree with flat nodes (of type `PagedTreeNode<F>`), where nodes are dynamically loaded and paged. The tree has two **filters** (equal to or derived from `TreeNodeFilter`):
|
|
140
|
+
|
|
141
|
+
- a _global_ filter, applied to all the pages it handles;
|
|
142
|
+
- _per-node_ filters, which apply to the node's children to determine their page.
|
|
143
|
+
|
|
144
|
+
When expanding a node, a page of children nodes is fetched from a configured service. Each parent node maintains paging information about the current page of its children, orchestrating its filter and page number.
|
|
145
|
+
|
|
146
|
+

|
|
57
147
|
|
|
58
148
|
### Nodes
|
|
59
149
|
|
|
@@ -71,27 +161,19 @@ A paged tree is based on **nodes** of type `TreeNode` or its derived types. This
|
|
|
71
161
|
|
|
72
162
|
#### PagedTreeNode
|
|
73
163
|
|
|
74
|
-
A **paged tree node**
|
|
75
|
-
|
|
76
|
-
- a _filter_ for its children. Node filters derive from `TreeNodeFilter`, which just refers to `TreeNode`'s properties: so, the base filter just has a tag and a parent ID. Typically you derive your own filter from this type.
|
|
77
|
-
|
|
78
|
-
>⚠️ It is very important to take into account the parent ID property of the base filter when implementing the paging service.
|
|
164
|
+
A **paged tree node** (`PagedTreeNode<F>` where `F` is the type of the nodes filter) derives from `TreeNode` adding:
|
|
79
165
|
|
|
80
|
-
-
|
|
81
|
-
|
|
82
|
-
The base type for a paged tree node is thus `PagedTreeNode<F>`, where `F` is the type of the filter used for filtering nodes; this adds properties:
|
|
83
|
-
|
|
84
|
-
- `paging`: paging information. This is required.
|
|
166
|
+
- `paging`: paging information for children nodes (required). This is of type `PagingInfo` which has page number, page items count, and total items count.
|
|
85
167
|
- `expanded`: for expanded/collapsed state. Initially this is `undefined`.
|
|
86
|
-
- `filter`: filter
|
|
168
|
+
- `filter`: filter for children nodes. If not set, all children are included.
|
|
169
|
+
|
|
170
|
+
>Node filters derive from `TreeNodeFilter`, which refers to `TreeNode`'s properties: so, the base filter has only a tag and a parent ID. Typically you derive your own filter from this type. In your filter logic it is very important to take into account the parent ID property of the base filter!
|
|
87
171
|
|
|
88
172
|
### Services
|
|
89
173
|
|
|
90
174
|
#### PagedTreeStoreService
|
|
91
175
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
So, your data must be provided by this service which implements interface `PagedTreeStoreService<F>`, where `F` is the tree node filter type. The interface requires you to implement a single function, `getNodes`, which returns a specific page of nodes, applying the specified filter:
|
|
176
|
+
Data for the tree is dynamically loaded or loaded all at once by a service implementing `PagedTreeStoreService<F>`, where `F` is the tree node filter type. This interface requires to implement `getNodes` to fetch nodes with filtering and paging:
|
|
95
177
|
|
|
96
178
|
```ts
|
|
97
179
|
getNodes(
|
|
@@ -102,7 +184,14 @@ getNodes(
|
|
|
102
184
|
): Observable<DataPage<TreeNode>>;
|
|
103
185
|
```
|
|
104
186
|
|
|
105
|
-
>This service deals with `TreeNode`'s (or their derivations), returning a specific page of them. It is the service which is directly in contact with your data source, and its only purpose is to return a page of nodes.
|
|
187
|
+
>This service deals with `TreeNode`'s (or their derivations), returning a specific page of them. It is the service which is directly in contact with your data source, and its only purpose is to return a page of nodes. The store extends these nodes with paging and filtering using `PagedTreeNode`. If you want a single root mock node, your implementation of this service must provide it (with Y=0).
|
|
188
|
+
|
|
189
|
+
The `getNodes` method can also get a mock-root parameter referring to two data models:
|
|
190
|
+
|
|
191
|
+
- when you have a data model with a single root node for the whole tree, this parameter is `false`, because the root node comes from your data. It will have Y=1 and X=1, and contain all the tree nodes as its descendants.
|
|
192
|
+
- when instead your data lacks a single root node, and at its root level has many root nodes, this parameter can be either `true` or `false`. When `false`, multiple root level nodes are allowed but they are all loaded at once. So this is useful only when your root nodes number is limited. When instead the parameter is `true`, it is up to your service implementation to provide a mock root node for the multiple root nodes, which become its children. This mock root will have Y=0.
|
|
193
|
+
|
|
194
|
+
>As a sample, compare the [mock service](./src/app/services/paged-tree-browser.service.ts) used in this shell. Its `getData` method gets this parameter and when true it creates a root node with Y=0, X=1, and id=1; in this case, the root nodes are made children of it. Otherwise, it just goes with multiple root nodes. In the example shell, you are allowed to toggle this parameter to see these two models in action. In a real-world application instead you will usually stick to a single model.
|
|
106
195
|
|
|
107
196
|
#### EditablePagedTreeStoreService
|
|
108
197
|
|
|
@@ -134,16 +223,14 @@ A base implementation `EditablePagedTreeStoreServiceBase<F>` is provided that ha
|
|
|
134
223
|
- `fetchNodes()`: Get nodes from your actual data source
|
|
135
224
|
- `persistChanges()`: Save changes to your data source
|
|
136
225
|
|
|
137
|
-
This design ensures full backward compatibility - existing readonly implementations continue to work unchanged.
|
|
138
|
-
|
|
139
226
|
#### PagedTreeStore
|
|
140
227
|
|
|
141
|
-
The tree logic is implemented by the `PagedTreeStore<E,F>`, where:
|
|
228
|
+
The tree logic is implemented by the top-level class `PagedTreeStore<E,F>`, where:
|
|
142
229
|
|
|
143
230
|
- `E` is the element (node) type (a `PagedTreeNode<F>` or any derived type);
|
|
144
231
|
- `F` is the filter type (a `TreeNodeFilter` or any derived type).
|
|
145
232
|
|
|
146
|
-
The store is used to load and manage a flat list of nodes. Every tree node in the list is extended with page number, page count and total items, plus expansion-
|
|
233
|
+
The store is used to load and manage a flat list of nodes. Every tree node in the list is extended with page number, page count and total items, plus expansion-state metadata. Users can expand and collapse nodes, browse through pages of children, and filter them.
|
|
147
234
|
|
|
148
235
|
When using an `EditablePagedTreeStoreService`, the store also supports editing operations:
|
|
149
236
|
|
|
@@ -165,7 +252,6 @@ All editing operations automatically handle:
|
|
|
165
252
|
The essential store **data** are:
|
|
166
253
|
|
|
167
254
|
- the flat _list of paged nodes_ (exposed in `nodes$`). This flat list is populated by using an instance of the paged tree store's service (`PagedTreeStoreService<F>`), injected in the store constructor together with its options (of type `PagedTreeStoreOptions`). Among other things, the options specify whether there must be a single mock root node, and the default page size. Once retrieved, pages can be fetched from an internal LRU cache (which is cleared when calling `reset`).
|
|
168
|
-
- a list of tree _tags_ (exposed in `tags$`).
|
|
169
255
|
- a _global filter_ (exposed in `filter$`). This gets combined with (overridden by) node-specific filters, when specified. You can set it with `setFilter`. To set the filter for the children of a specific node use `setNodeFilter`.
|
|
170
256
|
- the _page size_ (`pageSize`), a get/set property, initially set by the options injected in the store constructor. Setting this property resets the store (like calling `reset`).
|
|
171
257
|
|
|
@@ -185,11 +271,15 @@ The main **methods** are:
|
|
|
185
271
|
- `expand(id)`: expand the specified node (if it has children and is collapsed).
|
|
186
272
|
- `expandAll(id)`: expand all the descendants of the specified node.
|
|
187
273
|
- `collapse(id)`: collapse the specified node if expanded.
|
|
188
|
-
- `collapseAll()`:
|
|
274
|
+
- `collapseAll(id?)`: collapse all the descendants of the specified node. When called without an argument, all expanded root-level nodes are collapsed.
|
|
189
275
|
|
|
190
276
|
- `changePage(parentId, pageNumber)`: change the page of children nodes of the specified parent node.
|
|
191
277
|
- `setFilter(filter)`: sets the global filter, resetting the store.
|
|
192
278
|
- `setNodeFilter(id, filter)`: sets the node-filter for the specified node, resetting its children page number to 1.
|
|
279
|
+
- `findLabels(searchText)`: search all nodes (loading lazily as needed) for labels containing the search text, highlight matched nodes, and expand their ancestors so they become visible.
|
|
280
|
+
- `removeHilites()`: remove all search highlights from every node.
|
|
281
|
+
- `ensureNodeVisible(id, expanded?, refresh?)`: find a node by ID, expand all its ancestors, navigate to the page containing it, optionally set its expanded state, and highlight it. Useful for programmatic navigation to a specific node.
|
|
282
|
+
- `getAnchorForDeletedNode(id)`: returns the ID of the node that should be selected after deleting the given node (next sibling → previous sibling → parent → null). Useful for keeping focus meaningful after a deletion.
|
|
193
283
|
|
|
194
284
|
The store is a plain TypeScript class. If you want to **persist** it in the app, you can wrap it in a service and inject the service into your component. If you don't need this, just implement your data service to provide nodes via `getNodes`. Otherwise, you can wrap the store like e.g. here using a singleton for a single page of nodes in the demo app:
|
|
195
285
|
|
|
@@ -259,113 +349,160 @@ You should provide your components for:
|
|
|
259
349
|
- a filters component for global filters. This dummy component gets a `filter$` and emits `filterChange`.
|
|
260
350
|
- a tree view.
|
|
261
351
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
### 5.2.0
|
|
265
|
-
|
|
266
|
-
- 2026-03-02: ⚠️ migrated to `OnPush`.
|
|
352
|
+
### Using Tree
|
|
267
353
|
|
|
268
|
-
|
|
354
|
+
▶️ (1) to define your node, **create a node class** extending `PagedTreeNode<F>`. This parent (derived from `TreeNode`) already has properties for IDs, label, filtering and paging. In your derived class you should add data linked to the node. For example, here data is just the `count` property:
|
|
269
355
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
- 2026-01-07: updated Angular and packages.
|
|
276
|
-
|
|
277
|
-
### 5.1.2
|
|
278
|
-
|
|
279
|
-
- 2025-11-22:
|
|
280
|
-
- ⚠️ upgraded to Angular 21.
|
|
281
|
-
- ⚠️ migrated to `pnpm`.
|
|
282
|
-
|
|
283
|
-
### 5.1.1
|
|
284
|
-
|
|
285
|
-
- 2025-10-05:
|
|
286
|
-
- fixes in editable tree store.
|
|
287
|
-
- minor improvements in paged tree store.
|
|
288
|
-
- better documentation.
|
|
289
|
-
|
|
290
|
-
### 5.1.0
|
|
291
|
-
|
|
292
|
-
- 2025-09-23:
|
|
293
|
-
- added thesaurus tree demo page in app.
|
|
294
|
-
- added `findLabels` and `removeHilites` to tree store.
|
|
295
|
-
- updated package and tsconfig for library (version 5.0.3).
|
|
296
|
-
|
|
297
|
-
### 5.0.2
|
|
298
|
-
|
|
299
|
-
- 2025-06-15:
|
|
300
|
-
- fix to tree expand all.
|
|
301
|
-
- updated Angular and packages.
|
|
302
|
-
|
|
303
|
-
### 5.0.1
|
|
304
|
-
|
|
305
|
-
- 2025-06-10: removed dirty check in reset. When reset is requested, just do it.
|
|
306
|
-
|
|
307
|
-
### 5.0.0
|
|
308
|
-
|
|
309
|
-
- 2025-05-29: ⚠️ upgraded to Angular 20.
|
|
310
|
-
- 2025-05-25: updated Angular and packages.
|
|
311
|
-
|
|
312
|
-
### 4.0.2
|
|
313
|
-
|
|
314
|
-
- 2025-01-01: updated packages.
|
|
315
|
-
|
|
316
|
-
### 4.0.1
|
|
317
|
-
|
|
318
|
-
- 2024-12-04: added CSS variables to browser tree node.
|
|
319
|
-
|
|
320
|
-
### 4.0.0
|
|
356
|
+
```ts
|
|
357
|
+
export interface MockTreeNode extends PagedTreeNode<TreeNodeFilter> {
|
|
358
|
+
count: number;
|
|
359
|
+
}
|
|
360
|
+
```
|
|
321
361
|
|
|
322
|
-
|
|
323
|
-
- replaced dependencies with new standalone `ngx-mat-...` libraries.
|
|
324
|
-
- upgraded to standalone.
|
|
325
|
-
- upgraded to functional injection.
|
|
326
|
-
- upgraded to use signal-based properties and events.
|
|
327
|
-
- fixes to tree store for collapse.
|
|
362
|
+
▶️ (2) to define your filter, **create a filter class** for this node, extending `TreeNodeFilter` (this parent just provides `tags` and `parentId`). For example:
|
|
328
363
|
|
|
329
|
-
|
|
364
|
+
```ts
|
|
365
|
+
export interface MockTreeFilter extends TreeNodeFilter {
|
|
366
|
+
label?: string;
|
|
367
|
+
minCount?: number;
|
|
368
|
+
maxCount?: number;
|
|
369
|
+
}
|
|
370
|
+
```
|
|
330
371
|
|
|
331
|
-
|
|
372
|
+
▶️ (3) **create a service** implementing `PagedTreeStoreService<F>` to fetch a page of nodes from some data source (using filter of type `F`). For example:
|
|
332
373
|
|
|
333
|
-
|
|
374
|
+
```ts
|
|
375
|
+
@Injectable({
|
|
376
|
+
providedIn: 'root',
|
|
377
|
+
})
|
|
378
|
+
export class MockPagedTreeStoreService
|
|
379
|
+
implements PagedTreeStoreService<MockTreeFilter>
|
|
380
|
+
{
|
|
381
|
+
/**
|
|
382
|
+
* Get the specified page of nodes.
|
|
383
|
+
* @param filter The filter.
|
|
384
|
+
* @param pageNumber The page number.
|
|
385
|
+
* @param pageSize The page size.
|
|
386
|
+
* @param hasMockRoot Whether the root node is a mock node.
|
|
387
|
+
*/
|
|
388
|
+
public getNodes(
|
|
389
|
+
filter: MockTreeFilter,
|
|
390
|
+
pageNumber: number,
|
|
391
|
+
pageSize: number,
|
|
392
|
+
hasMockRoot?: boolean
|
|
393
|
+
): Observable<DataPage<MockTreeNode>> {
|
|
394
|
+
// TODO fetch nodes using filter and pageNumber, pageSize
|
|
395
|
+
// and return the requested page of nodes with items,
|
|
396
|
+
// pageNumber, pageSize, pageCount, total.
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
```
|
|
334
400
|
|
|
335
|
-
|
|
401
|
+
>⚠️ Always remember to **filter by parent ID** in your implementation of `getNodes`! For instance, here is a filter by label, which first filters by parent ID:
|
|
336
402
|
|
|
337
|
-
|
|
403
|
+
```ts
|
|
404
|
+
/**
|
|
405
|
+
* Get the specified page of nodes.
|
|
406
|
+
* @param filter The filter.
|
|
407
|
+
* @param pageNumber The page number.
|
|
408
|
+
* @param pageSize The page size.
|
|
409
|
+
* @param hasMockRoot Whether the root node is a mock node. Not used here.
|
|
410
|
+
*/
|
|
411
|
+
public getNodes(
|
|
412
|
+
filter: ThesEntryNodeFilter,
|
|
413
|
+
pageNumber: number,
|
|
414
|
+
pageSize: number,
|
|
415
|
+
hasMockRoot?: boolean
|
|
416
|
+
): Observable<DataPage<ThesEntryPagedTreeNode>> {
|
|
417
|
+
this.ensureNodes();
|
|
418
|
+
|
|
419
|
+
// apply filtering
|
|
420
|
+
let nodes = this._nodes.filter((n) => {
|
|
421
|
+
if (filter.parentId !== undefined && filter.parentId !== null) {
|
|
422
|
+
if (n.parentId !== filter.parentId) {
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
} else {
|
|
426
|
+
if (n.parentId) {
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (filter.label) {
|
|
432
|
+
const filterValue = filter.label.toLowerCase();
|
|
433
|
+
if (!n.label.toLowerCase().includes(filterValue)) {
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return true;
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// apply paging
|
|
441
|
+
const startIndex = (pageNumber - 1) * pageSize;
|
|
442
|
+
const endIndex = startIndex + pageSize;
|
|
443
|
+
const pagedNodes = nodes.slice(startIndex, endIndex);
|
|
444
|
+
|
|
445
|
+
// page and return
|
|
446
|
+
const paged = nodes.slice(
|
|
447
|
+
(pageNumber - 1) * pageSize,
|
|
448
|
+
pageNumber * pageSize
|
|
449
|
+
);
|
|
450
|
+
return of({
|
|
451
|
+
items: paged,
|
|
452
|
+
pageNumber: pageNumber,
|
|
453
|
+
pageSize: pageSize,
|
|
454
|
+
pageCount: Math.ceil(nodes.length / pageSize),
|
|
455
|
+
total: nodes.length,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
```
|
|
338
459
|
|
|
339
|
-
-
|
|
340
|
-
- updated Angular.
|
|
341
|
-
- updated package peer dependencies.
|
|
342
|
-
- added i18n.
|
|
343
|
-
- added `hideFilter` to paged tree browser.
|
|
460
|
+
▶️ (4) filter UI: create a **paged tree filter dumb component** which just gets the filter and emits it when changes. This component will be used both as the global filter editor and as the per-node filters editor. In the latter case, it will appear as a popup. So you need to inject a `MatDialogRef` and its data in the constructor, and call dialog ref's `close` function when changing the filter. Also, note that in the HTML template we add margin when the filter component is wrapped into a dialog.
|
|
344
461
|
|
|
345
|
-
|
|
462
|
+
- [paged tree filter example code](src/app/components/paged-tree-filter/paged-tree-filter.component.ts)
|
|
463
|
+
- [paged tree filter example template](src/app/components/paged-tree-filter/paged-tree-filter.component.html)
|
|
464
|
+
- [paged tree filter example styles](src/app/components/paged-tree-filter/paged-tree-filter.component.css)
|
|
346
465
|
|
|
347
|
-
|
|
466
|
+
▶️ (5) (optional) singleton service wrapper: if you want to persist the tree state, **create a service wrapper** for the store which gets instantiated with your data fetch service (implementing interface `PagedTreeStoreService<F>`), like in this example. This wrapper will also provide app-specific options for configuring the store, including the `hasMockRoot` parameter, should it be required.
|
|
348
467
|
|
|
349
|
-
|
|
468
|
+
```ts
|
|
469
|
+
// paged-tree-browser.service.ts
|
|
470
|
+
import { Injectable } from '@angular/core';
|
|
350
471
|
|
|
351
|
-
|
|
472
|
+
@Injectable({
|
|
473
|
+
providedIn: 'root',
|
|
474
|
+
})
|
|
475
|
+
export class PagedTreeBrowserService {
|
|
476
|
+
public readonly store: PagedTreeStore<__N__Node, __F__Filter>;
|
|
352
477
|
|
|
353
|
-
|
|
478
|
+
constructor(service: __S__Service) {
|
|
479
|
+
this.store = new PagedTreeStore<__N__Node, __F__Filter>(service);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
```
|
|
354
483
|
|
|
355
|
-
|
|
356
|
-
- updated Angular and packages.
|
|
357
|
-
- added get root node method to tree store.
|
|
358
|
-
- more comments.
|
|
484
|
+
If instead you are going to directly use the service, inject the service in a new store, in your browser component (see nr.6) e.g.:
|
|
359
485
|
|
|
360
|
-
|
|
486
|
+
```ts
|
|
487
|
+
/**
|
|
488
|
+
* The store instance, built from the service.
|
|
489
|
+
*/
|
|
490
|
+
public readonly store = computed(() => {
|
|
491
|
+
const service = this.service();
|
|
492
|
+
const store = new PagedTreeStore<
|
|
493
|
+
ThesEntryPagedTreeNode,
|
|
494
|
+
ThesEntryNodeFilter
|
|
495
|
+
>(service);
|
|
496
|
+
this.nodes$ = store.nodes$;
|
|
497
|
+
this.filter$ = store.filter$;
|
|
498
|
+
return store;
|
|
499
|
+
});
|
|
500
|
+
```
|
|
361
501
|
|
|
362
|
-
-
|
|
363
|
-
- 2024-08-02:
|
|
364
|
-
- upgraded Angular and packages.
|
|
365
|
-
- replaced `color` with `class`.
|
|
366
|
-
- refactored styles for new Material.
|
|
502
|
+
▶️ (6) tree-view browser: **create a tree browser component** which gets the wrapper service at (5) injected, and exposes from its wrapped store the filter and page observables, handling filter and page changes:
|
|
367
503
|
|
|
368
|
-
|
|
504
|
+
- [paged tree browser code example](src/app/components/paged-tree-browser/paged-tree-browser.component.ts)
|
|
505
|
+
- [paged tree browser template example](src/app/components/paged-tree-browser/paged-tree-browser.component.html)
|
|
506
|
+
- [paged tree browser styles example](src/app/components/paged-tree-browser/paged-tree-browser.component.css)
|
|
369
507
|
|
|
370
|
-
|
|
371
|
-
- 2023-11-09: upgraded to Angular 17.
|
|
508
|
+
💡 If you want to add search by label with multiple matches highlight, add a form to your component and call the corresponding methods of the store service, like in the [thesaurus paged browser tree component sample](./src/app/components/thesaurus-paged-tree-browser/thesaurus-paged-tree-browser.component.ts) (`findLabels`, `removeHilites`).
|