🎯 Angular Signals: Component Signals
The Walkie-Talkie Story đź“»
Imagine you have a team of friends building a treehouse. Each friend has a walkie-talkie. When one friend finds a nail, they press a button and say “Found a nail!” — and everyone who needs to know hears it instantly.
That’s exactly what Angular Component Signals do.
Your components are like friends in the treehouse. They need to:
- Receive messages (Signal Inputs)
- Send messages back (Signal Outputs)
- Share toys that anyone can change (Model Inputs)
- Find things in the treehouse (Signal Queries)
- Translate between old and new walkie-talkies (toSignal & toObservable)
Let’s meet each one!
📥 Signal Inputs: Receiving Messages
What Is It?
A Signal Input is like a mailbox on your component’s door. Parent components drop letters (data) into it. Your component reads them as signals.
Simple Example
Old Way (Before):
@Input() name = '';
New Way (Signal Input):
import { input } from '@angular/core';
name = input<string>('');
Why It’s Better
Think of it like this:
- Old mailbox: You had to keep checking if mail arrived
- Signal mailbox: It rings a bell when mail arrives!
// Reading the value
const currentName = this.name();
// React when it changes
effect(() => {
console.log('Name changed to:', this.name());
});
Required Inputs
Sometimes you MUST have a letter. No letter = no entry!
// This MUST be provided by parent
userId = input.required<number>();
Transform Inputs
What if the letter is in French but you speak English? Transform it!
age = input(0, {
transform: (value: string) => parseInt(value, 10)
});
Parent sends
"25"(string) → You receive25(number)
🔄 Model Inputs: Two-Way Walkie-Talkies
What Is It?
A Model Input is special. It’s like a walkie-talkie where both people can talk AND listen. When you change the value, the parent knows. When the parent changes it, you know!
Simple Example
Think of a toy that two kids share:
- Kid A changes the color → Kid B sees the new color
- Kid B changes the color → Kid A sees the new color
import { model } from '@angular/core';
// This creates a two-way connection!
checked = model<boolean>(false);
In the Template
<!-- Parent component -->
<my-toggle [(checked)]="isOn"></my-toggle>
The banana-in-a-box [()] means: “send AND receive!”
Changing the Value
// Read current value
const isChecked = this.checked();
// Change it (parent will know!)
this.checked.set(true);
// Or update based on current value
this.checked.update(current => !current);
graph TD A["Parent Component"] -->|sends value| B["Model Input"] B -->|notifies back| A B -->|updates| C["Child Component"] C -->|changes value| B
📤 Signal Outputs: Sending Messages Out
What Is It?
A Signal Output is like a megaphone. When something happens in your component, you shout it out so parents can hear.
Simple Example
Imagine a doorbell. When someone presses it, the house hears “DING!”
import { output } from '@angular/core';
// Create a doorbell
buttonClicked = output<void>();
// Ring it!
onClick() {
this.buttonClicked.emit();
}
With Data
What if the doorbell could say WHO is at the door?
import { output } from '@angular/core';
// Doorbell that announces the visitor
visitorArrived = output<string>();
announceVisitor(name: string) {
this.visitorArrived.emit(name);
}
Parent Listening
<my-doorbell (visitorArrived)="greet($event)">
</my-doorbell>
greet(visitorName: string) {
console.log('Hello, ' + visitorName + '!');
}
🔍 Signal Queries: Finding Things
What Is It?
Signal Queries help you find things inside your component. Like a flashlight in a dark room!
There are 4 types:
| Query | What It Finds |
|---|---|
viewChild |
One thing in your template |
viewChildren |
Many things in your template |
contentChild |
One thing a parent put inside you |
contentChildren |
Many things a parent put inside you |
viewChild: Find One Thing
import { viewChild, ElementRef } from '@angular/core';
// Find the input box
searchBox = viewChild<ElementRef>('searchInput');
<input #searchInput type="text">
focusSearch() {
// searchBox is a signal, so call it!
this.searchBox()?.nativeElement.focus();
}
viewChildren: Find Many Things
import { viewChildren } from '@angular/core';
// Find ALL buttons
buttons = viewChildren<ElementRef>('btn');
<button #btn>One</button>
<button #btn>Two</button>
<button #btn>Three</button>
countButtons() {
// Returns an array inside a signal
return this.buttons().length; // 3
}
contentChild & contentChildren
These find things that a parent placed inside your component.
// Child component
import { contentChild, contentChildren } from '@angular/core';
// Find projected content
header = contentChild<ElementRef>('cardHeader');
items = contentChildren<ItemComponent>(ItemComponent);
<!-- Parent using the child -->
<my-card>
<h2 #cardHeader>Hello!</h2>
<app-item></app-item>
<app-item></app-item>
</my-card>
graph TD A["viewChild"] -->|finds ONE| B["in your template"] C["viewChildren"] -->|finds MANY| B D["contentChild"] -->|finds ONE| E["projected by parent"] F["contentChildren"] -->|finds MANY| E
🌉 toSignal: Turning Streams into Signals
What Is It?
Imagine you have an old radio that plays music (Observable). But your new house only has signal-speakers (Signals).
toSignal is the adapter that connects them!
Simple Example
import { toSignal } from '@angular/core/rxjs-interop';
import { interval } from 'rxjs';
// Old radio: plays a number every second
const timer$ = interval(1000);
// Adapter: now it's a signal!
timerSignal = toSignal(timer$);
In Templates
<!-- No need for async pipe! -->
<p>Seconds: {{ timerSignal() }}</p>
With Initial Value
What if the radio hasn’t started playing yet?
// Start with 0 while waiting
timerSignal = toSignal(timer$, { initialValue: 0 });
With HTTP Calls
users = toSignal(
this.http.get<User[]>('/api/users'),
{ initialValue: [] }
);
đź’ˇ Pro Tip:
toSignalautomatically subscribes AND unsubscribes. No memory leaks!
đź”™ toObservable: Signals Back to Streams
What Is It?
Now imagine the opposite. Your friend has a new signal-walkie-talkie, but their house only has old radios.
toObservable converts signals back to observables.
When Would You Need This?
- Using RxJS operators like
debounceTime,switchMap - Connecting to libraries that expect observables
- Complex async operations
Simple Example
import { signal } from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';
import { debounceTime } from 'rxjs/operators';
// A signal
searchTerm = signal('');
// Convert to observable
searchTerm$ = toObservable(this.searchTerm);
// Now use RxJS magic!
results$ = this.searchTerm$.pipe(
debounceTime(300),
switchMap(term => this.searchService.find(term))
);
Practical Use Case: Search with Debounce
@Component({...})
export class SearchComponent {
query = signal('');
results = toSignal(
toObservable(this.query).pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(q => this.api.search(q))
),
{ initialValue: [] }
);
}
graph LR A["Signal"] -->|toObservable| B["Observable"] B -->|RxJS operators| C["Transformed Observable"] C -->|toSignal| D["Signal Again!"]
🎯 Quick Summary
| Feature | Purpose | Example |
|---|---|---|
input() |
Receive data from parent | name = input<string>('') |
input.required() |
Must receive data | id = input.required<number>() |
model() |
Two-way binding | checked = model(false) |
output() |
Send events to parent | clicked = output<void>() |
viewChild() |
Find one element | viewChild('ref') |
viewChildren() |
Find many elements | viewChildren('ref') |
contentChild() |
Find projected element | contentChild('ref') |
contentChildren() |
Find projected elements | contentChildren(Comp) |
toSignal() |
Observable → Signal | toSignal(obs$) |
toObservable() |
Signal → Observable | toObservable(sig) |
🎉 You Did It!
You now understand Angular’s Component Signals! They’re like a modern walkie-talkie system for your components:
- Signal Inputs = Mailboxes that ring when mail arrives
- Model Inputs = Two-way walkie-talkies
- Signal Outputs = Megaphones to announce events
- Signal Queries = Flashlights to find things
- toSignal = Old radio → New speaker adapter
- toObservable = New speaker → Old radio adapter
Go build something amazing! 🚀
