Beginner

Angular: Components and Templates

Template control flow, pipes, content projection and inter-component communication.

Demo application: Joe’s Robot Shop (e-commerce app for robot parts)

Table of Contents

  1. Module 1 — Control Flow in Angular Templates
  2. Module 2 — Angular Pipes
  3. Module 3 — Content Projection and Inter-component Communication
  4. Application Architecture
  5. 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-line if.

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 as is scoped only to the @if block in which it is defined. It is not available in the corresponding @else or @else if blocks.


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:

VariableDescription
$indexIndex of the current element (0-based)
$countTotal number of elements in the array
$eventrue if the index is even
$oddtrue if the index is odd
$firsttrue for the first element
$lasttrue 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

PipeDescriptionExample
lowercaseAll lowercase{{ text | lowercase }}
uppercaseAll uppercase{{ text | uppercase }}
titlecaseFirst letter of each word capitalized (except short words){{ text | titlecase }}

Number pipes

PipeDescriptionExample
decimalDecimal formatting with precision{{ value | number:'1.2-2' }}
currencyCurrency formatting{{ price | currency:'GBP':'symbol' }}
percentPercentage 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 @switch block 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

  1. Modify CartService to group identical products at the source
  2. Add a computed property in CartComponent that transforms the array
  3. Create a custom pipe groupByProduct similar to FilterByCategoryPipe
// 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

ModuleFeatures
01input<any>(), local availableInventory signal, inline Buy button, @if/@switch/@let/@for
02CurrencyPipe, i18nPlural, CategoryToPartTypePipe, FilterByCategoryPipe
03input.required<IProduct>, mode input, output<IProduct>, two-way SliderComponent, InventoryService, transforms

Key Concepts Summary

Control Flow Blocks

SyntaxRoleReplaces
@ifConditional rendering*ngIf
@else ifAlternative condition*ngIf + else
@switch/@caseSwitch conditionalMultiple *ngIf
@forList rendering loop*ngFor
@emptyFallback for empty array in @for
@letLocal 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

TypeChange detectionPerformanceWhen to use
Pure (default)Reference change onlyExcellentImmutable data or new references
Impure (pure: false)Every detection cyclePotentially expensiveUnavoidable mutations (avoid)

Best Practices Summary

  1. Always provide a track with a unique ID in @for
  2. Type input properties (avoid any) and use input.required when necessary
  3. Import CommonModule to access all built-in pipes
  4. Create new references for arrays/objects rather than mutating them, so pure pipes work correctly
  5. Use services to share state between non parent-child components
  6. Prefer @let to avoid repeated signal and expensive pipe calls
  7. 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

Interested in this course?

Contact us to book it or get a custom training plan for your team.