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

@kovalenko/base-filter

v2.0.2

Published

my base filter

Readme

@kovalenko/base-filter

Signal-based Angular filter that syncs with URL query params via qs.

Requirements

  • Angular >= 21
  • @angular/forms/signals (experimental signals-based forms API)

Install

npm i @kovalenko/base-filter

Peer dependencies:

npm i qs

Overview

BaseSignalFilter is a base class for typed URL filters. It:

  • reads query params from the URL (via qs) and populates filter fields
  • serializes filter state back to query params
  • integrates with @angular/forms/signals for reactive form binding
  • tracks page and limit automatically
  • exposes computed signals for use in components and HTTP requests

Basic usage

1. Define a filter class

import {computed, signal} from '@angular/core';
import {debounce, SchemaFn} from '@angular/forms/signals';
import {
  BaseSignalFilter,
  FilterProperty,
  TransformArray,
  TransformBoolean,
  TransformNumber,
} from '@kovalenko/base-filter';

export class ListFilter extends BaseSignalFilter {
  // Optional: configure field-level form schema (e.g. debounce)
  static override schema: SchemaFn<ListFilter> = (path) => {
    debounce(path.name, 300);
  };

  // Registered as a filter field, parsed as string[]
  @TransformArray()
  ids: string[] = [];

  // Registered as a filter field, plain string (with 300ms debounce via schema)
  @FilterProperty()
  name = '';

  // Parsed as boolean | null ('true' → true, 'false' → false, anything else → null)
  @TransformBoolean()
  active: boolean | null = null;

  // Parsed as number | null
  @TransformNumber()
  categoryId: number | null = null;

  // Namespace key: query params will be nested under ?f[ids]=...&f[name]=...
  override readonly key = signal('f').asReadonly();

  // Override serialized to inject extra fields that aren't filter inputs
  override readonly serialized = computed(() => ({
    ...this.serialize(),
    type: this.fixedType,
  }));

  // Fields set externally, not from URL
  fixedType?: string;
}

2. Use in a route component

import {Component, effect, inject} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {toSignal} from '@angular/core/rxjs-interop';
import {map} from 'rxjs';

@Component({
  selector: 'app-list',
  template: '',
})
export class ListRouteComponent {
  readonly filter = new ListFilter(25, inject(ActivatedRoute).queryParams);

  readonly #route = inject(ActivatedRoute);
  readonly #router = inject(Router);

  // Watch for the filter's namespace key in query params
  readonly #q = toSignal(
    this.#route.queryParams.pipe(map(p => p[this.filter.key()])),
  );

  constructor() {
    effect(() => {
      this.#router.navigate([], {
        queryParams: typeof this.#q() === 'string'
          ? {[this.filter.key()]: null}
          : this.filter.q(),
        relativeTo: this.#route,
        queryParamsHandling: 'merge',
      });
    });
  }
}

3. Use in a data component

import {Component, effect, inject, input, signal} from '@angular/core';
import {Subscription} from 'rxjs';

@Component({
  selector: 'app-table',
  template: '',
})
export class TableComponent {
  readonly filter = input.required<ListFilter>();
  readonly busy = signal(false);

  readonly #service = inject(MyService);
  #subs?: Subscription;

  constructor() {
    effect(this.#load);
  }

  readonly #load = (): void => {
    this.busy.set(true);
    this.#subs?.unsubscribe();

    // qsQueryParams() returns an HttpParams-compatible object serialized with qs
    this.#subs = this.#service
      .list(this.filter().qsQueryParams())
      .subscribe(data => {
        this.busy.set(false);
        // handle data
      });
  };
}

Constructor

new ListFilter(defaultLimit, queryParams?)

| Parameter | Type | Description | |---|---|---| | defaultLimit | number \| undefined | Default page size. Pass undefined to disable pagination. | | queryParams | Observable<any> | Typically ActivatedRoute.queryParams. When provided, the filter will reactively parse the URL on every change. |

If defaultLimit is provided and limitOptions is non-empty, the value must be included in limitOptions — otherwise an error is thrown.

API

Signals & computed

| Member | Type | Description | |---|---|---| | key | Signal<string> | Namespace key for query params. Override to nest params: ?key[field]=value. Empty string means no nesting. | | page | WritableSignal<number> | Current page. Resets to defaultPage (1) on filter change. | | limit | WritableSignal<number \| null> | Current page size. | | limitOptions | Signal<number[]> | Override to restrict allowed page sizes. | | editable | WritableSignal<this> | Current editable state of filter fields (excludes form itself). | | form | FieldTree<this> | Signals-based form tree for binding inputs. | | query | Signal<any> | Raw query params signal (from queryParams observable). | | serialized | Signal<Record<string, any>> | Serialized filter state. Override to add computed/external fields. | | q | Signal<Record<string, any>> | Query params object ready for Router.navigate. Respects key namespacing. | | qsQueryParams | Signal<QsHttpParams> | HttpParams-compatible object serialized with qs (array brackets format). Pass to HttpClient methods. | | isEmpty | Signal<boolean> | true when all filter fields are empty (ignores page and limit). |

Decorators

Decorators register fields for URL parsing and serialization. They must be applied to class properties.

@FilterProperty(serialize?)

Registers a plain filter field. Optionally accepts a custom SerializeFn for serialization.

@FilterProperty()
search = '';

// With custom serialization
@FilterProperty(v => v?.toUpperCase() ?? null)
status = '';

@TransformArray()

Wraps a single query param value in an array. Ensures the field is always T[] | null regardless of whether the URL contains one or multiple values.

@TransformArray()
ids: string[] = [];

@TransformBoolean()

Parses 'true'true, 'false'false, anything else → null. Also handles arrays of booleans.

@TransformBoolean()
active: boolean | null = null;

@TransformNumber()

Parses string to number. Returns null for NaN. Handles arrays of numbers (filters out NaN values).

@TransformNumber()
categoryId: number | null = null;

Types

// Custom parse function for a field
type ParseFn = (v: any, filter?: BaseSignalFilter) => any;

// Custom serialize function for a field
type SerializeFn = (v: any, filter?: BaseSignalFilter) => string | string[] | null;

QsHttpParams

Extends HttpParams. Serializes to a query string using qs with arrayFormat: 'brackets'.

// ?ids[]=1&ids[]=2 instead of ?ids=1&ids=2
this.http.get('/api/list', {params: this.filter.qsQueryParams()});

Entry points

@kovalenko/base-filter/luxon

Provides TransformLuxon — parses a query param ISO string into a luxon DateTime. Returns null for invalid or missing values.

npm i luxon
npm i -D @types/luxon
import {TransformLuxon} from '@kovalenko/base-filter/luxon';

export class ReportFilter extends BaseSignalFilter {
  @TransformLuxon()
  from: DateTime | null = null;

  @TransformLuxon()
  to: DateTime | null = null;
}

@kovalenko/base-filter/moment

Provides TransformMoment — parses a query param string into a moment object. Returns null for invalid or missing values.

npm i moment
import {TransformMoment} from '@kovalenko/base-filter/moment';

export class ReportFilter extends BaseSignalFilter {
  @TransformMoment()
  from: moment.Moment | null = null;
}

tsconfig.json requirements

Secondary entry points (/luxon, /moment) and Angular subpath imports require moduleResolution: bundler:

{
  "compilerOptions": {
    "moduleResolution": "bundler",
    "module": "ES2022",
    "target": "ES2022"
  }
}

License

MIT