ngx-signal-router
v1.0.0
Published
Production-ready, type-safe Signal-first Angular Router state access library with built-in codecs, schema inference, and SSR/Zoneless support.
Maintainers
Readme
ngx-signal-router ⚡
Production-Ready, Type-Safe Signal-First Angular Router State Access Library.
ngx-signal-router brings native Angular Signals to ActivatedRoute parameter access, query strings, fragments, titles, route data, and whole-query schemas with built-in codecs, non-permissive type coercion, subscription sharing, and zero memory leaks.
Features
- ⚡ Native Angular Signals: Replaces RxJS subscriptions with reactive
Signal<T>getters for all route state. - 📦 Shared Subscriptions: Multiple derived signals off the same route share underlying subscriptions (bounded 6 source signals per route).
- 🛡️ Type-Safe Codec System: Includes non-permissive built-in codecs (
stringParam,numberParam,integerParam,booleanParam,dateParam,dateTimeParam,enumParam,literalParam,jsonParam,customParam,arrayParam,csvParam). - 📋 Whole-Query Schemas: Define entire query parameter contracts with
defineQuerySchema()and parse them into single typed signals viaqueryParamsSignal()orroute.schema(). - 🔐 Security Hardened: Built-in defense against prototype pollution (
__proto__,constructor,prototypefiltered). - 🧪 Consumer Testing Harness: Secondary entry point
ngx-signal-router/testingwithcreateRouteSignalTestContext()andprovideRouteSignalTesting()for synchronous unit testing without complex router mocks. - 🌐 SSR & Zoneless Native: Works seamlessly with Server-Side Rendering (SSR/Hydration) and Zoneless Angular 20–22 applications.
Installation
npm install ngx-signal-routerQuick Start
1. Unified Facade (injectRouteSignals)
import { Component } from '@angular/core';
import { injectRouteSignals, integerParam, booleanParam, defineQuerySchema, stringParam } from 'ngx-signal-router';
const filterSchema = defineQuerySchema({
page: integerParam({ defaultValue: 1, min: 1 }),
search: stringParam({ trim: true, defaultValue: '' }),
activeOnly: booleanParam({ defaultValue: true }),
});
@Component({
standalone: true,
template: `
<h1>User #{{ userId() }}</h1>
<p>Page: {{ query().page }}</p>
<p>Search: {{ query().search }}</p>
`,
})
export class UserDetailComponent {
readonly route = injectRouteSignals();
// Route parameter with integer coercion
readonly userId = this.route.param('id', integerParam({ required: true }));
// Whole-query schema signal
readonly query = this.route.schema(filterSchema);
}2. Standalone Signals
import { Component } from '@angular/core';
import { routeParamSignal, queryParamSignal, integerParam } from 'ngx-signal-router';
@Component({
standalone: true,
template: `<p>User ID: {{ userId() }} (Page {{ page() }})</p>`,
})
export class UserComponent {
readonly userId = routeParamSignal('id', integerParam({ required: true }));
readonly page = queryParamSignal('page', integerParam({ defaultValue: 1 }));
}Codecs
| Codec | Usage | Coercion Rules |
| :--- | :--- | :--- |
| stringParam | stringParam({ trim: true }) | String trimming, empty string fallback |
| integerParam | integerParam({ min: 1, max: 100 }) | Strict integer regex /^-?\d+$/ (rejects "12abc") |
| numberParam | numberParam({ defaultValue: 0 }) | Strict float parsing |
| booleanParam | booleanParam({ trueValues: ['true', '1'] }) | Strict explicit truthy/falsy list |
| dateParam | dateParam({ format: 'YYYY-MM-DD' }) | Strict calendar date validation (rejects invalid leap years) |
| enumParam | enumParam(StatusEnum) | String enum matching |
| arrayParam | arrayParam(integerParam()) | Coerces repeated query parameters (?tag=1&tag=2) |
| csvParam | csvParam(stringParam()) | Delimited string parsing (?tags=angular,signals) |
Testing (ngx-signal-router/testing)
import { TestBed } from '@angular/core';
import { createRouteSignalTestContext } from 'ngx-signal-router/testing';
import { UserComponent } from './user.component';
describe('UserComponent', () => {
it('should update state synchronously when query parameters change', () => {
const testContext = createRouteSignalTestContext({
params: { id: '42' },
queryParams: { page: '3' },
});
TestBed.configureTestingModule({
providers: [
{ provide: ActivatedRoute, useValue: testContext.route },
],
});
const fixture = TestBed.createComponent(UserComponent);
const instance = fixture.componentInstance;
expect(instance.userId()).toBe(42);
expect(instance.page()).toBe(3);
// Update state dynamically without full router navigation
testContext.setQueryParams({ page: '10' });
expect(instance.page()).toBe(10);
});
});License
MIT © Antigravity Team
