npm package: @gitesh08/signal-utils
Description: Minimal Angular signal utilities: debounce, throttle, and merge signals without RxJS bloat. Lightweight, tree-shakable, zoneless-friendly.
Author: Gitesh Mahadik
Latest version: 1.0.4

# @gitesh08/signal-utils

Minimal, tree-shakable Angular signal utilities for debounce, throttle, and merge operations. Built for modern Angular applications without RxJS overhead.

[![npm version](https://img.shields.io/npm/v/@gitesh08/signal-utils.svg)](https://www.npmjs.com/package/@gitesh08/signal-utils)
![Angular](https://img.shields.io/badge/Angular-17+-red?logo=angular&logoColor=white)
![License](https://img.shields.io/badge/License-MIT-green)

## Why This Library?

Working with Angular signals for search inputs, scroll handlers, or form state? You probably need debouncing or throttling. This library gives you exactly that—nothing more, nothing less.

**What you get:**
- Debounced signals for search boxes and text inputs
- Throttled signals for scroll, resize, and mouse events  
- Signal merging for combining multiple reactive values
- Type-safe, validated inputs (throws on negative delays)
- Works in zoneless Angular apps
- Zero dependencies except `@angular/core`

**What you don't get:**
- Extra bloat
- RxJS as a peer dependency
- Complicated APIs

## Live Interactive Demo

Try debounce, throttle & merge in your browser – edit/fork freely:

[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/edit/signal-utils-demo)

## Installation
```bash
npm install @gitesh08/signal-utils
```

**Requirements:**
- Angular 17 or higher
- TypeScript 5.5+

## Quick Start

### Debounced Search

Stop hammering your API on every keystroke. Debounce waits for the user to stop typing before updating.
```typescript
import { Component, signal, effect } from '@angular/core';
import { debouncedSignal } from '@gitesh08/signal-utils';

@Component({
  selector: 'app-search',
  standalone: true,
  template: `
    <input type="text" [(ngModel)]="searchTerm" placeholder="Search products..." />
    <p *ngIf="debouncedTerm()">Searching: {{ debouncedTerm() }}</p>
  `
})
export class SearchComponent {
  searchTerm = signal('');
  debouncedTerm = debouncedSignal(this.searchTerm, 400);

  constructor() {
    effect(() => {
      const term = this.debouncedTerm();
      if (term.length > 2) {
        // Make your API call here
        console.log('Fetching results for:', term);
      }
    });
  }
}
```

### Throttled Window Events

Limit how often expensive operations run during rapid events like scrolling or resizing.
```typescript
import { Component, signal, effect, OnInit, OnDestroy } from '@angular/core';
import { throttledSignal } from '@gitesh08/signal-utils';

@Component({
  selector: 'app-resize-detector',
  standalone: true,
  template: `<div>Window: {{ width() }}px × {{ height() }}px</div>`
})
export class ResizeDetectorComponent implements OnInit, OnDestroy {
  private rawWidth = signal(window.innerWidth);
  private rawHeight = signal(window.innerHeight);
  
  width = throttledSignal(this.rawWidth, 200);
  height = throttledSignal(this.rawHeight, 200);

  private listener = () => {
    this.rawWidth.set(window.innerWidth);
    this.rawHeight.set(window.innerHeight);
  };

  ngOnInit() {
    window.addEventListener('resize', this.listener);
  }

  ngOnDestroy() {
    window.removeEventListener('resize', this.listener);
  }
}
```

### Merge Multiple Signals

Combine related signals into a single reactive object. Great for forms or grouped state.
```typescript
import { Component, signal } from '@angular/core';
import { mergeSignals } from '@gitesh08/signal-utils';

@Component({
  selector: 'app-user-form',
  standalone: true,
  template: `
    <input [(ngModel)]="firstName" placeholder="First name" />
    <input [(ngModel)]="lastName" placeholder="Last name" />
    <input [(ngModel)]="email" placeholder="Email" />
    
    <pre>{{ userData() | json }}</pre>
  `
})
export class UserFormComponent {
  firstName = signal('');
  lastName = signal('');
  email = signal('');

  userData = mergeSignals({
    firstName: this.firstName,
    lastName: this.lastName,
    email: this.email
  });
  
  // userData() returns: { firstName: '...', lastName: '...', email: '...' }
}
```

## API Reference

| Function | Description | Parameters | Returns |
|----------|-------------|------------|---------|
| `debouncedSignal<T>` | Delays signal updates until activity stops | `source: Signal<T>`, `delay?: number` (default: 300ms) | `Signal<T>` (readonly) |
| `throttledSignal<T>` | Limits signal updates to once per interval | `source: Signal<T>`, `interval?: number` (default: 300ms) | `Signal<T>` (readonly) |
| `mergeSignals<T>` | Combines multiple signals into one object | `signals: { [K in keyof T]: Signal<T[K]> }` | `Signal<T>` (computed) |

**Error handling:**
- Both `debouncedSignal` and `throttledSignal` throw an error if delay/interval is negative
- All functions must be called within an injection context (component, service, etc.)

## Development
```bash
# Build the library
ng build signal-utils

# Run tests
npm run test:lib

# Watch mode during development
npm run test:lib -- --watch
```

Tests are written with Vitest and use Angular's TestBed for proper signal context.

## Contributing

Found a bug? Have an idea? 

Please go through [CONTRIBUTING.md](https://github.com/Gitesh08/signal-utils/blob/main/CONTRIBUTING.md) before creating issue or PR.

## License

MIT © [Gitesh Mahadik](https://github.com/Gitesh08/signal-utils/blob/main/LICENSE)

---

**Built with signals, for signals.**