docs: add signals tutorial (#62750)

PR Close #62750
This commit is contained in:
Ben Hong 2025-09-08 18:37:11 -04:00 committed by Andrew Scott
parent b1aa2d016a
commit d0f0c0824f
70 changed files with 1107 additions and 831 deletions

View file

@ -8,9 +8,9 @@ In this activity, you'll learn how to:
- Display its value in a template
- Update the signal value using `set()` and `update()` methods
<hr />
Let's build an interactive user status system with signals!
Let's build an interactive user status system with signals.
<hr />
<docs-workflow>
@ -38,18 +38,25 @@ export class App {
</docs-step>
<docs-step title="Display the signal value in the template">
Update the template to display the current user status by calling the signal `userStatus()` with parentheses.
Update the status indicator to display the current user status by:
1. Binding the signal to the class attribute with `[class]="userStatus()"`
2. Displaying the status text by replacing `???` with `{{ userStatus() }}`
```html
<div class="user-profile">
<h1>User Dashboard</h1>
<div class="status-indicator" [class]="userStatus()">
<span class="status-dot"></span>
Status: {{ userStatus() }}
</div>
<!-- Update from: -->
<div class="status-indicator offline">
<span class="status-dot"></span>
Status: ???
</div>
<!-- To: -->
<div class="status-indicator" [class]="userStatus()">
<span class="status-dot"></span>
Status: {{ userStatus() }}
</div>
```
Notice how we call the signal `userStatus()` with parentheses to read its value.
</docs-step>
<docs-step title="Add methods to update the signal">
@ -69,25 +76,19 @@ The `set()` method replaces the signal's value entirely with a new value.
</docs-step>
<docs-step title="Add buttons to control the status">
Add control buttons to the template for changing the user's status.
<docs-step title="Wire up the control buttons">
The buttons are already in the template. Now connect them to your methods by adding:
1. Click handlers with `(click)`
2. Disabled states with `[disabled]` when already in that status
```html
<div class="user-profile">
<h1>User Dashboard</h1>
<div class="status-indicator" [class]="userStatus()">
<!-- Status indicator content omitted -->
</div>
<div class="status-controls">
<button (click)="goOnline()" [disabled]="userStatus() === 'online'">
Go Online
</button>
<button (click)="goOffline()" [disabled]="userStatus() === 'offline'">
Go Offline
</button>
</div>
</div>
<!-- Add bindings to the existing buttons: -->
<button (click)="goOnline()" [disabled]="userStatus() === 'online'">
Go Online
</button>
<button (click)="goOffline()" [disabled]="userStatus() === 'offline'">
Go Offline
</button>
```
</docs-step>
@ -105,17 +106,13 @@ The `update()` method takes a function that receives the current value and retur
</docs-step>
<docs-step title="Add the toggle button">
Add the toggle button to your status controls.
<docs-step title="Add the toggle button handler">
The toggle button is already in the template. Connect it to your `toggleStatus()` method:
```html
<div class="status-controls">
<!-- "Go Online" button -->
<!-- "Go Offline" button -->
<button (click)="toggleStatus()" class="toggle-btn">
Toggle Status
</button>
</div>
<button (click)="toggleStatus()" class="toggle-btn">
Toggle Status
</button>
```
</docs-step>

View file

@ -45,3 +45,34 @@
.away .status-dot {
background-color: #ff9800;
}
.status-controls {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
button {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
transition: background-color 0.2s;
}
button:not(:disabled) {
background-color: #007bff;
color: white;
}
button:disabled {
background-color: #6c757d;
color: #fff;
cursor: not-allowed;
opacity: 0.6;
}
.status-controls .toggle-btn {
background-color: #28a745;
}

View file

@ -6,12 +6,24 @@ import {Component, ChangeDetectionStrategy} from '@angular/core';
template: `
<div class="user-profile">
<h1>User Dashboard</h1>
<!-- TODO: Update class binding and display userStatus() -->
<div class="status-indicator offline">
<span class="status-dot"></span>
Status: ???
</div>
<!-- TODO: Add status control buttons -->
<div class="status-controls">
<!-- TODO: Add (click) and [disabled] bindings -->
<button>
Go Online
</button>
<button>
Go Offline
</button>
<button class="toggle-btn">
Toggle Status
</button>
</div>
</div>
`,
styleUrl: './app.css',
@ -19,4 +31,7 @@ import {Component, ChangeDetectionStrategy} from '@angular/core';
})
export class App {
// TODO: Create a userStatus signal with type 'online' | 'offline' and the initial value of 'offline'
// TODO: Add goOnline() method using set()
// TODO: Add goOffline() method using set()
// TODO: Add toggleStatus() method using update()
}

View file

@ -1,8 +1,8 @@
# Reacting to signal changes with effect
Now that you've learned [querying child elements with signal queries](/tutorials/signals/8-query-child-elements-with-signal-queries), let's explore how to react to signal changes with effects. Effects are functions that run automatically when their dependencies change, making them perfect for side effects like logging, DOM manipulation, or API calls.
Now that you've learned [querying child elements with signal queries](/tutorials/signals/9-query-child-elements-with-signal-queries), let's explore how to react to signal changes with effects. Effects are functions that run automatically when their dependencies change, making them perfect for side effects like logging, DOM manipulation, or API calls.
**Important: Effects should be your last resort.** Always prefer `computed()` for derived values and `linkedSignal()` for values that can be both derived and manually set. If you find yourself copying data from one signal to another with an effect, it's a sign you should move your source-of-truth higher up and use `computed()` or `linkedSignal()` instead. Effects are best for syncing signal state to imperative, non-signal APIs.
**Important: Effects should be the last API you reach for.** Always prefer `computed()` for derived values and `linkedSignal()` for values that can be both derived and manually set. If you find yourself copying data from one signal to another with an effect, it's a sign you should move your source-of-truth higher up and use `computed()` or `linkedSignal()` instead. Effects are best for syncing signal state to imperative, non-signal APIs.
In this activity, you'll learn how to use the `effect()` function appropriately for legitimate side effects that respond to signal changes.
@ -17,7 +17,7 @@ Add `effect` to your existing imports.
```ts
// Add effect to existing imports
import {Component, signal, computed, effect} from '@angular/core';
import {Component, signal, computed, effect, ChangeDetectionStrategy} from '@angular/core';
```
The `effect` function creates a reactive side effect that runs automatically when any signals it reads change.

View file

@ -8,7 +8,12 @@ import {Component, signal, computed, effect, ChangeDetectionStrategy} from '@ang
<div class="controls">
<button (click)="toggleTheme()">
Switch to {{ theme() === 'light' ? 'Dark' : 'Light' }} Theme
Switch to
@if (theme() === 'light') {
Dark
} @else {
Light
} Theme
</button>
@if (!isLoggedIn()) {
@ -21,7 +26,13 @@ import {Component, signal, computed, effect, ChangeDetectionStrategy} from '@ang
<div class="info">
<p>Current theme: {{ theme() }}</p>
<p>User: {{ username() }}</p>
<p>Status: {{ isLoggedIn() ? 'Logged in' : 'Logged out' }}</p>
<p>Status:
@if (isLoggedIn()) {
Logged in
} @else {
Logged out
}
</p>
</div>
<div class="demo">

View file

@ -10,7 +10,12 @@ import {Component, signal, computed, ChangeDetectionStrategy} from '@angular/cor
<div class="controls">
<button (click)="toggleTheme()">
Switch to {{ theme() === 'light' ? 'Dark' : 'Light' }} Theme
Switch to
@if (theme() === 'light') {
Dark
} @else {
Light
} Theme
</button>
@if (!isLoggedIn()) {
@ -23,7 +28,13 @@ import {Component, signal, computed, ChangeDetectionStrategy} from '@angular/cor
<div class="info">
<p>Current theme: {{ theme() }}</p>
<p>User: {{ username() }}</p>
<p>Status: {{ isLoggedIn() ? 'Logged in' : 'Logged out' }}</p>
<p>Status:
@if (isLoggedIn()) {
Logged in
} @else {
Logged out
}
</p>
</div>
<div class="demo">

View file

@ -4,9 +4,9 @@ Now that you've learned [how to create and update signals](/tutorials/signals/1-
In this activity, you'll learn how to use the `computed()` function to create derived state that updates automatically when the underlying signals change.
<hr />
Let's enhance our user status system by adding computed values that derive information from our user status signal. The starter code now includes three status options: `'online'`, `'away'`, and `'offline'`.
Let's enhance our user status system by adding computed values that derive information from our user status signal.
<hr />
<docs-workflow>
@ -15,7 +15,7 @@ Add `computed` to your existing imports.
```ts
// Add computed to existing imports
import {Component, signal, computed} from '@angular/core';
import {Component, signal, computed, ChangeDetectionStrategy} from '@angular/core';
```
</docs-step>
@ -64,41 +64,35 @@ This demonstrates how computed signals can perform calculations and combine mult
</docs-step>
<docs-step title="Display the computed values in the template">
Update your template to use the computed signals.
The template already has placeholders showing "Loading...". Replace them with your computed signals:
1. For notifications, replace `Loading...` with an @if block:
```html
<!-- Update the status indicator with the current status -->
<div class="status-indicator" [class]="userStatus()">
<span class="status-dot"></span>
Status: {{ userStatus() }}
</div>
<!-- Add new section to display computed values -->
<div class="status-info">
<div class="notifications">
<strong>Notifications:</strong>
@if (notificationsEnabled()) {
Enabled
} @else {
Disabled
}
</div>
<div class="message">
<strong>Message:</strong> {{ statusMessage() }}
</div>
<div class="working-hours">
<strong>Within Working Hours:</strong>
@if (isWithinWorkingHours()) {
Yes
} @else {
No
}
</div>
</div>
<!-- Existing status controls remain unchanged -->
@if (notificationsEnabled()) {
Enabled
} @else {
Disabled
}
```
2. For the message, replace `Loading...` with:
```html
{{ statusMessage() }}
```
3. For working hours, replace `Loading...` with an @if block:
```html
@if (isWithinWorkingHours()) {
Yes
} @else {
No
}
```
Notice how computed signals are called just like regular signals - with parentheses!
</docs-step>
</docs-workflow>

View file

@ -13,13 +13,19 @@ import {Component, signal, ChangeDetectionStrategy} from '@angular/core';
<div class="status-info">
<div class="notifications">
<strong>Notifications:</strong> Loading...
<strong>Notifications:</strong>
<!-- TODO: Replace 'Loading...' with @if block using notificationsEnabled() -->
Loading...
</div>
<div class="message">
<strong>Message:</strong> Loading...
<strong>Message:</strong>
<!-- TODO: Replace 'Loading...' with {{ statusMessage() }} -->
Loading...
</div>
<div class="working-hours">
<strong>Within Working Hours:</strong> Loading...
<strong>Within Working Hours:</strong>
<!-- TODO: Replace 'Loading...' with @if block using isWithinWorkingHours() -->
Loading...
</div>
</div>

View file

@ -1,23 +1,21 @@
# Deriving state with linked signals
In the [previous lesson](/tutorials/signals/2-deriving-state-with-computed-signals), you created a computed signal for `notificationsEnabled` that automatically followed your user status. But what if users want to manually disable notifications even when they're online? That's where linked signals come in.
Now that you've learned [how to derive state with computed signals](/tutorials/signals/2-deriving-state-with-computed-signals), you created a computed signal for `notificationsEnabled` that automatically followed your user status. But what if users want to manually disable notifications even when they're online? That's where linked signals come in.
Linked signals are writable signals that maintain a reactive connection to their source signals. They're perfect for creating state that normally follows a computation but can be overridden when needed.
In this activity, you'll learn how `linkedSignal()` differs from `computed()` by converting your notifications example.
In this activity, you'll learn how `linkedSignal()` differs from `computed()` by enhancing the previous user status system's computed `notificationsEnabled` to a writable linked signal.
<hr />
Let's enhance our user status system by converting the read-only computed `notificationsEnabled` to a writable linked signal.
<docs-workflow>
<docs-step title="Import linkedSignal function">
Add `linkedSignal` to your existing imports.
```ts
// Add linkedSignal to existing imports
import {Component, signal, computed, linkedSignal} from '@angular/core';
// Add linkedSignal to existing imports
import {Component, signal, computed, linkedSignal, ChangeDetectionStrategy} from '@angular/core';
```
</docs-step>
@ -49,13 +47,13 @@ toggleNotifications() {
This is the key difference: computed signals are read-only, but linked signals can be updated manually while still maintaining their reactive connection.
</docs-step>
<docs-step title="Update the template to show manual control">
<docs-step title="Update the template to add manual notification control">
Update your template to add a toggle button for notifications:
```html
<div class="status-info">
<div class="notifications">
<strong>Notifications:</strong>
<strong>Notifications:</strong>
@if (notificationsEnabled()) {
Enabled
} @else {
@ -94,4 +92,4 @@ Excellent! You've learned the key differences between computed and linked signal
- **Use computed when**: The value should always be calculated
- **Use linkedSignal when**: You need a default computation that can be overridden
In the next lesson, you'll learn [how to manage async data with signals](/tutorials/signals/4-managing-async-data-with-signals)!
In the next lesson, you'll learn [how to manage async data with signals](/tutorials/signals/4-managing-async-data-with-signals)!

View file

@ -23,6 +23,18 @@
display: inline-block;
}
.status-indicator.online .status-dot {
background: #4caf50;
}
.status-indicator.away .status-dot {
background: #ff9800;
}
.status-indicator.offline .status-dot {
background: #f44336;
}
.status-info {
margin: 20px 0;
padding: 16px;
@ -32,7 +44,7 @@
border-left-color: var(--status-color);
}
.availability, .message {
.notifications, .message, .working-hours {
margin: 8px 0;
}
@ -95,4 +107,12 @@ button:disabled {
.status-controls .toggle-btn {
background-color: #28a745;
}
.notifications .override-btn {
background-color: #ffc107;
color: #212529;
margin-left: 8px;
font-size: 0.8em;
padding: 4px 8px;
}

View file

@ -103,17 +103,17 @@ export class App {
}
toggleStatus() {
this.userStatus.update((current: 'online' | 'offline' | 'away') => {
switch (current) {
case 'offline':
return 'online';
case 'online':
return 'away';
case 'away':
return 'offline';
default:
return 'offline';
}
});
const current = this.userStatus();
switch (current) {
case 'offline':
this.userStatus.set('online');
break;
case 'online':
this.userStatus.set('away');
break;
case 'away':
this.userStatus.set('offline');
break;
}
}
}

View file

@ -23,6 +23,18 @@
display: inline-block;
}
.status-indicator.online .status-dot {
background: #4caf50;
}
.status-indicator.away .status-dot {
background: #ff9800;
}
.status-indicator.offline .status-dot {
background: #f44336;
}
.status-info {
margin: 20px 0;
padding: 16px;
@ -32,7 +44,7 @@
border-left-color: var(--status-color);
}
.availability, .message {
.notifications, .message, .working-hours {
margin: 8px 0;
}
@ -95,4 +107,12 @@ button:disabled {
.status-controls .toggle-btn {
background-color: #28a745;
}
.notifications .override-btn {
background-color: #ffc107;
color: #212529;
margin-left: 8px;
font-size: 0.8em;
padding: 4px 8px;
}

View file

@ -11,10 +11,15 @@ import {Component, signal, computed, ChangeDetectionStrategy} from '@angular/cor
<span class="status-dot"></span>
Status: {{ userStatus() }}
</div>
<div class="status-info">
<div class="notifications">
<strong>Notifications:</strong> Loading...
<strong>Notifications:</strong>
@if (notificationsEnabled()) {
Enabled
} @else {
Disabled
}
<!-- TODO: Add button to toggle notifications -->
</div>
<div class="message">
@ -29,7 +34,7 @@ import {Component, signal, computed, ChangeDetectionStrategy} from '@angular/cor
}
</div>
</div>
<div class="status-controls">
<button (click)="goOnline()" [disabled]="userStatus() === 'online'">
Go Online
@ -50,7 +55,7 @@ import {Component, signal, computed, ChangeDetectionStrategy} from '@angular/cor
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
userStatus = signal<'online' | 'offline' | 'away'>('offline');
userStatus = signal<'online' | 'away' | 'offline'>('offline');
// Currently using computed - read-only
notificationsEnabled = computed(() => this.userStatus() === 'online');
@ -80,10 +85,10 @@ export class App {
});
// TODO: Add toggleNotifications method to manually set notificationsEnabled
toggleNotifications() {
// TODO: Implement to toggle notificationsEnabled using set()
}
// toggleNotifications() {
// // This works with linkedSignal but would error with computed!
// this.notificationsEnabled.set(!this.notificationsEnabled());
// }
goOnline() {
this.userStatus.set('online');
@ -98,17 +103,17 @@ export class App {
}
toggleStatus() {
this.userStatus.update((current: 'online' | 'offline' | 'away') => {
switch (current) {
case 'offline':
return 'online';
case 'online':
return 'away';
case 'away':
return 'offline';
default:
return 'offline';
}
});
const current = this.userStatus();
switch (current) {
case 'offline':
this.userStatus.set('online');
break;
case 'online':
this.userStatus.set('away');
break;
case 'away':
this.userStatus.set('offline');
break;
}
}
}

View file

@ -2,12 +2,10 @@
Now that you've learned [how to derive state with linked signals](/tutorials/signals/3-deriving-state-with-linked-signals), let's explore how to handle asynchronous data with the Resource API. The Resource API provides a powerful way to manage async operations using signals, with built-in loading states, error handling, and request management.
In this activity, you'll learn how to use the `resource()` function to load data asynchronously and how to handle different states of async operations.
In this activity, you'll learn how to use the `resource()` function to load data asynchronously and how to handle different states of async operations by building a user profile loader that demonstrates the Resource API in action.
<hr />
Let's build a user profile loader that demonstrates the Resource API in action.
<docs-workflow>
<docs-step title="Import resource function and API">
@ -15,7 +13,7 @@ Add `resource` to your existing imports and import the fake API function.
```ts
// Add resource to existing imports
import {Component, signal, computed, resource} from '@angular/core';
import {Component, signal, computed, resource, ChangeDetectionStrategy} from '@angular/core';
// Import mock API function
import {loadUser} from './user-api';
```
@ -63,35 +61,41 @@ hasError = computed(() => this.userResource.status() === 'error');
Resources provide a `status()` signal that can be 'loading', 'success', or 'error', a `value()` signal for the loaded data, and a `hasValue()` method that safely checks if data is available.
</docs-step>
<docs-step title="Create the template to display resource states">
Add the template to show different resource states and control buttons.
<docs-step title="Wire up the buttons and display resource states">
The template structure is already provided. Now connect everything:
Part 1. **Add click handlers to the buttons:**
```html
<div>
<h2>User Profile Loader</h2>
<div>
<button (click)="loadUser(1)">Load User 1</button>
<button (click)="loadUser(2)">Load User 2</button>
<button (click)="loadUser(999)">Load Invalid User</button>
<button (click)="reloadUser()">Reload</button>
</div>
<div class="status">
@if (isLoading()) {
<p>Loading user...</p>
} @else if (hasError()) {
<p class="error">Error: {{ userResource.error()?.message }}</p>
} @else if (userResource.hasValue()) {
<div class="user-info">
<h3>{{ userResource.value().name }}</h3>
<p>{{ userResource.value().email }}</p>
</div>
}
</div>
</div>
<button (click)="loadUser(1)">Load User 1</button>
<button (click)="loadUser(2)">Load User 2</button>
<button (click)="loadUser(999)">Load Invalid User</button>
<button (click)="reloadUser()">Reload</button>
```
Part 2. **Replace the placeholder with resource state handling:**
```html
@if (isLoading()) {
<p>Loading user...</p>
} @else if (hasError()) {
<p class="error">Error: {{ userResource.error()?.message }}</p>
} @else if (userResource.hasValue()) {
<div class="user-info">
<h3>{{ userResource.value().name }}</h3>
<p>{{ userResource.value().email }}</p>
</div>
}
```
The resource provides different methods to check its state:
- `isLoading()` - true when fetching data
- `hasError()` - true when an error occurred
- `userResource.hasValue()` - true when data is available
- `userResource.value()` - access the loaded data
- `userResource.error()` - access error information
</docs-step>
</docs-workflow>
@ -103,4 +107,4 @@ Excellent! You've now learned how to use the Resource API with signals. Key conc
- **Automatic cleanup**: Resources handle request cancellation and cleanup automatically
- **Manual control**: You can manually reload or abort requests when needed
In the next lesson, you'll learn [how to use signals for communication between components](/tutorials/signals/5-component-communication-with-signals)!
In the next lesson, you'll learn [how to pass data to components with input signals](/tutorials/signals/5-component-communication-with-signals)!

View file

@ -8,16 +8,21 @@ import {loadUser} from './user-api';
<div>
<h2>User Profile Loader</h2>
<!-- TODO: Attach the appropriate methods for each button -->
<div>
<!-- TODO: Add (click) handlers to call loadUser() with appropriate IDs -->
<button>Load User 1</button>
<button>Load User 2</button>
<button>Load Invalid User</button>
<!-- TODO: Add (click) handler to call reloadUser() -->
<button>Reload</button>
</div>
<div class="status">
<!-- TODO: Add conditional rendering for different resource states -->
<!-- TODO: Replace with @if blocks for loading, error, and success states -->
<!-- Use isLoading(), hasError(), and userResource.hasValue() -->
<!-- For loading: show "Loading user..." -->
<!-- For error: show error message with userResource.error()?.message -->
<!-- For success: show user name and email from userResource.value() -->
<p>Click a button to load user data</p>
</div>
</div>

View file

@ -1,192 +1,105 @@
# Component communication with signals
# Passing data to components with input signals
Now that you've learned [managing async data with signals](/tutorials/signals/4-managing-async-data-with-signals), let's explore Angular's signal-based APIs for component inputs, outputs, and two-way binding, making component data flow more reactive and efficient.
Now that you've learned [managing async data with signals](/tutorials/signals/4-managing-async-data-with-signals), let's explore Angular's signal-based `input()` API for passing data from parent to child components, making component data flow more reactive and efficient. If you're familiar with component props from other frameworks, inputs are the same idea.
In this activity, you'll add signal communication to pre-built components to see the three main patterns in action.
In this activity, you'll add signal inputs to a product card component and see how parent data flows down reactively.
<hr />
<docs-workflow>
<docs-step title="Add signal inputs to ProductCard">
Add signal `input` and `output` to receive and send data in the `product-card` component.
Add signal `input()` functions to receive data in the `product-card` component.
```ts
// Add imports for signal based communication between components
import {Component, input, output} from '@angular/core';
// Add imports for signal inputs
import {Component, input} from '@angular/core';
// Add these signal inputs
name = input.required<string>();
price = input.required<number>();
available = input<boolean>(true);
// Add signal output
addProductToCart = output<string>();
```
Notice how `input.required()` creates an input that must be provided, while `input()` with a default value is optional.
</docs-step>
<docs-step title="Connect signals to the template">
Update the template in `product-card` to display the signal values and handle clicks.
<docs-step title="Connect inputs to the template">
Update the template in `product-card` to display the signal input values.
```html
<div class="product-card">
<h3>{{ name() }}</h3>
<p class="price">\${{ price() }}</p>
<p class="status">Status: {{ available() ? 'Available' : 'Out of Stock' }}</p>
<button
(click)="addToCart()"
[disabled]="!available()">
{{ available() ? 'Click Me!' : 'Unavailable' }}
</button>
<p class="status">Status:
@if (available()) {
Available
} @else {
Out of Stock
}
</p>
</div>
```
</docs-step>
<docs-step title="Add the event and display the data">
Implement the method that emits data from the child to the parent and updates the app to display the last added product.
```ts
// product-card.ts
addToCart() {
if (this.available()) {
this.addProductToCart.emit(this.name());
}
}
```
```html
<!-- app.ts -->
<p>Last product added to cart: {{ lastAdded() || 'None yet' }}</p>
```
Input signals work just like regular signals in templates - call them as functions to access their values.
</docs-step>
<docs-step title="Connect parent signals to child inputs">
Chase the static values on the `product-card` component in `app.ts` to use dynamic values from the parent's signals.
Update the `product-card` usage in `app.ts` to pass dynamic signal values instead of static ones.
```html
<!-- Change from static values: -->
<product-card
name="'Static Product'"
name="Static Product"
price="99"
available="true"
/>
<!-- To dynamic signals: -->
<product-card
[name]="productName()"
[price]="productPrice()"
[available]="productAvailable()"
(addProductToCart)="onProductClicked($event)"
/>
```
The square brackets `[]` create property bindings that pass the current signal values to the child.
</docs-step>
<docs-step title="Implement parent signal updates">
Add a method to handle the the `addProductToCart` output from `product-card` in `app.ts`.
<docs-step title="Test reactive updates">
Add methods in `app.ts` to update the parent signals and see how the child component reacts automatically.
```ts
onProductClicked(productName: string) {
this.lastAdded.set(`${productName}`);
updateProduct() {
this.productName.set('Updated Product');
this.productPrice.set(149);
}
toggleAvailability() {
this.productAvailable.set(!this.productAvailable());
}
```
</docs-step>
<docs-step title="Set up the QuantitySelector model input">
Next, we need to setup two-way binding with a signal model in `quantity-selector.ts` file to receive and update the parent's signal model.
```ts
// Add imports
import {Component, input, model} from '@angular/core';
// Signal model input which receives parent's model
quantity = model.required<number>();
// Signal inputs for constraints
min = input<number>(1);
max = input<number>(10);
```
This creates a model input that will receive the parent's signal model.
</docs-step>
<docs-step title="Connect the template and add methods">
Update the `quantity-selector.ts` component to use the model and add increment/decrement methods.
```html
<div class="quantity-selector">
<label>Quantity:</label>
<button (click)="decrement()" [disabled]="quantity() <= min()">-</button>
<span class="quantity">{{ quantity() }}</span>
<button (click)="increment()" [disabled]="quantity() >= max()">+</button>
</div>
```
```ts
// Methods that modify the parent's model
increment() {
if (this.quantity() < this.max()) {
this.quantity.set(this.quantity() + 1);
}
}
decrement() {
if (this.quantity() > this.min()) {
this.quantity.set(this.quantity() - 1);
}
}
```
Notice: When the child calls `this.quantity.set()`, it's actually modifying the parent's model!
</docs-step>
<docs-step title="Set up two-way binding with signal models">
Create a signal model and connect it to the `quantity-selector` component in `app.ts`.
```ts
// Add signal model
selectedQuantity = model(1);
// Add methods to test two-way binding
resetQuantity() {
this.selectedQuantity.set(1);
}
increaseQuantity() {
this.selectedQuantity.set(this.selectedQuantity() + 1);
}
```
Then update the template to show the two-way binding in action:
```html
// With the actual component and controls:
<quantity-selector
[(quantity)]="selectedQuantity"
[min]="1"
[max]="10">
</quantity-selector>
<!-- Add controls to test reactivity -->
<div class="controls">
<p>Selected quantity: {{ selectedQuantity() }}</p>
<button (click)="resetQuantity()">Reset to 1</button>
<button (click)="increaseQuantity()">Increase from Parent</button>
<button (click)="updateProduct()">Update Product Info</button>
<button (click)="toggleAvailability()">Toggle Availability</button>
</div>
```
</docs-step>
When parent signals change, the child component automatically receives and displays the new values!
</docs-step>
</docs-workflow>
Perfect! You've implemented the three core signal communication patterns:
Excellent! You've learned how signal inputs work:
- **Signal inputs** - Parent data flows down to child components reactively
- **Signal outputs** - Child events flow up to parent components with type safety
- **Signal models** - Two-way binding keeps parent and child synchronized automatically
- **Signal inputs** - Use `input()` and `input.required()` to receive data from parent components
- **Reactive updates** - Child components automatically update when parent signal values change
- **Type safety** - Signal inputs provide full TypeScript type checking
- **Default values** - Optional inputs can have default values while required inputs must be provided
In the next lesson, you'll learn about [using signals with services](/tutorials/signals/6-using-signals-with-services)!
Signal inputs make component communication more reactive and eliminate the need for `OnChanges` lifecycle hooks in many cases.
In the next lesson, you'll learn about [two-way binding with model signals](/tutorials/signals/6-two-way-binding-with-model-signals)!

View file

@ -27,9 +27,18 @@
color: white;
}
.product-card button:disabled {
background: #ccc;
cursor: not-allowed;
.status {
margin: 8px 0;
}
.available {
color: #4caf50;
font-weight: bold;
}
.unavailable {
color: #f44336;
font-weight: bold;
}
/* QuantitySelector Component Styles */

View file

@ -1,13 +1,12 @@
import {Component, signal, model, ChangeDetectionStrategy} from '@angular/core';
import {Component, signal, ChangeDetectionStrategy} from '@angular/core';
import {ProductCard} from './product-card';
import {QuantitySelector} from './quantity-selector';
@Component({
selector: 'app-root',
imports: [ProductCard, QuantitySelector],
imports: [ProductCard],
template: `
<div class="shopping-app">
<h1>Component Communication with Signals</h1>
<h1>Input Signals Demo</h1>
<div class="demo-section">
<h2>Signal Inputs (Parent Child)</h2>
@ -17,7 +16,6 @@ import {QuantitySelector} from './quantity-selector';
[name]="productName()"
[price]="productPrice()"
[available]="productAvailable()"
(addProductToCart)="onProductClicked($event)"
/>
<div class="controls">
@ -25,37 +23,6 @@ import {QuantitySelector} from './quantity-selector';
<button (click)="toggleAvailability()">Toggle Availability</button>
</div>
</div>
<div class="demo-section">
<h2>Signal Models (Parent Child)</h2>
<p>Two-way binding allows parent and child to share state:</p>
<quantity-selector
[(quantity)]="selectedQuantity"
[min]="1"
[max]="10"
/>
<div class="controls">
<p>Selected quantity: {{ selectedQuantity() }}</p>
<button (click)="resetQuantity()">Reset to 1</button>
<button (click)="increaseQuantity()">Increase from Parent</button>
</div>
<div class="explanation">
<p><strong>Try this:</strong></p>
<ul>
<li>Click +/- buttons above (child changes parent)</li>
<li>Click "Increase from Parent" (parent changes child)</li>
<li>Both automatically sync! That's the power of signal models.</li>
</ul>
</div>
</div>
<div class="demo-section">
<h2>Signal Outputs (Child Parent)</h2>
<p>Last product added to cart: {{ lastAdded() || 'None yet' }}</p>
</div>
</div>
`,
styleUrl: './app.css',
@ -67,12 +34,6 @@ export class App {
productPrice = signal(99);
productAvailable = signal(true);
// Signal model for two-way binding
selectedQuantity = model(1);
// Signal for tracking events
lastAdded = signal<string | null>(null);
updateProduct() {
this.productName.set(`Product ${Math.floor(Math.random() * 100)}`);
this.productPrice.set(Math.floor(Math.random() * 500) + 50);
@ -81,16 +42,4 @@ export class App {
toggleAvailability() {
this.productAvailable.set(!this.productAvailable());
}
resetQuantity() {
this.selectedQuantity.set(1);
}
increaseQuantity() {
this.selectedQuantity.set(this.selectedQuantity() + 1);
}
onProductClicked(productName: string) {
this.lastAdded.set(`${productName}`);
}
}

View file

@ -1,4 +1,4 @@
import {Component, input, output, ChangeDetectionStrategy} from '@angular/core';
import {Component, input, ChangeDetectionStrategy} from '@angular/core';
@Component({
selector: 'product-card',
@ -6,12 +6,14 @@ import {Component, input, output, ChangeDetectionStrategy} from '@angular/core';
<div class="product-card">
<h3>{{ name() }}</h3>
<p class="price">\${{ price() }}</p>
<p class="status">Status: {{ available() ? 'Available' : 'Out of Stock' }}</p>
<button
(click)="addToCart()"
[disabled]="!available()">
{{ available() ? 'Add to cart' : 'Unavailable' }}
</button>
<p class="status">
Status:
@if (available()) {
<span class="available">Available</span>
} @else {
<span class="unavailable">Out of Stock</span>
}
</p>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
@ -21,13 +23,4 @@ export class ProductCard {
name = input.required<string>();
price = input.required<number>();
available = input<boolean>(true);
// Signal output - send events to parent
addProductToCart = output<string>();
addToCart() {
if (this.available()) {
this.addProductToCart.emit(this.name());
}
}
}

View file

@ -1,34 +0,0 @@
import {Component, input, model, ChangeDetectionStrategy} from '@angular/core';
@Component({
selector: 'quantity-selector',
template: `
<div class="quantity-selector">
<label>Quantity:</label>
<button (click)="decrement()" [disabled]="quantity() <= min()">-</button>
<span class="quantity">{{ quantity() }}</span>
<button (click)="increment()" [disabled]="quantity() >= max()">+</button>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class QuantitySelector {
// Signal model INPUT - receives parent's model for two-way binding
quantity = model.required<number>();
// Signal inputs for constraints
min = input<number>(1);
max = input<number>(10);
increment() {
if (this.quantity() < this.max()) {
this.quantity.set(this.quantity() + 1);
}
}
decrement() {
if (this.quantity() > this.min()) {
this.quantity.set(this.quantity() - 1);
}
}
}

View file

@ -6,5 +6,5 @@
"src/app/app.css"
],
"type": "editor",
"title": "Component communication with signals"
"title": "Passing data to components with input signals"
}

View file

@ -27,9 +27,18 @@
color: white;
}
.product-card button:disabled {
background: #ccc;
cursor: not-allowed;
.status {
margin: 8px 0;
}
.available {
color: #4caf50;
font-weight: bold;
}
.unavailable {
color: #f44336;
font-weight: bold;
}
/* QuantitySelector Component Styles */

View file

@ -1,90 +1,46 @@
// TODO: Import signal and model from @angular/core
import {Component, signal, ChangeDetectionStrategy} from '@angular/core';
// TODO: Import signal from @angular/core
import {Component, ChangeDetectionStrategy} from '@angular/core';
import {ProductCard} from './product-card';
import {QuantitySelector} from './quantity-selector';
@Component({
selector: 'app-root',
imports: [ProductCard, QuantitySelector],
imports: [ProductCard],
template: `
<div class="shopping-app">
<h1>Component Communication with Signals</h1>
<h1>Input Signals Demo</h1>
<div class="demo-section">
<h2>Signal Inputs (Parent Child)</h2>
<p>Data flows down from parent to child via signal inputs:</p>
<!-- TODO: Change from static values to dynamic signal values -->
<product-card
name="'Static Product'"
name="Static Product"
price="99"
available="true"
/>
<!-- TODO: Add controls to test reactive updates -->
<div class="controls">
<button (click)="updateProduct()">Update Product Data</button>
<button (click)="toggleAvailability()">Toggle Availability</button>
<!-- Add buttons to update product data and toggle availability -->
</div>
</div>
<div class="demo-section">
<h2>Signal Models (Parent Child)</h2>
<p>Two-way binding allows parent and child to share state:</p>
<!-- TODO: Update quantity-selector with two-way binding -->
<quantity-selector></quantity-selector>
<div class="controls">
<p>Selected quantity: 1</p>
<button (click)="resetQuantity()">Reset to 1</button>
<button (click)="increaseQuantity()">Increase from Parent</button>
</div>
<div class="explanation">
<p><strong>After implementing signal models:</strong></p>
<ul>
<li>Click +/- buttons (child changes parent)</li>
<li>Click "Increase from Parent" (parent changes child)</li>
<li>Watch both sync automatically!</li>
</ul>
</div>
</div>
<div class="demo-section">
<h2>Signal Outputs (Child Parent)</h2>
<p>Last product clicked on: None yet</p>
</div>
</div>
`,
styleUrl: './app.css',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
productName = signal('Demo Product');
productPrice = signal(99);
productAvailable = signal(true);
lastAdded = signal<string | null>(null);
// TODO: Create signal model for selectedQuantity
// TODO: Create signal for lastAdded tracking
updateProduct() {
this.productName.set(`Product ${Math.floor(Math.random() * 100)}`);
this.productPrice.set(Math.floor(Math.random() * 500) + 50);
}
toggleAvailability() {
this.productAvailable.set(!this.productAvailable());
}
resetQuantity() {
// TODO: Reset selectedQuantity to 1
console.log('TODO: Reset quantity');
}
increaseQuantity() {
// TODO: Increase selectedQuantity by 1
console.log('TODO: Increase quantity');
}
// TODO: Add onProductClicked method to handle child events
// TODO: Create parent signals for product data
// productName = signal('Demo Product');
// productPrice = signal(99);
// productAvailable = signal(true);
// TODO: Add methods to update parent signals
// updateProduct() {
// this.productName.set('Updated Product');
// this.productPrice.set(149);
// }
// toggleAvailability() {
// this.productAvailable.set(!this.productAvailable());
// }
}

View file

@ -1,21 +1,22 @@
import {Component, ChangeDetectionStrategy} from '@angular/core';
// TODO: Import input and output from @angular/core
// TODO: Import input from @angular/core
@Component({
selector: 'product-card',
template: `
<div class="product-card">
<!-- TODO: Display signal input values -->
<h3>Product Name</h3>
<p class="price">$0</p>
<p class="status">Status: Available</p>
<button>Add to cart</button>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ProductCard {
// TODO: Create signal inputs for name, price, and available
// TODO: Create signal output for clicked events
// TODO: Implement addToCart method
// name = input.required<string>();
// price = input.required<number>();
// available = input<boolean>(true);
}

View file

@ -1,23 +0,0 @@
import {Component, ChangeDetectionStrategy} from '@angular/core';
// TODO: Import input and model from @angular/core
@Component({
selector: 'quantity-selector',
template: `
<div class="quantity-selector">
<label>Quantity:</label>
<button [disabled]="true">-</button>
<span class="quantity">1</span>
<button [disabled]="true">+</button>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class QuantitySelector {
// TODO: Create signal model INPUT for quantity (not creating a new model!)
// Use model.required<number>() to receive parent's model
// TODO: Create signal inputs for min and max constraints
// TODO: Add increment method that updates the model
// TODO: Add decrement method that updates the model
}

View file

@ -0,0 +1,128 @@
# Two-way binding with model signals
Now that you've learned [passing data to components with input signals](/tutorials/signals/5-component-communication-with-signals), let's explore Angular's `model()` API for two-way binding. Model signals are perfect for UI components like checkboxes, sliders, or custom form controls where the component needs to both receive a value AND update it.
In this activity, you'll create a custom checkbox component that manages its own state while keeping the parent synchronized.
<hr />
<docs-workflow>
<docs-step title="Set up the custom checkbox with model signal">
Create a model signal in the `custom-checkbox` component that can both receive and update the parent's value.
```ts
// Add imports for model signals
import {Component, model, input} from '@angular/core';
// Model signal for two-way binding
checked = model.required<boolean>();
// Optional input for label
label = input<string>('');
```
Unlike `input()` signals which are read-only, `model()` signals can be both read and written to.
</docs-step>
<docs-step title="Create the checkbox template">
Build the checkbox template that responds to clicks and updates its own model.
```html
<label class="custom-checkbox">
<input
type="checkbox"
[checked]="checked()"
(change)="toggle()">
<span class="checkmark"></span>
{{ label() }}
</label>
```
The component reads from its model signal and has a method to update it.
</docs-step>
<docs-step title="Add the toggle method">
Implement the toggle method that updates the model signal when the checkbox is clicked.
```ts
toggle() {
// This updates BOTH the component's state AND the parent's model!
this.checked.set(!this.checked());
}
```
When the child component calls `this.checked.set()`, it automatically propagates the change back to the parent. This is the key difference from `input()` signals.
</docs-step>
<docs-step title="Set up two-way binding in the parent">
First, uncomment the model signal properties and methods in `app.ts`:
```ts
// Parent signal models
agreedToTerms = model(false);
enableNotifications = model(true);
// Methods to test two-way binding
toggleTermsFromParent() {
this.agreedToTerms.set(!this.agreedToTerms());
}
resetAll() {
this.agreedToTerms.set(false);
this.enableNotifications.set(false);
}
```
Then update the template:
Part 1. **Uncomment the checkboxes and add two-way binding:**
- Replace `___ADD_TWO_WAY_BINDING___` with `[(checked)]="agreedToTerms"` for the first checkbox
- Replace `___ADD_TWO_WAY_BINDING___` with `[(checked)]="enableNotifications"` for the second
Part 2. **Replace the `???` placeholders with @if blocks:**
```html
@if (agreedToTerms()) {
Yes
} @else {
No
}
```
Part 3. **Add click handlers to the buttons:**
```html
<button (click)="toggleTermsFromParent()">Toggle Terms from Parent</button>
<button (click)="resetAll()">Reset All</button>
```
The `[(checked)]` syntax creates two-way binding - data flows down to the component AND changes flow back up to the parent by emitting an event that references the signal itself and does _not_ call the signal getter directly.
</docs-step>
<docs-step title="Test the two-way binding">
Interact with your app to see two-way binding in action:
1. **Click checkboxes** - Component updates its own state and notifies parent
2. **Click "Toggle Terms from Parent"** - Parent updates propagate down to component
3. **Click "Reset All"** - Parent resets both models and components update automatically
Both the parent and child can update the shared state, and both stay in sync automatically!
</docs-step>
</docs-workflow>
Perfect! You've learned how model signals enable two-way binding:
- **Model signals** - Use `model()` and `model.required()` for values that can be both read and written
- **Two-way binding** - Use `[(property)]` syntax to bind parent signals to child models
- **Perfect for UI components** - Checkboxes, form controls, and widgets that need to manage their own state
- **Automatic synchronization** - Parent and child stay in sync without manual event handling
**When to use `model()` vs `input()`:**
- Use `input()` for data that only flows down (display data, configuration)
- Use `model()` for UI components that need to update their own value (form controls, toggles)
In the next lesson, you'll learn about [using signals with services](/tutorials/signals/7-using-signals-with-services)!

View file

@ -0,0 +1,81 @@
/* CustomCheckbox Component Styles */
.custom-checkbox {
display: flex;
align-items: center;
gap: 8px;
margin: 12px 0;
cursor: pointer;
user-select: none;
}
.custom-checkbox input[type='checkbox'] {
margin: 0;
}
.checkmark {
width: 18px;
height: 18px;
border: 2px solid #ddd;
border-radius: 3px;
display: inline-block;
position: relative;
}
.custom-checkbox input[type='checkbox']:checked + .checkmark {
background-color: #4caf50;
border-color: #4caf50;
}
.custom-checkbox input[type='checkbox']:checked + .checkmark::after {
content: '✓';
position: absolute;
color: white;
font-size: 14px;
top: -2px;
left: 2px;
}
/* App Component Styles */
.shopping-app {
max-width: 600px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}
.demo-section {
margin: 20px 0;
padding: 20px;
border: 1px solid #e0e0e0;
border-radius: 8px;
background: #fafafa;
}
.controls {
margin: 20px 0;
padding: 16px;
background: #f0f8ff;
border-radius: 8px;
border-left: 4px solid #007bff;
}
.controls p {
margin: 8px 0;
font-weight: bold;
}
button {
margin-right: 8px;
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
background: #1976d2;
color: white;
font-weight: bold;
transition: background-color 0.2s;
}
button:hover {
background: #1565c0;
}

View file

@ -0,0 +1,63 @@
import {Component, model, ChangeDetectionStrategy} from '@angular/core';
import {CustomCheckbox} from './custom-checkbox';
@Component({
selector: 'app-root',
imports: [CustomCheckbox],
template: `
<div class="shopping-app">
<h1>Custom Checkbox Example</h1>
<div class="demo-section">
<!-- Two-way binding with custom components -->
<custom-checkbox
[(checked)]="agreedToTerms"
label="I agree to the terms"
/>
<custom-checkbox
[(checked)]="enableNotifications"
label="Enable notifications"
/>
<!-- Controls to test two-way binding -->
<div class="controls">
<p>Terms agreed:
@if (agreedToTerms()) {
Yes
} @else {
No
}
</p>
<p>Notifications:
@if (enableNotifications()) {
Enabled
} @else {
Disabled
}
</p>
<button (click)="toggleTermsFromParent()">Toggle Terms from Parent</button>
<button (click)="resetAll()">Reset All</button>
</div>
</div>
</div>
`,
styleUrl: './app.css',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
// Parent signal models
agreedToTerms = model(false);
enableNotifications = model(true);
// Methods to test two-way binding
toggleTermsFromParent() {
this.agreedToTerms.set(!this.agreedToTerms());
}
resetAll() {
this.agreedToTerms.set(false);
this.enableNotifications.set(false);
}
}

View file

@ -0,0 +1,28 @@
import {Component, model, input, ChangeDetectionStrategy} from '@angular/core';
@Component({
selector: 'custom-checkbox',
template: `
<label class="custom-checkbox">
<input
type="checkbox"
[checked]="checked()"
(change)="toggle()">
<span class="checkmark"></span>
{{ label() }}
</label>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomCheckbox {
// Model signal for two-way binding
checked = model.required<boolean>();
// Optional input for label
label = input<string>('');
toggle() {
// This updates BOTH the component's state AND the parent's model!
this.checked.set(!this.checked());
}
}

View file

@ -0,0 +1,5 @@
{
"openFiles": ["src/app/app.ts", "src/app/custom-checkbox.ts", "src/app/app.css"],
"type": "editor",
"title": "Two-way binding with model signals"
}

View file

@ -0,0 +1,81 @@
/* CustomCheckbox Component Styles */
.custom-checkbox {
display: flex;
align-items: center;
gap: 8px;
margin: 12px 0;
cursor: pointer;
user-select: none;
}
.custom-checkbox input[type='checkbox'] {
margin: 0;
}
.checkmark {
width: 18px;
height: 18px;
border: 2px solid #ddd;
border-radius: 3px;
display: inline-block;
position: relative;
}
.custom-checkbox input[type='checkbox']:checked + .checkmark {
background-color: #4caf50;
border-color: #4caf50;
}
.custom-checkbox input[type='checkbox']:checked + .checkmark::after {
content: '✓';
position: absolute;
color: white;
font-size: 14px;
top: -2px;
left: 2px;
}
/* App Component Styles */
.shopping-app {
max-width: 600px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}
.demo-section {
margin: 20px 0;
padding: 20px;
border: 1px solid #e0e0e0;
border-radius: 8px;
background: #fafafa;
}
.controls {
margin: 20px 0;
padding: 16px;
background: #f0f8ff;
border-radius: 8px;
border-left: 4px solid #007bff;
}
.controls p {
margin: 8px 0;
font-weight: bold;
}
button {
margin-right: 8px;
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
background: #1976d2;
color: white;
font-weight: bold;
transition: background-color 0.2s;
}
button:hover {
background: #1565c0;
}

View file

@ -0,0 +1,57 @@
// TODO: Import model from @angular/core
import {Component, ChangeDetectionStrategy} from '@angular/core';
import {CustomCheckbox} from './custom-checkbox';
@Component({
selector: 'app-root',
imports: [CustomCheckbox],
template: `
<div class="shopping-app">
<h1>Custom Checkbox Example</h1>
<div class="demo-section">
<!-- TODO: Uncomment and add [(checked)] two-way binding -->
<!--
<custom-checkbox
___ADD_TWO_WAY_BINDING___
label="I agree to the terms"
/>
<custom-checkbox
___ADD_TWO_WAY_BINDING___
label="Enable notifications"
/>
-->
<div class="controls">
<p>Terms agreed:
<!-- TODO: Replace with @if block using agreedToTerms() -->
???
</p>
<p>Notifications:
<!-- TODO: Replace with @if block using enableNotifications() -->
???
</p>
<!-- TODO: Add (click) handlers -->
<button>Toggle Terms from Parent</button>
<button>Reset All</button>
</div>
</div>
</div>
`,
styleUrl: './app.css',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
// TODO: Add parent signal models
// agreedToTerms = model(false);
// enableNotifications = model(true);
// TODO: Add methods to test two-way binding
// toggleTermsFromParent() {
// this.agreedToTerms.set(!this.agreedToTerms());
// }
// resetAll() {
// this.agreedToTerms.set(false);
// this.enableNotifications.set(false);
// }
}

View file

@ -0,0 +1,25 @@
import {Component, ChangeDetectionStrategy} from '@angular/core';
// TODO: Import model and input from @angular/core
@Component({
selector: 'custom-checkbox',
template: `
<label class="custom-checkbox">
<!-- TODO: Add checkbox input with [checked] binding and (change) event -->
<span class="checkmark"></span>
<!-- TODO: Display label -->
</label>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomCheckbox {
// TODO: Add model signal for two-way binding
// checked = model.required<boolean>();
// TODO: Add optional input for label
// label = input<string>('');
// TODO: Add toggle method
// toggle() {
// this.checked.set(!this.checked());
// }
}

View file

@ -1,173 +0,0 @@
# Using signals with services
Now that you've learned [component communication with signals](/tutorials/signals/5-component-communication-with-signals), let's explore how to use signals with Angular services. Services are perfect for sharing reactive state across multiple components, and signals make this even more powerful by providing automatic change detection and clean reactive patterns.
In this activity, you'll learn how to create a cart store that uses signals to manage shopping cart state and share it across components.
<hr />
Let's build a simple shopping cart store that manages cart state with signals, allowing the cart display component to react to cart changes automatically.
<docs-workflow>
<docs-step title="Add cart store signals">
Add readonly and computed signals to make the cart state reactive in `cart-store.ts`.
```ts
// Readonly signals
readonly cartItems = this.items.asReadonly();
// Computed signals
readonly totalQuantity = computed(() => {
return this.items().reduce((sum, item) => sum + item.quantity, 0);
});
readonly totalPrice = computed(() => {
return this.items().reduce((sum, item) => sum + item.price * item.quantity, 0);
});
```
These signals allow components to reactively access cart data and computed totals.
</docs-step>
<docs-step title="Update the cart display component">
Update the cart display component in `cart-display.ts` to use the cart store signals.
```ts
import {Component, inject} from '@angular/core';
import {CartStore} from './cart-store';
@Component({
selector: 'cart-display',
template: `
<div class="cart-display">
<h2>Shopping Cart Demo</h2>
<div class="add-products">
<h3>Add Products</h3>
<button (click)="addLaptop()">Add Laptop ($999)</button>
<button (click)="addMouse()">Add Mouse ($25)</button>
<button (click)="addKeyboard()">Add Keyboard ($79)</button>
</div>
<h3>Cart Contents</h3>
@if (cartStore.cartItems().length === 0) {
<p class="empty-message">Your cart is empty</p>
} @else {
<div class="cart-items">
@for (item of cartStore.cartItems(); track item.id) {
<div class="cart-item">
<div class="item-info">
<h4>{{ item.name }}</h4>
<p class="price">\${{ item.price }} each</p>
</div>
<div class="quantity-controls">
<button (click)="decreaseQuantity(item.id)">-</button>
<span class="quantity">{{ item.quantity }}</span>
<button (click)="increaseQuantity(item.id)">+</button>
<button (click)="removeItem(item.id)" class="remove">×</button>
</div>
</div>
}
</div>
<div class="cart-summary">
<p>Total Items: {{ cartStore.totalQuantity() }}</p>
<p class="total-price">Total: \${{ cartStore.totalPrice() }}</p>
<button (click)="clearCart()" class="clear-btn">Clear Cart</button>
</div>
}
</div>
`,
})
export class CartDisplay {
cartStore = inject(CartStore);
addLaptop() {
this.cartStore.addItem('1', 'Laptop', 999);
}
addMouse() {
this.cartStore.addItem('2', 'Mouse', 25);
}
addKeyboard() {
this.cartStore.addItem('3', 'Keyboard', 79);
}
increaseQuantity(id: string) {
const items = this.cartStore.cartItems();
const currentItem = items.find((item) => item.id === id);
if (currentItem) {
this.cartStore.updateQuantity(id, currentItem.quantity + 1);
}
}
decreaseQuantity(id: string) {
const items = this.cartStore.cartItems();
const currentItem = items.find((item) => item.id === id);
if (currentItem && currentItem.quantity > 1) {
this.cartStore.updateQuantity(id, currentItem.quantity - 1);
}
}
removeItem(id: string) {
this.cartStore.removeItem(id);
}
clearCart() {
this.cartStore.clearCart();
}
}
```
This component includes buttons to add products and displays cart contents using service signals.
</docs-step>
<docs-step title="Update the main app component">
Update the main app component in `app.ts` to use the cart service and display the cart component.
```ts
import {Component, inject} from '@angular/core';
import {CartStore} from './cart-store';
import {CartDisplay} from './cart-display';
@Component({
selector: 'app-root',
imports: [CartDisplay],
template: `
<div class="shopping-app">
<header>
<h1>Signals with Services Demo</h1>
<div class="cart-badge">
Cart: {{ cartStore.totalQuantity() }} items (\${{ cartStore.totalPrice() }})
</div>
</header>
<main>
<cart-display></cart-display>
</main>
</div>
`,
styleUrls: ['./app.css'],
})
export class App {
cartStore = inject(CartStore);
}
```
This component demonstrates how services with signals provide automatic reactivity in the header cart badge.
</docs-step>
</docs-workflow>
Excellent! You've now learned how to use signals with services. Key concepts to remember:
- **Service-level signals**: Services can use signals to manage reactive state
- **Dependency injection**: Use `inject()` to access services with signals in components
- **Computed signals in services**: Create derived state that updates automatically
- **Readonly signals**: Expose read-only versions of signals to prevent external mutations
In the next lesson, you'll learn about [how to use signals with directives](/tutorials/signals/7-using-signals-with-directives)!

View file

@ -1,138 +0,0 @@
# Using signals with directives
Now that you've learned [using signals with services](/tutorials/signals/6-using-signals-with-services), let's explore how directives can use signals to create reactive behavior that automatically responds to changes. This makes directives more powerful and easier to manage.
In this activity, you'll learn how to use signals in directives for reactive styling and user interactions.
<hr />
Let's build a simple highlight directive that demonstrates the core signals concepts in directives.
<docs-workflow>
<docs-step title="Import signal functions">
First, add the signal imports to the directive.
```ts
import {Directive, input, signal, computed} from '@angular/core';
```
Directives can use all the same signal APIs as components.
</docs-step>
<docs-step title="Create signal inputs">
Add signal inputs to configure the directive behavior.
```ts
// Signal inputs for configuration
color = input<string>('yellow');
intensity = input<number>(0.3);
```
Signal inputs allow parent elements to pass reactive data to the directive.
</docs-step>
<docs-step title="Add internal signal state">
Create an internal signal to track the hover state.
```ts
// Internal signal for hover state
private isHovered = signal(false);
```
This signal will track whether the user is hovering over the element.
</docs-step>
<docs-step title="Create computed signal for styling">
Add a computed signal that calculates the background style.
```ts
// Computed signal for background style
backgroundStyle = computed(() => {
const baseColor = this.color();
const alpha = this.isHovered() ? this.intensity() : this.intensity() * 0.5;
const colorMap: Record<string, string> = {
'yellow': `rgba(255, 255, 0, ${alpha})`,
'blue': `rgba(0, 100, 255, ${alpha})`,
'green': `rgba(0, 200, 0, ${alpha})`,
'red': `rgba(255, 0, 0, ${alpha})`,
};
return colorMap[baseColor] || colorMap['yellow'];
});
```
This computed signal reactively calculates the background color based on inputs and hover state.
</docs-step>
<docs-step title="Configure the directive with host bindings">
Use the `host` object to bind the computed signal and handle events.
```ts
@Directive({
selector: '[highlight]',
host: {
'[style.backgroundColor]': 'backgroundStyle()',
'(mouseenter)': 'onMouseEnter()',
'(mouseleave)': 'onMouseLeave()',
},
})
```
This approach is more declarative and follows Angular v20+ best practices.
</docs-step>
<docs-step title="Add event handler methods">
Add the methods that update the hover signal.
```ts
onMouseEnter() {
this.isHovered.set(true);
}
onMouseLeave() {
this.isHovered.set(false);
}
```
These methods update the signal, which automatically triggers the computed signal to recalculate.
</docs-step>
<docs-step title="Use the directive in your template">
Update the app template to demonstrate the directive.
```ts
template: `
<div>
<h1>Directive with Signals</h1>
<div highlight color="yellow" [intensity]="0.2">
Hover me - Yellow highlight
</div>
<div highlight color="blue" [intensity]="0.4">
Hover me - Blue highlight
</div>
<div highlight color="green" [intensity]="0.6">
Hover me - Green highlight
</div>
</div>
`,
```
The directive automatically applies reactive highlighting based on the signal inputs.
</docs-step>
</docs-workflow>
Excellent! You've learned how to use signals in directives. Key concepts to remember:
- **Signal inputs** - Reactive configuration from parent elements
- **Internal signals** - Managing directive state
- **Computed signals** - Reactive calculations based on multiple signals
- **Host object bindings** - A declarative approach to connect signals to DOM properties and events
- **Reactive event handling** - Updating signals in response to user interactions
Signals make directives more predictable and easier to debug by providing clear reactive data flow.

View file

@ -0,0 +1,103 @@
# Using signals with services
Now that you've learned [two-way binding with model signals](/tutorials/signals/6-two-way-binding-with-model-signals), let's explore how to use signals with Angular services. Services are perfect for sharing reactive state across multiple components, and signals make this even more powerful by providing automatic change detection and clean reactive patterns.
In this activity, you'll learn how to create a cart store with signals that allow the cart display component to react to state changes automatically.
<hr />
<docs-workflow>
<docs-step title="Add cart store signals">
Add readonly and computed signals to make the cart state reactive in `cart-store.ts`.
```ts
// Add the computed import
import {Injectable, signal, computed} from '@angular/core';
// Then add these signals to the class:
// Readonly signals
readonly cartItems = this.items.asReadonly();
// Computed signals
readonly totalQuantity = computed(() => {
return this.items().reduce((sum, item) => sum + item.quantity, 0);
});
readonly totalPrice = computed(() => {
return this.items().reduce((sum, item) => sum + item.price * item.quantity, 0);
});
```
These signals allow components to reactively access cart data and computed totals. The `asReadonly()` method prevents external code from modifying the cart items directly, while `computed()` creates derived state that automatically updates when the source signal changes.
</docs-step>
<docs-step title="Complete the quantity update methods">
The cart display component in `cart-display.ts` already uses the cart store signals in its template. Complete the quantity update methods to modify cart items:
```ts
increaseQuantity(id: string) {
const items = this.cartStore.cartItems();
const currentItem = items.find((item) => item.id === id);
if (currentItem) {
this.cartStore.updateQuantity(id, currentItem.quantity + 1);
}
}
decreaseQuantity(id: string) {
const items = this.cartStore.cartItems();
const currentItem = items.find((item) => item.id === id);
if (currentItem && currentItem.quantity > 1) {
this.cartStore.updateQuantity(id, currentItem.quantity - 1);
}
}
```
These methods read the current cart state using `cartItems()` and update quantities through the store's methods. The UI automatically updates when the signals change!
</docs-step>
<docs-step title="Update the main app component">
Update the main app component in `app.ts` to use the cart service and display the cart component.
```ts
import {Component, inject} from '@angular/core';
import {CartStore} from './cart-store';
import {CartDisplay} from './cart-display';
@Component({
selector: 'app-root',
imports: [CartDisplay],
template: `
<div class="shopping-app">
<header>
<h1>Signals with Services Demo</h1>
<div class="cart-badge">
Cart: {{ cartStore.totalQuantity() }} items (\${{ cartStore.totalPrice() }})
</div>
</header>
<main>
<cart-display></cart-display>
</main>
</div>
`,
styleUrl: './app.css',
})
export class App {
cartStore = inject(CartStore);
}
```
</docs-step>
</docs-workflow>
Excellent! You've now learned how to use signals with services. Key concepts to remember:
- **Service-level signals**: Services can use signals to manage reactive state
- **Dependency injection**: Use `inject()` to access services with signals in components
- **Computed signals in services**: Create derived state that updates automatically
- **Readonly signals**: Expose read-only versions of signals to prevent external mutations
In the next lesson, you'll learn about [how to use signals with directives](/tutorials/signals/8-using-signals-with-directives)!

View file

@ -1,7 +1,7 @@
{
"openFiles": [
"src/app/app.ts",
"src/app/cart-service.ts",
"src/app/cart-store.ts",
"src/app/cart-display.ts",
"src/app/cart-types.ts",
"src/app/app.css"

View file

@ -16,12 +16,11 @@ import {CartStore} from './cart-store';
</div>
<h3>Cart Contents</h3>
<!-- TODO: Use cart signals to display cart items -->
@if (false) {
@if (cartStore.cartItems().length === 0) {
<p class="empty-message">Your cart is empty</p>
} @else {
<div class="cart-items">
@for (item of []; track item.id) {
@for (item of cartStore.cartItems(); track item.id) {
<div class="cart-item">
<div class="item-info">
<h4>{{ item.name }}</h4>
@ -39,8 +38,8 @@ import {CartStore} from './cart-store';
</div>
<div class="cart-summary">
<!-- TODO: Display cart summary using computed signals -->
<p class="total-price">Total: $0</p>
<p>Total Items: {{ cartStore.totalQuantity() }}</p>
<p class="total-price">Total: \${{ cartStore.totalPrice() }}</p>
<button (click)="clearCart()" class="clear-btn">Clear Cart</button>
</div>
}
@ -64,11 +63,13 @@ export class CartDisplay {
}
increaseQuantity(id: string) {
// TODO: Get current item and update quantity
// TODO: Get current item from cartStore.cartItems()
// and call cartStore.updateQuantity() with quantity + 1
}
decreaseQuantity(id: string) {
// TODO: Get current item and update quantity
// TODO: Get current item from cartStore.cartItems()
// and call cartStore.updateQuantity() with quantity - 1 (if > 1)
}
removeItem(id: string) {

View file

@ -7,11 +7,14 @@ import {CartItem} from './cart-types';
export class CartStore {
private items = signal<CartItem[]>([]);
// TODO: Create readonly signal for items using asReadonly()
// TODO: Create readonly signal for cartItems using this.items.asReadonly()
// readonly cartItems = ???
// TODO: Create computed signal for total quantity
// TODO: Create computed signal for totalQuantity
// readonly totalQuantity = computed(() => ???)
// TODO: Create computed signal for total price
// TODO: Create computed signal for totalPrice
// readonly totalPrice = computed(() => ???)
addItem(id: string, name: string, price: number) {
this.items.update((currentItems) => {

View file

@ -0,0 +1,110 @@
# Using signals with directives
Now that you've learned [using signals with services](/tutorials/signals/7-using-signals-with-services), let's explore how directives use signals. **The great news: signals work exactly the same in directives as they do in components!** The main difference is that since directives don't have templates, you'll primarily use signals in host bindings to reactively update the host element.
In this activity, you'll build a highlight directive that demonstrates how signals create reactive behavior in directives.
<hr />
<docs-workflow>
<docs-step title="Set up signals just like in a component">
Import the signal functions and create your reactive state. This works exactly the same as in components:
```ts
import {Directive, input, signal, computed} from '@angular/core';
@Directive({
selector: '[highlight]',
})
export class HighlightDirective {
// Signal inputs - same as components!
color = input<string>('yellow');
intensity = input<number>(0.3);
// Internal state - same as components!
private isHovered = signal(false);
// Computed signals - same as components!
backgroundStyle = computed(() => {
const baseColor = this.color();
const alpha = this.isHovered() ? this.intensity() : this.intensity() * 0.5;
const colorMap: Record<string, string> = {
'yellow': `rgba(255, 255, 0, ${alpha})`,
'blue': `rgba(0, 100, 255, ${alpha})`,
'green': `rgba(0, 200, 0, ${alpha})`,
'red': `rgba(255, 0, 0, ${alpha})`,
};
return colorMap[baseColor] || colorMap['yellow'];
});
}
```
Notice how this is identical to component patterns - the only difference is we're in a `@Directive` instead of `@Component`.
</docs-step>
<docs-step title="Use signals in host bindings">
Since directives don't have templates, you'll use signals in **host bindings** to reactively update the host element. Add the `host` configuration and event handlers:
```ts
@Directive({
selector: '[highlight]',
host: {
'[style.backgroundColor]': 'backgroundStyle()',
'(mouseenter)': 'onMouseEnter()',
'(mouseleave)': 'onMouseLeave()',
},
})
export class HighlightDirective {
// ... signals from previous step ...
onMouseEnter() {
this.isHovered.set(true);
}
onMouseLeave() {
this.isHovered.set(false);
}
}
```
The host bindings automatically re-evaluate when the signals change - just like template bindings in components! When `isHovered` changes, the `backgroundStyle` computed signal recalculates, and the host binding updates the element's style.
</docs-step>
<docs-step title="Use the directive in your template">
Update the app template to demonstrate the reactive directive:
```ts
template: `
<div>
<h1>Directive with Signals</h1>
<div highlight color="yellow" [intensity]="0.2">
Hover me - Yellow highlight
</div>
<div highlight color="blue" [intensity]="0.4">
Hover me - Blue highlight
</div>
<div highlight color="green" [intensity]="0.6">
Hover me - Green highlight
</div>
</div>
`,
```
The directive automatically applies reactive highlighting based on the signal inputs!
</docs-step>
</docs-workflow>
Perfect! You've now seen how signals work with directives. Some key takeaways from this lesson are:
- **Signals are universal** - All signal APIs (`input()`, `signal()`, `computed()`, `effect()`) work the same in both directives and components
- **Host bindings are the primary use case** - Since directives don't have templates, you use signals in host bindings to reactively modify the host element
- **Same reactive patterns** - Signal updates trigger automatic re-evaluation of computed signals and host bindings, just like in component templates
In the next lesson, you'll [learn how to query child elements with signal queries](/tutorials/signals/9-query-child-elements-with-signal-queries)!

View file

@ -1,6 +1,6 @@
# Query child elements with signal queries
Now that you've learned [how to use signals with directives](/tutorials/signals/7-using-signals-with-directives), let's explore signal-based query APIs. These provide a reactive way to access and interact with child components. Unlike traditional ViewChild, signal queries automatically update and provide type-safe access to child components.
Now that you've learned [how to use signals with directives](/tutorials/signals/8-using-signals-with-directives), let's explore signal-based query APIs. These provide a reactive way to access and interact with child components and directives. Both components and directives can perform queries while also being queried themselves. Unlike the traditional ViewChild, signal queries automatically update and provide type-safe access to child components and directives.
In this activity, you'll add viewChild queries to interact with child components programmatically.
@ -12,7 +12,7 @@ In this activity, you'll add viewChild queries to interact with child components
First, add the `viewChild` import to access child components in `app.ts`.
```ts
import {Component, signal, computed, viewChild} from '@angular/core';
import {Component, signal, computed, viewChild, ChangeDetectionStrategy} from '@angular/core';
```
</docs-step>
@ -63,4 +63,4 @@ Click the buttons to see how viewChild queries enable parent components to contr
Perfect! You've learned how to use signal-based query APIs for child component interaction:
In the next lesson, you'll learn about [how to react to signal changes with effect](/tutorials/signals/9-reacting-to-signal-changes-with-effect)!
In the next lesson, you'll learn about [how to react to signal changes with effect](/tutorials/signals/10-reacting-to-signal-changes-with-effect)!

View file

@ -9,7 +9,11 @@ import {Component, input, signal, ChangeDetectionStrategy} from '@angular/core';
<p class="description">{{ description() }}</p>
<div class="actions">
<button (click)="toggleDetails()">
{{ showDetails() ? 'Hide' : 'Show' }} Details
@if (showDetails()) {
Hide
} @else {
Show
} Details
</button>
</div>
@if (showDetails()) {
@ -24,12 +28,12 @@ import {Component, input, signal, ChangeDetectionStrategy} from '@angular/core';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ProductCard {
name = input.required<string>();
name = input.required();
price = input.required<number>();
description = input<string>('');
available = input<boolean>(true);
productId = input<string>('');
category = input<string>('');
description = input('');
available = input(true);
productId = input('');
category = input('');
showDetails = signal(false);

View file

@ -1,4 +1,12 @@
import {Component, input, signal, OnDestroy, ChangeDetectionStrategy} from '@angular/core';
import {
Component,
DestroyRef,
inject,
input,
signal,
OnDestroy,
ChangeDetectionStrategy,
} from '@angular/core';
@Component({
selector: 'cart-summary',
@ -22,16 +30,12 @@ export class CartSummary implements OnDestroy {
isAnimating = signal(false);
private timeoutId?: ReturnType<typeof setTimeout>;
private destroyRef = inject(DestroyRef);
// Public method for parent interaction
initiateCheckout() {
this.isAnimating.set(true);
this.timeoutId = setTimeout(() => this.isAnimating.set(false), 2000);
}
ngOnDestroy() {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
const timeoutId = setTimeout(() => this.isAnimating.set(false), 2000);
this.destroyRef.onDestroy(() => clearTimeout(this.timeoutId));
}
}

View file

@ -9,7 +9,11 @@ import {Component, input, signal, ChangeDetectionStrategy} from '@angular/core';
<p class="description">{{ description() }}</p>
<div class="actions">
<button (click)="toggleDetails()">
{{ showDetails() ? 'Hide' : 'Show' }} Details
@if (showDetails()) {
Hide
} @else {
Show
} Details
</button>
</div>
@if (showDetails()) {
@ -24,12 +28,12 @@ import {Component, input, signal, ChangeDetectionStrategy} from '@angular/core';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ProductCard {
name = input.required<string>();
name = input.required();
price = input.required<number>();
description = input<string>('');
available = input<boolean>(true);
productId = input<string>('');
category = input<string>('');
description = input('');
available = input(true);
productId = input('');
category = input('');
showDetails = signal(false);