npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@myrmidon/paged-data-browsers

v5.2.2

Published

Generic simple paged data browsers.

Readme

Paged Data Browsers

  • 📦 @myrmidon/paged-data-browsers

This library provides components to display filtered and paged data from some service:

  • a flat paged list of data.
  • a paged tree, to display hierarchically structured data, combining a tree view with paging.
  • a compact pager component for the tree.
  • a LRU cache service.

Paged List

Paged list is provided by a single utility class, PagedListStore. This templated class provides:

  • a filter object of type F;
  • a list element object of type E.

You provide:

  • a filter class to represent the filter for list elements (for F).
  • an element item class (for E).
  • a service to fetch data, implementing interface PagedListStoreService<F, E>.

In this workspace demo app, a local service provides mock data.

Using Paged List

▶️ (1) create a filter dumb component which just gets the filter and emits it when changes.

▶️ (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.

/**
 * Wrapper for a PagedListStore<F, E> instance.
 * This singleton service ensures that the corresponding component
 * preserves its state when navigating away and back.
 */
@Injectable({
  providedIn: 'root',
})
export class PagedListBrowserService {
  public readonly store: PagedListStore<__F__Filter, __E__>;

  constructor(service: __S__Service) {
    this.store = new PagedListStore<__F__Filter, __E__>(service);
  }
}
  • 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.

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.

For instance, here is how you can implement loadPage and wrap store functionality in a DocumentRepository implements PagedListStoreService<DocumentFilter, Document> singleton service:

// implement interface
private _store: PagedListStore<DocumentFilter, Document>;

constructor(private _docService: DocumentService) {
  this._store = new PagedListStore<DocumentFilter, Document>(this);
  this._loading$ = new BehaviorSubject<boolean>(false);
  // ... etc.
  this._store.reset();
}

public loadPage(
  pageNumber: number,
  pageSize: number,
  filter: DocumentFilter
): Observable<DataPage<Document>> {
  this._loading$.next(true);
  return this._docService.getDocuments(filter, pageNumber, pageSize).pipe(
    tap({
      next: () => this._loading$.next(false),
      error: () => this._loading$.next(false),
    })
  );
}

// wrap store functions
public async reset(): Promise<void> {
  this._loading$.next(true);
  try {
    await this._store.reset();
  } catch (error) {
    throw error;
  } finally {
    this._loading$.next(false);
  }
}

public async setFilter(filter: DocumentFilter): Promise<void> {
  this._loading$.next(true);
  try {
    await this._store.setFilter(filter);
  } catch (error) {
    throw error;
  } finally {
    this._loading$.next(false);
  }
}

public getFilter(): DocumentFilter {
  return this._store.getFilter();
}

public async setPage(pageNumber: number, pageSize: number): Promise<void> {
  this._loading$.next(true);
  try {
    await this._store.setPage(pageNumber, pageSize);
  } catch (error) {
    throw error;
  } finally {
    this._loading$.next(false);
  }
}

▶️ (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:


Paged Tree

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):

  • a global filter, applied to all the pages it handles;
  • per-node filters, which apply to the node's children to determine their page.

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.

overview

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, hasChildren is false and this will prevent further roundtrips to the server.

PagedTreeNode

A paged tree node (PagedTreeNode<F> where F is the type of the nodes filter) derives from TreeNode adding:

  • paging: paging information for children nodes (required). This is of type PagingInfo which has page number, page items count, and total items count.
  • expanded: for expanded/collapsed state. Initially this is undefined.
  • filter: filter for children nodes. If not set, all children are included.

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!

Services

PagedTreeStoreService

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:

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. 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).

The getNodes method can also get a mock-root parameter referring to two data models:

  • 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.
  • 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.

As a sample, compare the mock service 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.

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 getNodes method 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 source
  • persistChanges(): Save changes to your data source

PagedTreeStore

