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

ngx-deep-signal

v0.0.3

Published

Deep/nested writable Angular signals with per-path dependency tracking (Proxy + field-level computed).

Readme

ngx-deep-signal

中文 README_zh.md

Deep / nested writable signals for Angular: reading state.user.name() only subscribes to that path, so updates to sibling fields (e.g. state.user.age) do not invalidate consumers that only read name. Leaf fields expose WritableSignal-style set / update; replacing whole branches still uses the root set / update.

Public API: deepSignal, type WritableDeepSignal (see lib/public-api.ts).

Requirements

  • Node.js and npm (see packageManager in root package.json)
  • Angular CLI 21.x (dev dependency)
  • For E2E: install Playwright browsers once (Chromium is enough for the default config):
npx playwright install chromium

Install

After building, consume from dist/ngx-deep-signal (file:../dist/ngx-deep-signal), or publish to npm:

npm install ngx-deep-signal

Usage

import { computed, effect, linkedSignal, untracked } from '@angular/core';
import { deepSignal } from 'ngx-deep-signal';

const state = deepSignal({ user: { name: 'Ada', age: 36, tags: ['dev'] as string[] } });

// Writable leaves
state.user.name.set('Bob');
state.user.age.update((n) => n + 1);
state.user.tags.update((tags) => [...tags, 'signal']);

// Whole tree
state.set({ user: { name: 'Ada', age: 36, tags: ['dev'] } });
state.update((d) => ({ user: { ...d.user, age: 18 } }));
// In `update`, `d` is a plain snapshot of `T`, not a deep signal — use `d.user.name`, not `d.user.name()`.

// Snapshots at any level (Signal call)
const root = state();           // { user: { name, age, tags } }
const user = state.user();      // { name, age, tags }
const name = state.user.name(); // string

// Angular Signal usage with deepSignal
const nameView = computed(() => state.user.name());
const ageUntracked = computed(() => `${state.user.name()}-${untracked(() => state.user.age())}`);

effect(() => {
  console.log('Name changed:', state.user.name());
});

const readonlyName = state.user.name.asReadonly();
const rootView = state.asReadonly();

// In app code, create `linkedSignal` inside an injection context (e.g. constructor / `inject` field initializer).
const alias = linkedSignal({
  source: () => state.user.name(),
  computation: (name) => name.toUpperCase(),
});

Nested records are split by key; arrays, Date, Map, etc. are treated as leaf values (same idea as NgRx deep signals).

Return type

deepSignal({ user: { name: 'Ada', age: 36 } }) is typed as:

WritableSignal<{
  user: WritableSignal<{
    name: WritableSignal<string>;
    age: WritableSignal<number>;
  }>;
}>

i.e. the root and every known-record branch are themselves WritableSignal of the same shape, recursively. Leaves (Date, arrays, primitives) are WritableSignal<leafType>.

RxJS interop and resource (experimental)

import { Injector, computed, inject, resource } from '@angular/core';
import { rxResource, toObservable, toSignal } from '@angular/core/rxjs-interop';
import { BehaviorSubject, of } from 'rxjs';

const injector = inject(Injector);
const n$ = new BehaviorSubject(1);
const n = toSignal(n$, { initialValue: 0, injector });

const res = resource({
  params: () => state.user.name(),
  loader: ({ params }) => Promise.resolve(`hi:${params}`),
  defaultValue: 'hi:',
  injector,
});

const name$ = toObservable(computed(() => state.user.name()), { injector });
const rxRes = rxResource({
  params: () => state.user.name(),
  stream: ({ params }) => of(`rx:${params}`),
  defaultValue: 'rx:',
  injector,
});

resource is experimental in Angular; afterRenderEffect is browser-oriented and is not covered here (unit tests run under jsdom/Node).

Angular Signal APIs covered by tests

  • WritableSignal#set and WritableSignal#update (leaf and root)
  • WritableSignal#asReadonly (root and nested leaves)
  • computed (single-path granularity, multiple tracked leaves, and equal to suppress downstream updates)
  • effect (with TestBed.runInInjectionContext + TestBed.tick()), including onCleanup when EffectRef#destroy is called
  • EffectRef#destroy to stop an effect
  • linkedSignal with a deep leaf as source
  • untracked
  • isSignal on the root and branch proxies (linked leaves implement WritableSignal but may not pass isSignal)
  • toSignal / toObservable from @angular/core/rxjs-interop (deep path reads via computed bridge where needed)
  • takeUntilDestroyed tied to a host DestroyRef with toObservable
  • firstValueFrom on toObservable for a deep leaf
  • resource with a deep leaf as reactive params (async loader)
  • rxResource with stream as Observable keyed by a deep leaf
  • signal read () at root / branch / leaf levels
  • Date treated as a writable leaf (NonRecord)

See lib/deep-signal.spec.ts for examples.

Build

npm run build
# or
ng build ngx-deep-signal

Output: dist/ngx-deep-signal/.

Unit tests

Vitest via Angular's unit-test builder; coverage uses @vitest/coverage-v8.

npm test
# CI-style
ng test ngx-deep-signal --no-watch

Coverage (text + summary + lcov + HTML under coverage/ngx-deep-signal/):

npm run test:coverage

Demo application

Standalone app under projects/demo/ that imports the built package ngx-deep-signal from dist/ngx-deep-signal (path mapping in root tsconfig.json).

npm start

Runs ng build ngx-deep-signal then ng serve demo (see prestart in package.json). Production bundle: npm run build:demodist/demo/.

The demo includes projects/demo/src/app/io-demo.ts, a child component using input(), output(), and model() together with the parent’s deepSignal ([displayName]="state.user.name()", (rename)state.user.name.set, [(guest)] ↔ parent guestDraft signal).

E2E tests (Playwright)

Uses playwright-ng-schematics with @playwright/test. Specs live in e2e/; config is playwright.config.ts. The e2e target on the demo project in angular.json starts demo:serve and runs Playwright against http://localhost:4200.

npm run e2e

Interactive UI mode:

npm run e2e:ui

Watch build (development)

npm run watch

Publish (npm)

cd dist/ngx-deep-signal
npm publish

Adjust version in lib/package.json before building if needed.

Repository layout

| Path | Role | |------|------| | lib/ | Library source, unit tests, ng-packagr / tsconfig configs | | projects/demo/ | Demo Angular app (ng serve demo) | | e2e/ | Playwright end-to-end specs | | playwright.config.ts | Playwright configuration | | dist/ngx-deep-signal/ | Packaged library after ng build / npm run build | | dist/demo/ | Demo app production build after ng build demo |