Demo application: Joe’s Robot Shop (e-commerce app for robot parts)
Table of Contents
- Module 1 — Control Flow in Angular Templates
- 1.1 Introduction and Application Overview
- 1.2 Conditional Rendering with @if
- 1.3 Expression Aliases with
as - 1.4 Conditional Rendering with @switch @case
- 1.5 Local Variables in Templates with @let
- 1.6 Template Reference Variables
- 1.7 Template Reference Variable Scope
- 1.8 @empty Fallback in @for Loops
- 1.9 Efficient List Rendering with @for and track
- 1.10 Contextual Variables in @for Loops
- Module 2 — Angular Pipes
- Module 3 — Content Projection and Inter-component Communication
- 3.1 Content Projection with ng-content
- 3.2 Input Properties — Passing Data to a Child Component
- 3.3 Input Property Types and Required Properties
- 3.4 Output Properties — Child to Parent Communication
- 3.5 Using Output Properties
- 3.6 Two-way Binding with Model Inputs
- 3.7 Implicit Change Events of Model Inputs
- 3.8 Transforming Input Property Values
- 3.9 Inter-component Communication with Services
- 3.10 Challenge: Fixing the Cart Bug
- Application Architecture
- Key Concepts Summary
Module 1 — Control Flow in Angular Templates
1.1 Introduction and Application Overview
This course covers advanced template syntax techniques and inter-component communication in Angular. The demo application is Joe’s Robot Shop, a robot parts e-commerce store featuring:
- A Catalog page displaying all available parts
- A Cart allowing users to view added items
Application Architecture
graph TD
App["App (app.ts)"]
SiteHeader["SiteHeaderComponent\n(bot-site-header)"]
Router["RouterOutlet"]
Catalog["CatalogComponent\n(bot-catalog)"]
Cart["CartComponent\n(bot-cart)"]
ProductDetails["ProductDetailsComponent\n(bot-product-details)"]
CartItem["CartItemComponent\n(bot-cart-item)"]
CartService["CartService\n(singleton)"]
InventoryService["InventoryService\n(singleton)"]
App --> SiteHeader
App --> Router
Router --> Catalog
Router --> Cart
Catalog --> ProductDetails
Cart --> ProductDetails
CartService --> Catalog
CartService --> Cart
InventoryService --> ProductDetails
Root Component Structure
// app.ts
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { SiteHeaderComponent } from "./site-header/site-header.component";
@Component({
selector: 'app-root',
imports: [RouterOutlet, SiteHeaderComponent],
templateUrl: './app.html',
styleUrl: './app.css'
})
export class App {
title = 'joes-robot-shop';
}
<!-- app.html -->
<bot-site-header />
<router-outlet />
Data Model
// product.model.ts
export interface IProduct {
id: number;
description: string;
productNotes?: string; // optional field
name: string;
imageName: string;
category: string;
price: number;
discount: number;
}
1.2 Conditional Rendering with @if
The control flow block @if replaces the older ngIf directive in modern Angular. It controls what Angular renders based on conditions.
Basic Syntax
@if (condition) {
<!-- content shown if true -->
} @else if (otherCondition) {
<!-- content shown if other condition -->
} @else {
<!-- content shown otherwise -->
}
Important: The curly braces
{}are required, even for a single line. Unlike TypeScript/JavaScript where you can omit them for a single-lineif.
Example — Displaying available inventory with pills
Available Inventory: {{ availableInventory() }}
@if (availableInventory() === 1) {
<span class="pill red">Only one left!</span>
} @else if (availableInventory() < 1) {
<span class="pill red">Out of stock!</span>
} @else if (availableInventory() < 5) {
<span class="pill yellow">Few left!</span>
} @else {
<span class="pill">Get yours today!</span>
}
Example — Displaying price with discount
<p [ngClass]="getPriceClasses()">{{ p.price | currency }}</p>
@if (p.discount > 0) {
<p>{{ p.price * (1 - p.discount) | currency }}</p>
}
1.3 Expression Aliases with as
You can capture the result of a conditional expression with an as alias. This avoids repeating a long expression in the @if block.
Syntax
@if (expression; as variableName) {
<!-- use variableName here -->
}
Example — Optional display of product notes
@if (p.productNotes; as notes) {
<p>Product Notes: {{ notes }}</p>
}
Limitation: The variable created with
asis scoped only to the@ifblock in which it is defined. It is not available in the corresponding@elseor@else ifblocks.
1.4 Conditional Rendering with @switch @case
@switch provides an alternative to chained @if blocks for handling multiple possible values.
Differences from classic switches
- No fall-through between cases (no need for
break) - Case content is HTML, not JavaScript
Example — Transforming product categories
@switch(p.category) {
@case('heads') {
Head
}
@case('arms') {
Arm
}
@case('bases') {
Base
}
@case('torsos') {
Torso
}
@default {
{{ p.category }}
}
}
1.5 Local Variables in Templates with @let
@let allows you to declare local variables in a template to avoid repeated evaluations and improve readability.
Main use cases
- Avoid unwrapping a signal multiple times
- Improve readability with derived values
- Performance gains for expensive computed signals
Syntax
@let variableName = expression;
Example — Unwrapping a product signal
@let p = product();
@let discountedPrice = p.price * (1 - p.discount) | currency;
<h2>{{ p.name }}</h2>
<p>{{ p.description }}</p>
<p>Discounted price: {{ discountedPrice }}</p>
Without @let, you would need to write product().name, product().description, etc. — each call being a separate signal evaluation.
1.6 Template Reference Variables
Template reference variables allow you to reference a DOM element from another place in the same template.
Syntax
<element #variableName>...</element>
The #variableName variable returns the native browser DOM element.
Example — Scroll to the Checkout button
<!-- Button at the top that scrolls to the bottom -->
<button (click)="checkout.scrollIntoView({behavior: 'smooth'})">
Proceed to Checkout
</button>
<!-- ... product list ... -->
<!-- Reference to this element -->
<span #checkout>
@if(hasCartItems) {
<button class="cta">Checkout</button>
}
</span>
scrollIntoView()is a native JavaScript DOM method, not Angular-specific.
1.7 Template Reference Variable Scope
The scope of template reference variables is limited to the block in which they are declared.
Problem — Variable inside an @if
<!-- ERROR: checkout is defined inside an @if, inaccessible from outside -->
@if(hasCartItems) {
<button #checkout class="cta">Checkout</button>
}
<button (click)="checkout.scrollIntoView()"> <!-- ← Error here -->
Proceed to Checkout
</button>
Solution — Move the variable to a parent element that is always present
<span #checkout> <!-- ← span always present -->
@if(hasCartItems) {
<button class="cta">Checkout</button>
}
</span>
1.8 @empty Fallback in @for Loops
@for loops support an @empty block displayed when the array is empty.
Syntax
@for (item of items; track item.id) {
<!-- content for each element -->
} @empty {
<!-- content shown if the array is empty -->
}
Example — Empty cart
<ul>
@for (product of cartItems(); track product.id) {
<li>
<bot-product-details [product]="product" />
</li>
} @empty {
<h2>You have no items in your cart</h2>
}
</ul>
1.9 Efficient List Rendering with @for and track
@for is a control flow construct that replaces the older *ngFor directive. The track clause is required.
Why track is required
track uniquely identifies each element in the array. When data changes, Angular can update only the modified elements instead of re-rendering the entire list.
flowchart LR
DataChange["Data changed"] --> TrackCheck["Check via track"]
TrackCheck --> |"Element found"| UpdateOnly["Partial update\n(performant)"]
TrackCheck --> |"Element not found"| Rerender["Full re-render\n(slow)"]
Example — Displaying the catalog
<ul>
@for (product of products; track product.id) {
<li>
<bot-product-details [product]="product" />
</li>
}
</ul>
Recommendation: Always use a unique ID for
track. If your data doesn’t have one, strongly consider adding one.
Alternative if no unique ID
@for (item of items; track $index) { ... }
$index can be used as a last resort, but a real ID is preferable.
1.10 Contextual Variables in @for Loops
Inside a @for block, several contextual variables are available using the let syntax:
| Variable | Description |
|---|---|
$index | Index of the current element (0-based) |
$count | Total number of elements in the array |
$even | true if the index is even |
$odd | true if the index is odd |
$first | true for the first element |
$last | true for the last element |
Example — Using multiple contextual variables
@for (
product of products;
track product.id;
let idx = $index,
count = $count,
even = $even,
odd = $odd,
first = $first,
last = $last
) {
<li [class.alt]="odd" [class.top]="first">
<div>Product {{ idx + 1 }} of {{ count }}</div>
<bot-product-details [product]="product" />
</li>
}
Example CSS — Alternating style
/* catalog.component.css */
.alt {
background-color: #f5f5f5;
}
Module 2 — Angular Pipes
2.1 Angular Built-in Pipes
Angular pipes transform values directly in HTML templates without bloating component logic.
Categories of built-in pipes
mindmap
root((Angular Pipes))
Text formatting
lowercase
uppercase
titlecase
Number formatting
decimal
currency
percent
Dates
date
Internationalization i18n
i18nSelect
i18nPlural
Specialized
async
json
keyvalue
slice
Text pipes
| Pipe | Description | Example |
|---|---|---|
lowercase | All lowercase | {{ text | lowercase }} |
uppercase | All uppercase | {{ text | uppercase }} |
titlecase | First letter of each word capitalized (except short words) | {{ text | titlecase }} |
Number pipes
| Pipe | Description | Example |
|---|---|---|
decimal | Decimal formatting with precision | {{ value | number:'1.2-2' }} |
currency | Currency formatting | {{ price | currency:'GBP':'symbol' }} |
percent | Percentage formatting | {{ ratio | percent:'1.0-2' }} |
2.2 Using Built-in Pipes
Basic Syntax
{{ expression | pipeName }}
{{ expression | pipeName:param1:param2 }}
Example — CurrencyPipe with parameters
<!-- Default currency (USD) -->
{{ p.price | currency }}
<!-- In British pounds with symbol -->
{{ p.price | currency:'GBP':'symbol' }}
<!-- In British pounds with code -->
{{ p.price | currency:'GBP':'code' }}
Required import
All Angular built-in pipes come from CommonModule or @angular/common:
// product-details.component.ts
import { CommonModule } from '@angular/common';
@Component({
imports: [CommonModule], // imports all built-in pipes at once
...
})
Example — i18nPlural pipe
// In the component
inventoryMap = {
'=0': 'Out of Stock',
'=1': 'Only one left!',
'=2': 'Few left!',
'=3': 'Few left!',
'=4': 'Few left!',
'other': 'Get yours today!'
};
<span class="pill" [class.red]="availableInventory() < 5">
{{ availableInventory() | i18nPlural:inventoryMap }}
</span>
2.3 Creating Custom Pipes
A custom pipe is a TypeScript class decorated with @Pipe that implements the PipeTransform interface.
Custom pipe structure
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'pipeName' // name used in templates
})
export class MyPipe implements PipeTransform {
transform(value: InputType, ...args: any[]): OutputType {
// transformation logic
return transformedValue;
}
}
Generating with Angular CLI
ng generate pipe categoryToPartType
# or shorthand:
ng g pipe categoryToPartType
This generates: category-to-part-type-pipe.ts and category-to-part-type-pipe.spec.ts.
Example — CategoryToPartTypePipe
This pipe transforms plural lowercase category names (heads, arms) into singular capitalized part names (Head, Arm).
// category-to-part-type-pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'categoryToPartType'
})
export class CategoryToPartTypePipe implements PipeTransform {
transform(category: string, uppercase: boolean): string {
if (uppercase)
return category[0].toUpperCase() + category.slice(1, -1);
else
return category.slice(0, -1);
}
}
<!-- Usage in the template -->
<p>Part Type: {{ p.category | categoryToPartType:true }}</p>
This pipe advantageously replaces the
@switchblock used in module 1.
Example — FilterByCategoryPipe
// filter-by-category-pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
import { IProduct } from './product.model';
@Pipe({
name: 'filterByCategory'
})
export class FilterByCategoryPipe implements PipeTransform {
transform(products: IProduct[], category: string | null): IProduct[] {
if (!category)
return products;
return products.filter(p => p.category === category);
}
}
<!-- Usage: filters products by category -->
@for (product of products | filterByCategory:categoryFilter; track product.id) {
...
}
2.4 Pure vs Impure Pipes
Pure pipes (default)
A pure pipe does not re-evaluate its transformation if the reference of the object has not changed. It does not detect internal mutations (property change, item added to an array).
@Pipe({
name: 'myPipe',
// pure: true ← default value, no need to specify
})
Impure pipes
An impure pipe re-evaluates on every change detection cycle. More flexible but potentially expensive in terms of performance.
@Pipe({
name: 'myPipe',
pure: false // ← avoid if possible
})
Illustrated problem: FilterByCategoryPipe
sequenceDiagram
participant User
participant CatalogComponent
participant FilterByCategoryPipe
User->>CatalogComponent: Click "Add New"
CatalogComponent->>CatalogComponent: this.products = [...this.products, newProduct]
Note over CatalogComponent: New array reference → pipe re-evaluates ✓
User->>CatalogComponent: Direct mutation (e.g. products.push())
Note over FilterByCategoryPipe: Same reference → pipe does NOT re-evaluate ✗
Best practice: Always create a new array instead of mutating the existing one so that pure pipes detect the change.
// ✓ Correct — creates a new reference
this.products = [...this.products, newProduct];
// ✗ Incorrect — mutates the existing array, pure pipe won't detect it
this.products.push(newProduct);
Module 3 — Content Projection and Inter-component Communication
3.1 Content Projection with ng-content
Content projection allows a parent component to pass arbitrary HTML content to a child component, which displays it at a defined location.
Use case
In the application, ProductDetailsComponent and CartItemComponent are nearly identical. The only difference is the button: Buy for the catalog and Remove for the cart. Content projection solves this duplication.
Content projection flow
flowchart LR
Parent["Parent Component\n(catalog or cart)"]
Child["ProductDetailsComponent"]
Slot["<ng-content />"]
Parent -- "projected content\n(Buy or Remove button)" --> Child
Child -- "displayed at position" --> Slot
In the child component — define the slot
<!-- product-details.component.html -->
<div class="price">
<p>{{ p.price | currency }}</p>
<ng-content /> <!-- ← projected content is displayed here -->
</div>
In the parent component — pass the content
<!-- catalog.component.html -->
<bot-product-details [product]="product">
<button class="cta" (click)="addToCart(product)">Buy</button>
</bot-product-details>
<!-- cart.component.html -->
<bot-product-details [product]="product">
<button class="remove" (click)="removeFromCart(product)">Remove</button>
</bot-product-details>
3.2 Input Properties — Passing Data to a Child Component
Input properties allow passing data from a parent component to a child component.
Syntax in the child component
import { input } from '@angular/core';
export class ProductDetailsComponent {
// Simple input (optional value)
product = input<IProduct>();
// Input with default value
mode = input<'shop' | 'cart'>('shop');
// Required input
product = input.required<IProduct>();
}
Syntax in the parent component (binding)
<!-- Passing an object — requires square brackets [] -->
<bot-product-details [product]="myProductObject" />
<!-- Passing a string — brackets optional -->
<bot-product-details name="Jim Cooper" />
<!-- Passing a number — brackets required for the expression -->
<bot-product-details [age]="5" />
<!-- Setting the mode -->
<bot-product-details mode="shop" [product]="product" />
Rule: Square brackets
[]are needed when you want Angular to evaluate the value as an expression. Without brackets, it is always a string.
3.3 Input Property Types and Required Properties
Required input with input.required
// Before — loose typing, can be undefined
product = input<any>();
// After — typed and required
product = input.required<IProduct>();
With input.required, Angular generates a compile-time error if the parent uses the component without passing this property.
Benefits of strong typing
// catalog.component.html — Angular flags an error for wrong type
<bot-product-details [product]="{}" /> // ✗ Error: {} does not match IProduct
<bot-product-details [product]="product" /> // ✓ Correct
Lifecycle and input availability
stateDiagram-v2
[*] --> Construction: new Component()
Construction --> InputsSet: ngOnInit() / signals ready
InputsSet --> ChangeDetection: Change detection
ChangeDetection --> InputsSet: Parent data changes
ChangeDetection --> [*]: Destruction
note right of Construction : Input signals\nare available\nfrom construction
3.4 Output Properties — Child to Parent Communication
Output properties allow a child component to notify its parent component that an event has occurred.
Syntax in the child component
import { output } from '@angular/core';
import { IProduct } from '../product.model';
export class ProductDetailsComponent {
// Output without data
clicked = output<void>();
// Output with data
addToCart = output<IProduct>();
removeFromCart = output<IProduct>();
}
Emitting the event
add() {
this.addToCart.emit(this.product());
}
remove() {
this.removeFromCart.emit(this.product());
}
Child component template
<!-- product-details.component.html -->
@if (mode() === 'shop') {
<button class="cta" (click)="add()">Buy</button>
} @else {
<button class="remove" (click)="remove()">Remove</button>
}
3.5 Using Output Properties
Syntax in the parent component (listening to the event)
<!-- catalog.component.html -->
<bot-product-details
mode="shop"
[product]="product"
(addToCart)="addToCart($event)"
/>
<!-- cart.component.html -->
<bot-product-details
mode="cart"
[product]="product"
(removeFromCart)="removeFromCart($event)"
/>
Handling in the parent component
// catalog.component.ts
addToCart(product: IProduct) {
this.cartService.addToCart(product);
}
// cart.component.ts
removeFromCart(product: IProduct) {
this.cartService.removeFromCart(product);
}
Complete communication flow
sequenceDiagram
participant Parent as CatalogComponent (parent)
participant Child as ProductDetailsComponent (child)
participant Service as CartService
Parent->>Child: [product]="product" (input)
Parent->>Child: mode="shop" (input)
Note over Child: User clicks "Buy"
Child->>Child: add() called
Child-->>Parent: (addToCart) emitted with IProduct
Parent->>Service: cartService.addToCart(product)
3.6 Two-way Binding with Model Inputs
Model inputs enable bidirectional binding: the parent can initialize a value and the child can update that same value.
Use case
A custom SliderComponent whose value needs to:
- Be initializable from the parent (default)
- Be sent back to the parent when it changes
Creating the SliderComponent
// slider.component.ts
import { Component, model } from '@angular/core';
@Component({
selector: 'bot-slider',
templateUrl: './slider.component.html',
styleUrl: './slider.component.css'
})
export class SliderComponent {
sliderValue = model(0); // ← model input with default value 0
}
<!-- slider.component.html -->
<input
#slider
type="range"
[value]="sliderValue()"
(input)="sliderValue.set(+slider.value)"
min="1"
max="5"
/>
Usage in the parent component
// product-details.component.ts
favorite = signal(3); // parent signal
<!-- product-details.component.html -->
<div>Favorite rating: {{ favorite() }}</div>
<!-- Two-way binding with [()] "banana in a box" syntax -->
<bot-slider [(sliderValue)]="favorite" />
Explanation of [()] syntax
flowchart LR
Parent["favorite (signal)"]
Child["sliderValue (model)"]
Parent -- "[sliderValue]='favorite'\n(binding to child)" --> Child
Child -- "(sliderValueChange)\n(event to parent)" --> Parent
Note["[(sliderValue)]='favorite'\n= syntactic sugar for both"]
The [(sliderValue)] syntax is equivalent to:
<bot-slider
[sliderValue]="favorite"
(sliderValueChange)="favorite.set($event)"
/>
3.7 Implicit Change Events of Model Inputs
Each model input automatically generates a corresponding output event, named modelInputName + "Change".
Example — listening to sliderValueChange
<bot-slider
[(sliderValue)]="favorite"
(sliderValueChange)="handleSliderChange($event)"
/>
// product-details.component.ts
handleSliderChange(newValue: number) {
console.log('New slider value:', newValue);
}
This allows running custom code on every change while keeping the two-way binding.
3.8 Transforming Input Property Values
It is possible to apply a transform function to an input property, which modifies the value before it is available in the component.
Syntax
property = input<InternalType, ExternalType>(defaultValue, {
transform: (value: ExternalType) => value as InternalType
});
When a transform is defined, both types must be specified:
input<ReturnType, ReceivedType>.
Example — Normalizing the discount
// product-details.component.ts
product = input.required<IProduct, IProduct>({
transform: this.normalizeDiscount
});
normalizeDiscount(product: IProduct): IProduct {
// If discount >= 1, assume it is an integer percentage (e.g. 20 = 20%)
if (product.discount < 1)
return product;
return { ...product, discount: product.discount / 100 };
}
3.9 Inter-component Communication with Services
In addition to input/output properties, shared services enable indirect communication between components. This is particularly useful for managing shared state.
CartService — Cart Management
// cart.service.ts
import { Injectable, signal } from '@angular/core';
import { IProduct } from './product.model';
@Injectable({
providedIn: 'root' // singleton across the entire application
})
export class CartService {
cart = signal<IProduct[]>([]);
addToCart(product: IProduct) {
this.cart.update(cart => [...cart, product]);
}
removeFromCart(product: IProduct) {
this.cart.update(cart => cart.filter(p => p.id !== product.id));
}
}
InventoryService — Shared Inventory Management
// inventory.service.ts
import { Injectable, signal } from '@angular/core';
import allProducts from './products.json';
@Injectable({
providedIn: 'root'
})
export class InventoryService {
private inventoryFromProducts = allProducts.reduce((acc, product) => {
acc[product.id] = 5;
return acc;
}, {} as Record<number, number>);
private allInventory = signal<Record<number, number>>(this.inventoryFromProducts);
get(productId: number): number {
return this.allInventory()[productId];
}
decrement(productId: number) {
this.allInventory.update(i => ({ ...i, [productId]: i[productId] - 1 }));
}
increment(productId: number) {
this.allInventory.update(i => ({ ...i, [productId]: i[productId] + 1 }));
}
}
Why a shared service solves the sync problem
Before (bug): Each instance of ProductDetailsComponent had its own availableInventory = signal(5), so the Catalog and Cart pages were out of sync.
After (fixed): InventoryService is a singleton — any modification is reflected across all components that access it.
// product-details.component.ts — fixed version
availableInventory = computed(() => this.inventoryService.get(this.product().id));
constructor(private inventoryService: InventoryService) {}
Service-based communication architecture
graph LR
CatalogComp["CatalogComponent"]
CartComp["CartComponent"]
ProductDetails["ProductDetailsComponent"]
CartService["CartService\n(signal)"]
InventoryService["InventoryService\n(signal)"]
CatalogComp -->|"addToCart()"| CartService
CartComp -->|"removeFromCart()"| CartService
CartService -->|"cart signal"| CartComp
ProductDetails -->|"get(id)"| InventoryService
ProductDetails -->|"decrement(id)"| InventoryService
3.10 Challenge: Fixing the Cart Bug
Bug description
If multiple copies of the same item are added to the cart, clicking Remove deletes all of them at once (because the filter by id removes all occurrences).
Possible solutions
- Modify
CartServiceto group identical products at the source - Add a computed property in
CartComponentthat transforms the array - Create a custom pipe
groupByProductsimilar toFilterByCategoryPipe
// Example grouping logic
interface CartGroup {
product: IProduct;
quantity: number;
}
// In a pipe or computed:
transform(products: IProduct[]): CartGroup[] {
return products.reduce((acc, product) => {
const existing = acc.find(g => g.product.id === product.id);
if (existing) {
existing.quantity++;
} else {
acc.push({ product, quantity: 1 });
}
return acc;
}, [] as CartGroup[]);
}
Application Architecture
Final Overview (Module 3)
graph TB
subgraph "App Shell"
App["App Component"]
SiteHeader["SiteHeaderComponent"]
Router["RouterOutlet"]
end
subgraph "Pages"
Catalog["CatalogComponent"]
Cart["CartComponent"]
end
subgraph "Reusable Components"
ProductDetails["ProductDetailsComponent\n(mode: shop | cart)"]
Slider["SliderComponent\n(two-way model input)"]
end
subgraph "Services (Singletons)"
CartService["CartService\ncart: signal<IProduct[]>"]
InventoryService["InventoryService\nallInventory: signal<Record>"]
end
subgraph "Pipes"
CategoryPipe["CategoryToPartTypePipe\ncategoryToPartType"]
FilterPipe["FilterByCategoryPipe\nfilterByCategory"]
end
App --> SiteHeader
App --> Router
Router --> Catalog
Router --> Cart
Catalog --> ProductDetails
Cart --> ProductDetails
ProductDetails --> Slider
ProductDetails -.->|"input: product, mode"| Catalog
ProductDetails -.->|"output: addToCart, removeFromCart"| Catalog
ProductDetails -.->|"input: product, mode"| Cart
ProductDetails -.->|"output: removeFromCart"| Cart
Catalog --> CartService
Cart --> CartService
ProductDetails --> InventoryService
Catalog --> FilterPipe
ProductDetails --> CategoryPipe
Evolution of ProductDetailsComponent
| Module | Features |
|---|---|
| 01 | input<any>(), local availableInventory signal, inline Buy button, @if/@switch/@let/@for |
| 02 | CurrencyPipe, i18nPlural, CategoryToPartTypePipe, FilterByCategoryPipe |
| 03 | input.required<IProduct>, mode input, output<IProduct>, two-way SliderComponent, InventoryService, transforms |
Key Concepts Summary
Control Flow Blocks
| Syntax | Role | Replaces |
|---|---|---|
@if | Conditional rendering | *ngIf |
@else if | Alternative condition | *ngIf + else |
@switch/@case | Switch conditional | Multiple *ngIf |
@for | List rendering loop | *ngFor |
@empty | Fallback for empty array in @for | — |
@let | Local variable declaration in the template | — |
Component Communication
flowchart TD
subgraph "Parent → Child"
IP["Input Properties\n[property]='value'"]
end
subgraph "Child → Parent"
OP["Output Properties\n(event)='handler($event)'"]
end
subgraph "Bidirectional"
MI["Model Inputs\n[(property)]='signal'"]
end
subgraph "Between any components"
SS["Shared Services\n(Singletons with signals)"]
end
subgraph "Flexible content"
CP["Content Projection\n<ng-content />"]
end
Pipes — Summary
| Type | Change detection | Performance | When to use |
|---|---|---|---|
| Pure (default) | Reference change only | Excellent | Immutable data or new references |
Impure (pure: false) | Every detection cycle | Potentially expensive | Unavoidable mutations (avoid) |
Best Practices Summary
- Always provide a
trackwith a unique ID in@for - Type input properties (avoid
any) and useinput.requiredwhen necessary - Import
CommonModuleto access all built-in pipes - Create new references for arrays/objects rather than mutating them, so pure pipes work correctly
- Use services to share state between non parent-child components
- Prefer
@letto avoid repeated signal and expensive pipe calls - Limit impure pipes — always consider a pure alternative first
Search Terms
angular · components · templates · frontend · development · component · syntax · pipes · parent · child · communication · input · content · flow · properties · required · variables · application · architecture · built-in · cart · case · displaying · model