@myrmidon/paged-data-browsers
v5.1.4
Published
Generic simple paged data browsers.
Readme
Paged Data Browsers
This library provides simple components to display filtered and paged data from some service.
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.
Also, there is a LRU cache service and a compact pager used in the paged tree component.
Paged List
Paged list support is provided by a single utility class, PagedListStore. This templated class provides logic for:
- a filter object of type
F; - a list element object of type
E.
So you need:
- a filter class to represent your elements filter (for
F). - an element item class (for
E). - a service to fetch data, implementing interface PagedListStoreService<F, E>. In this shell, a local service provides mock data.
Paged Tree
The paged tree is a tree where each node can page and filter its direct children.
Nodes
TreeNode
A paged tree is based on nodes of type TreeNode or its derived types. This basic tree node contains:
id: a unique numeric ID.parentId: the ID of the parent, or undefined if it's a root-level node.y: Y is the node depth, starting from 1. As we often need a single root node (for paging its children), unless we have a small number of root nodes, when your data source does not return a single root node you have the option of using a mock root. In this case, the mock root's Y level will be 0 and all the other nodes will descend from it. Anyway you can also have many root level nodes, whose parent ID is undefined; but in this case all these nodes will appear at once without any paging, because paging is governed by the parent node.x: X is the ordinal number of the node among its siblings. The first child of a given node has X=1, the second has X=2, and so forth.label: a human-friendly label for the node.tag: a tag string for virtually categorizing or grouping the node in some way.hasChildren: true if the node has children, false if it has no children; undefined if this has not yet been determined. This is initially undefined, until the service fetching data has been used to retrieve the children of a node. After that, if no nodes were returned,hasChildrenisfalseand this will prevent further roundtrips to the server.
PagedTreeNode
A paged tree node type (PagedTreeNode<F>) derives from this basic TreeNode, adding two essential components for filtering and paging its children:
- a filter for its children. Node filters derive from
TreeNodeFilter, which just refers toTreeNode's properties: so, the base filter just has a tag and a parent ID. Typically you derive your own filter from this type.
⚠️ It is very important to take into account the parent ID property of the base filter when implementing the paging service.
- paging information for its children. This is of type
PagingInfowhich has page number, page items count, and total items count.
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:
paging: paging information. This is required.expanded: for expanded/collapsed state. Initially this isundefined.filter: filter object. If not set, there will be no filtering and thus all the children will be included.
Services
PagedTreeStoreService
Typically, data for the tree is dynamically loaded. It can also be loaded all at once; in both cases, data will always be provided by a service implementing the same interface. This is the paged tree store's service, which fetches data from your data source on behalf of the paged tree store.
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:
getNodes(
filter: F,
pageNumber: number,
pageSize: number,
hasMockRoot?: boolean
): Observable<DataPage<TreeNode>>;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. It is the store which extends these nodes with paging and filtering data using thePagedTreeNodetype. Note that if you want a single root mock node, your implementation of this service must provide it (with Y=0).
EditablePagedTreeStoreService
For editing scenarios, you can use EditablePagedTreeStoreService<F> which extends the base service interface with editing capabilities. This service maintains changes in memory until they are explicitly saved, allowing for:
- Change tracking: All modifications (add, remove, update) are tracked internally
- Optimistic updates: The service's
getNodesmethod returns data including unsaved changes - Batch saving: All changes can be committed to the data source at once
- ID mapping: Temporary IDs for new nodes are mapped to permanent IDs after saving
The interface adds these methods:
// Save all pending changes to the data source
saveChanges(): Observable<Map<number, number>>;
// Check if there are any unsaved changes
hasChanges(): boolean;
// Clear all pending changes without saving
clearChanges(): void;
// Get all pending change operations
getChanges(): ChangeOperation[];A base implementation EditablePagedTreeStoreServiceBase<F> is provided that handles all the change tracking logic. You only need to implement:
fetchNodes(): Get nodes from your actual data sourcepersistChanges(): Save changes to your data source
This design ensures full backward compatibility - existing readonly implementations continue to work unchanged.
PagedTreeStore
The tree logic is implemented by the PagedTreeStore<E,F>, where:
Eis the element (node) type (aPagedTreeNode<F>or any derived type);Fis the filter type (aTreeNodeFilteror any derived type).
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-related metadata. Users can expand and collapse nodes, browse through pages of children, and filter them.
When using an EditablePagedTreeStoreService, the store also supports editing operations:
addChild(parentId, child, first): Add a child node to the specified parentaddSibling(anchorId, sibling, before): Add a sibling node relative to an anchor noderemoveNode(nodeId): Remove a node and optionally its descendantsreplaceNode(oldNodeId, newNode, keepDescendants): Replace a node with new datasaveChanges(): Persist all changes to the data sourcehasUnsavedChanges(): Check if there are pending changesclearUnsavedChanges(): Discard all pending changes
All editing operations automatically handle:
- Position recalculation (x coordinates)
- Parent
hasChildrenstatus updates - Cache invalidation
- UI updates for expanded nodes
The essential store data are:
- 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 typePagedTreeStoreOptions). 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 callingreset). - a list of tree tags (exposed in
tags$). - a global filter (exposed in
filter$). This gets combined with (overridden by) node-specific filters, when specified. You can set it withsetFilter. To set the filter for the children of a specific node usesetNodeFilter. - 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 callingreset).
To initialize the store, you call reset, which loads root nodes (via its service's getNodes) and their direct children. Users then expand or collapse nodes, change page, and set the global or node filters as desired.
The main methods are:
reset(): reset the store, loading root node(s) and peeking at their direct children to determine whether they can be expanded.clear(): clear the whole store, emptying the cache and removing all the nodes.clearCache(): clears the pages cache.hasCachedPage(pageNumber, filter): checks whether the specified page is cached.isEmpty(): true if the list is empty.getNodes(): gets all the nodes in the list.getRootNode(): returns the root node, i.e. the first node in the list (unless this is empty).getChildren(id): gets the children of the specified node.expand(id): expand the specified node (if it has children and is collapsed).expandAll(id): expand all the descendants of the specified node.collapse(id): collapse the specified node if expanded.collapseAll(): collpase all the descendants of the specified node.changePage(parentId, pageNumber): change the page of children nodes of the specified parent node.setFilter(filter): sets the global filter, resetting the store.setNodeFilter(id, filter): sets the node-filter for the specified node, resetting its children page number to 1.
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:
@Injectable({
providedIn: 'root',
})
export class PagedTreeBrowserService {
public store: PagedTreeStore<MockTreeNode, MockTreeFilter>;
constructor() {
this.store = new PagedTreeStore<MockTreeNode, MockTreeFilter>(
new MockPagedTreeStoreService()
);
}
/**
* Set the mock root node for the paged list store. This is for testing
* purposes; in a real world scenario, this parameter would hardly change
* once set.
* @param on Whether to enable the mock root node.
*/
public setHasMockRoot(on: boolean): void {
this.store = new PagedTreeStore<MockTreeNode, MockTreeFilter>(
new MockPagedTreeStoreService(),
{
...DEFAULT_PAGED_LIST_STORE_OPTIONS,
hasMockRoot: on,
} as PagedTreeStoreOptions
);
}
}Node Component
The only piece of UI provided by this library is for displaying a single node of the tree. The component for visualizing each single node of the paged tree is BrowserTreeNodeComponent. This wraps some HTML content providing a toggle button to expand/collapse the node, a paging control for the node's children, and a button to edit the node's filter.
Unless you are satisfied with these essential data, you can then provide the HTML content to display more node's data inside this component, e.g.:
<pdb-browser-tree-node [node]="node">
<!-- add your data here... -->
<your-node-view [node]="node" />
<pdb-browser-tree-node>This component API has:
- ▶️
node(PagedTreeNode) to display. - ▶️
paging(PagingInfo): optional paging information about node's children. - ▶️
debug(boolean) flag to toggle debug information in the view. - ▶️
hideLabel(boolean) flag to hide the node's loc and label. This is useful if you want to provide your own view for the node, between the expansion toggle and the filter edit button. In this case, in your consumer template provide your own view as the content of this component. If instead you are fine with the default loc and label, and just want to add more data to the view, then you can just add your own content to this component's template, without setting this property to true. - ▶️
hideLoc(boolean): true to hide the node's location (X and Y). - ▶️
hideFilter(boolean): true to hide the node's filter edit button. Do this when you do not want to allow users to apply per-node filters. - ▶️
hidePaging(boolean): true to hide the node's paging control unless hovered. - ▶️
indentSize(number): the indent size for the node's children. - ▶️
rangeWidth(number): the width of the range view. This is a small horizontal bar showing you the location of the page in the set of nodes. - 🔥
toggleExpandedRequest(PagedTreeNode): emitted when the user wants to toggle the expanded state of the node. - 🔥
changePageRequest(PageChangeRequest): emitted when the user wants to change the page number of the node's children. - 🔥
editNodeFilterRequest(PagedTreeNode): emitted when the user wants to edit the node's filter for its children.
You should provide your components for:
- the node's filters component. This is used both for global and node-specific filters (in the latter case as a popup).
- the tree browser component. This combines:
- a filters component for global filters. This dummy component gets a
filter$and emitsfilterChange. - a tree view.
- a filters component for global filters. This dummy component gets a
History
5.1.3
- 2026-01-13: adding ensure node visible functionality to the paged tree store so that we can preserve focus on node when updating it in backend data.
- 2026-01-12:
- updated Angular and packages.
- refactored thesauri components moving them from demo app to a new
@myrmidon/cadmus-thesaurus-storelibrary. Once the components here reach production stage, the library will importThesaurusEntryfrom the Cadmus core package and be packaged and exported to become part of the Cadmus system. - fixes to editable thesaurus component.
- 2026-01-07: updated Angular and packages.
5.1.2
- 2025-11-22:
- ⚠️ upgraded to Angular 21.
- ⚠️ migrated to
pnpm.
5.1.1
- 2025-10-05:
- fixes in editable tree store.
- minor improvements in paged tree store.
- better documentation.
5.1.0
- 2025-09-23:
- added thesaurus tree demo page in app.
- added
findLabelsandremoveHilitesto tree store. - updated package and tsconfig for library (version 5.0.3).
5.0.2
- 2025-06-15:
- fix to tree expand all.
- updated Angular and packages.
5.0.1
- 2025-06-10: removed dirty check in reset. When reset is requested, just do it.
5.0.0
- 2025-05-29: ⚠️ upgraded to Angular 20.
- 2025-05-25: updated Angular and packages.
4.0.2
- 2025-01-01: updated packages.
4.0.1
- 2024-12-04: added CSS variables to browser tree node.
4.0.0
- 2024-12-02: ⚠️ refactored code:
- replaced dependencies with new standalone
ngx-mat-...libraries. - upgraded to standalone.
- upgraded to functional injection.
- upgraded to use signal-based properties and events.
- fixes to tree store for collapse.
- replaced dependencies with new standalone
3.0.0
- 2024-11-22: ⚠️ upgraded to Angular 19.
2.0.5
- 2024-10-24: added
hideLocto tree node component.
2.0.4
- 2024-10-23:
- updated Angular.
- updated package peer dependencies.
- added i18n.
- added
hideFilterto paged tree browser.
2.0.3
- 2024-10-16: added
clearmethod to stores.
2.0.2
- 2024-10-02: fix to paged tree store page change when any nodes in old page are expanded.
2.0.1
- 2024-10-02:
- updated Angular and packages.
- added get root node method to tree store.
- more comments.
2.0.0
- 2024-08-04: ⚠️ breaking changes to tree browser: refactored to remove tag-based multiple trees.
- 2024-08-02:
- upgraded Angular and packages.
- replaced
colorwithclass. - refactored styles for new Material.
1.1.0
- 2024-05-23: upgraded to Angular 18.
- 2023-11-09: upgraded to Angular 17.