The tree logic is implemented by the top-level class PagedTreeStore<E,F>, where:

  • E is the element (node) type (a PagedTreeNode<F> or any derived type);
  • F is the filter type (a TreeNodeFilter or 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-state 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 parent
  • addSibling(anchorId, sibling, before): Add a sibling node relative to an anchor node
  • removeNode(nodeId): Remove a node and optionally its descendants
  • replaceNode(oldNodeId, newNode, keepDescendants): Replace a node with new data
  • saveChanges(): Persist all changes to the data source
  • hasUnsavedChanges(): Check if there are pending changes
  • clearUnsavedChanges(): Discard all pending changes

All editing operations automatically handle:

  • Position recalculation (x coordinates)
  • Parent hasChildren status 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 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).
  • 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.
  • 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).

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(id?): collapse all the descendants of the specified node. When called without an argument, all expanded root-level nodes are collapsed.

  • 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.

  • 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.

  • removeHilites(): remove all search highlights from every node.

  • 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.

  • 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.

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 emits filterChange.
    • a tree view.

Using Tree

▶️ (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:

export interface MockTreeNode extends PagedTreeNode<TreeNodeFilter> {
  count: number;
}

▶️ (2) to define your filter, create a filter class for this node, extending TreeNodeFilter (this parent just provides tags and parentId). For example:

export interface MockTreeFilter extends TreeNodeFilter {
  label?: string;
  minCount?: number;
  maxCount?: number;
}

▶️ (3) create a service implementing PagedTreeStoreService<F> to fetch a page of nodes from some data source (using filter of type F). For example:

@Injectable({
  providedIn: 'root',
})
export class MockPagedTreeStoreService
  implements PagedTreeStoreService<MockTreeFilter>
{
  /**
   * Get the specified page of nodes.
   * @param filter The filter.
   * @param pageNumber The page number.
   * @param pageSize The page size.
   * @param hasMockRoot Whether the root node is a mock node.
   */
  public getNodes(
    filter: MockTreeFilter,
    pageNumber: number,
    pageSize: number,
    hasMockRoot?: boolean
  ): Observable<DataPage<MockTreeNode>> {
    // TODO fetch nodes using filter and pageNumber, pageSize
    // and return the requested page of nodes with items,
    // pageNumber, pageSize, pageCount, total.
  }
}

⚠️ 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:

  /**
   * Get the specified page of nodes.
   * @param filter The filter.
   * @param pageNumber The page number.
   * @param pageSize The page size.
   * @param hasMockRoot Whether the root node is a mock node. Not used here.
   */
  public getNodes(
    filter: ThesEntryNodeFilter,
    pageNumber: number,
    pageSize: number,
    hasMockRoot?: boolean
  ): Observable<DataPage<ThesEntryPagedTreeNode>> {
    this.ensureNodes();

    // apply filtering
    let nodes = this._nodes.filter((n) => {
      if (filter.parentId !== undefined && filter.parentId !== null) {
        if (n.parentId !== filter.parentId) {
          return false;
        }
      } else {
        if (n.parentId) {
          return false;
        }
      }

      if (filter.label) {
        const filterValue = filter.label.toLowerCase();
        if (!n.label.toLowerCase().includes(filterValue)) {
          return false;
        }
      }
      return true;
    });

    // apply paging
    const startIndex = (pageNumber - 1) * pageSize;
    const endIndex = startIndex + pageSize;
    const pagedNodes = nodes.slice(startIndex, endIndex);

    // page and return
    const paged = nodes.slice(
      (pageNumber - 1) * pageSize,
      pageNumber * pageSize
    );
    return of({
      items: paged,
      pageNumber: pageNumber,
      pageSize: pageSize,
      pageCount: Math.ceil(nodes.length / pageSize),
      total: nodes.length,
    });
  }

▶️ (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.

▶️ (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.

// paged-tree-browser.service.ts
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class PagedTreeBrowserService {
  public readonly store: PagedTreeStore<__N__Node, __F__Filter>;

  constructor(service: __S__Service) {
    this.store = new PagedTreeStore<__N__Node, __F__Filter>(service);
  }
}

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.:

  /**
   * The store instance, built from the service.
   */
  public readonly store = computed(() => {
    const service = this.service();
    const store = new PagedTreeStore<
      ThesEntryPagedTreeNode,
      ThesEntryNodeFilter
    >(service);
    this.nodes$ = store.nodes$;
    this.filter$ = store.filter$;
    return store;
  });

▶️ (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:

💡 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 (findLabels, removeHilites).