typescript
53 lines · 9 steps
Reactive filtering with Angular signals
An Angular standalone component uses signal inputs, models, and computed values to filter a product list without manual change detection.
Explained by
highlit
1import { Component, computed, input, model, signal } from '@angular/core';
2import { FormsModule } from '@angular/forms';
3
4interface Product {
5 id: number;
6 name: string;
7 category: string;
8 price: number;
9}
10
11@Component({
12 selector: 'app-product-filter',
13 standalone: true,
14 imports: [FormsModule],
15 template: `
16 <div class="filters">
17 <input
18 type="search"
19 placeholder="Search products…"
20 [(ngModel)]="query" />
21
22 <select [(ngModel)]="category">
23 <option value="">All categories</option>
24 @for (cat of categories(); track cat) {
25 <option [value]="cat">{{ cat }}</option>
26 }
27 </select>
28
29 <span class="count">{{ visible().length }} of {{ products().length }}</span>
30 </div>
31 `,
32})
33export class ProductFilterComponent {
34 readonly products = input.required<Product[]>();
35
36 readonly query = model('');
37 readonly category = model('');
38
39 protected readonly categories = computed(() =>
40 [...new Set(this.products().map((p) => p.category))].sort(),
41 );
42
43 readonly visible = computed(() => {
44 const term = this.query().trim().toLowerCase();
45 const cat = this.category();
46
47 return this.products().filter((p) => {
48 const matchesCategory = !cat || p.category === cat;
49 const matchesTerm = !term || p.name.toLowerCase().includes(term);
50 return matchesCategory && matchesTerm;
51 });
52 });
53}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1computed signals derive state automatically and recompute only when their dependencies change.
- 2model signals give two-way [(ngModel)] binding while staying part of the reactive graph.
- 3Deriving values from signals removes the need for manual change-detection or subscription cleanup.
Related explainers
typescript
import { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; import localeFrExtra from '@angular/common/locales/extra/fr'; import localeDe from '@angular/common/locales/de';
Locale-aware bootstrapping in Angular
i18n
localization
dependency-injection
Intermediate
8 steps
typescript
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi';
Validating env config at boot in NestJS
configuration
schema-validation
environment-variables
Intermediate
8 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { InjectRepository } from '@nestjs/typeorm';
A cache-aside country lookup in NestJS
cache-aside
dependency-injection
batch-lookup
Intermediate
8 steps
typescript
import { Injectable, effect, signal, computed } from '@angular/core'; interface Preferences { theme: 'light' | 'dark';
A signal-based preferences store in Angular
signals
state-management
persistence
Intermediate
7 steps
typescript
import { useEffect, useState } from "react"; interface Section { id: string;
Building a scroll-spy hook in React
custom-hooks
intersectionobserver
dom-observation
Intermediate
8 steps
typescript
import { Injectable, Scope, Inject, NotFoundException } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { Request } from 'express'; import { DataSource } from 'typeorm';
Per-tenant database connections in NestJS
multi-tenancy
connection-pooling
dependency-injection
Advanced
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/reactive-filtering-with-angular-signals-explained-typescript-4c3b/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.