Part 2 — Structuring User Interfaces with Components

Angular applications are built from components: small, focused building blocks that each own a part of the user interface and its behavior.

In this chapter you will learn:


Anatomy of an Angular Component (Angular 20 Style)

In Angular 20, when you generate a component, the CLI now uses simpler file names:

The idea is to reduce redundancy: the file name already tells you what this unit is.

A minimal root component looks like this:

// src/app/app.ts
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-root',
  templateUrl: './app.html',
  styleUrl: './app.css',
  imports: [RouterOutlet],
  standalone: true
})
export class App {
  title = 'World';
}

Explanation:

Legacy note: In older Angular versions, you would typically see:


Creating a Component with the CLI

To generate a feature component in Angular 20:

ng generate component product-list

With the new naming convention, this will create:

src/app/product-list/product-list.ts
src/app/product-list/product-list.html
src/app/product-list/product-list.css
src/app/product-list/product-list.spec.ts

The TypeScript file might look like this:

// src/app/product-list/product-list.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-product-list',
  templateUrl: './product-list.html',
  styleUrl: './product-list.css',
  standalone: true
})
export class ProductList {
}

Explanation:

To use this component inside App, you import it and add it to the imports array:

// src/app/app.ts
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { ProductList } from './product-list/product-list';

@Component({
  selector: 'app-root',
  templateUrl: './app.html',
  styleUrl: './app.css',
  standalone: true,
  imports: [RouterOutlet, ProductList]
})
export class App {
  title = 'World';
}

And in app.html:

<!-- src/app/app.html -->
<div class="content">
  <app-product-list></app-product-list>
</div>

Explanation:


Displaying Data in the Template

Component templates can render values from the class using interpolation or property binding.

Interpolation

<h1>Hello, {{ title }}</h1>

Explanation:

Property binding

<h1 [innerText]="title"></h1>

Explanation:


Modern Control Flow: @if, @for, @switch

Angular 17+ introduced a new control-flow syntax that is more readable and more efficient than the older directive-based approach.

Conditional rendering with @if

Example with a product list:

// product-list.ts
import { Component } from '@angular/core';

interface Product {
  id: number;
  title: string;
}

@Component({
  selector: 'app-product-list',
  templateUrl: './product-list.html',
  styleUrl: './product-list.css',
  standalone: true
})
export class ProductList {
  products: Product[] = [];
}
<!-- product-list.html -->
@if (products.length > 0) {
  <h1>Products ({{ products.length }})</h1>
} @else {
  <p>No products found!</p>
}

Explanation:

Legacy note (older Angular):

<h1 *ngIf="products.length > 0">Products ({{ products.length }})</h1>
<p *ngIf="products.length === 0">No products found!</p>

*ngIf is still supported, but the new @if syntax is the recommended style going forward.


Looping over data with @for

Let’s populate some mock products:

// product-list.ts
export class ProductList {
  products: Product[] = [
    { id: 1, title: 'Keyboard' },
    { id: 2, title: 'Microphone' },
    { id: 3, title: 'Web camera' },
    { id: 4, title: 'Tablet' }
  ];
}

Now, use @for in the template:

<!-- product-list.html -->
<ul class="pill-group">
  @for (product of products; track product.id) {
    <li class="pill">{{ product.title }}</li>
  } @empty {
    <p>No products found!</p>
  }
</ul>

Explanation:

Legacy note:

<li *ngFor="let product of products">{{ product.title }}</li>

*ngFor is the older syntax with similar behavior.


Switching templates with @switch

You can pick different content based on a value:

<!-- product-list.html -->
<ul class="pill-group">
  @for (product of products; track product.id) {
    <li class="pill">
      @switch (product.title) {
        @case ('Keyboard') { 🎹 }
        @case ('Microphone') { 🎤 }
        @default { 📦 }
      }
      {{ product.title }}
    </li>
  } @empty {
    <p>No products found!</p>
  }
</ul>

Explanation:

Legacy note:

<div [ngSwitch]="product.title">
  <span *ngSwitchCase="'Keyboard'">🎹</span>
  <span *ngSwitchCase="'Microphone'">🎤</span>
  <span *ngSwitchDefault>📦</span>
</div>

Again, [ngSwitch] and *ngSwitchCase are the older equivalents.


Handling User Interaction (Event Binding)

To send information from the template back to the component, Angular uses event bindings.

Extend the ProductList class:

// product-list.ts
export class ProductList {
  products: Product[] = [
    { id: 1, title: 'Keyboard' },
    { id: 2, title: 'Microphone' },
    { id: 3, title: 'Web camera' },
    { id: 4, title: 'Tablet' }
  ];

  selectedProduct: Product | undefined;
}

Update the template:

<!-- product-list.html -->
<ul class="pill-group">
  @for (product of products; track product.id) {
    <li class="pill" (click)="selectedProduct = product">
      {{ product.title }}
    </li>
  } @empty {
    <p>No products found!</p>
  }
</ul>

@if (selectedProduct) {
  <p>You selected: <strong>{{ selectedProduct.title }}</strong></p>
}

Explanation:


Styling Components and View Encapsulation

Angular lets you bind classes and styles dynamically.

Class binding

<li
  class="pill"
  [class.selected]="selectedProduct && selectedProduct.id === product.id"
>
  {{ product.title }}
</li>

Explanation:

You can also bind an entire object:

// product-list.ts
isSelected(product: Product) {
  return this.selectedProduct?.id === product.id;
}
<li
  class="pill"
  [class.selected]="isSelected(product)"
>
  {{ product.title }}
</li>

Style binding

<p [style.color]="selectedProduct ? 'green' : 'inherit'">
  {{ selectedProduct ? 'Product chosen' : 'No product selected' }}
</p>

Explanation:

View encapsulation

By default, Angular scopes CSS per component (Emulated mode), so styles from product-list.css will only affect that component’s template.

import { Component, ViewEncapsulation } from '@angular/core';

@Component({
  selector: 'app-product-detail',
  templateUrl: './product-detail.html',
  styleUrl: './product-detail.css',
  standalone: true,
  encapsulation: ViewEncapsulation.Emulated // default
})
export class ProductDetail {
}

If you explicitly set:

encapsulation: ViewEncapsulation.None

then styles defined in product-detail.css can leak into other parts of the app. This can be useful for global styling, but must be used carefully.


Passing Data Between Components (Inputs and Outputs)

Real-world applications rarely keep all UI in a single component. Often, a parent component owns the data and passes a piece of it down to a child component.

Passing data down with input()

Create a detail component:

// src/app/product-detail/product-detail.ts
import { Component, input } from '@angular/core';
import type { Product } from '../product-list/product-list';

@Component({
  selector: 'app-product-detail',
  templateUrl: './product-detail.html',
  styleUrl: './product-detail.css',
  standalone: true
})
export class ProductDetail {
  product = input<Product>();
}

Template:

<!-- product-detail.html -->
@if (product()) {
  <p>
    You selected:
    <strong>{{ product()!.title }}</strong>
  </p>
}

Explanation:

Now, use ProductDetail in ProductList:

// product-list.ts
import { Component } from '@angular/core';
import { ProductDetail } from '../product-detail/product-detail';

@Component({
  selector: 'app-product-list',
  templateUrl: './product-list.html',
  styleUrl: './product-list.css',
  standalone: true,
  imports: [ProductDetail]
})
export class ProductList {
  products: Product[] = [ /* ... */ ];
  selectedProduct: Product | undefined;
}
<!-- product-list.html -->
<ul class="pill-group">
  @for (product of products; track product.id) {
    <li class="pill" (click)="selectedProduct = product">
      {{ product.title }}
    </li>
  } @empty {
    <p>No products found!</p>
  }
</ul>

<app-product-detail [product]="selectedProduct"></app-product-detail>

Explanation:

Legacy note: Previously, you would see:

@Input() product!: Product;

instead of product = input<Product>().


Sending events up with output()

Let the detail component notify the parent that the user wants to add the product to a cart.

In ProductDetail:

import { Component, input, output } from '@angular/core';
import type { Product } from '../product-list/product-list';

@Component({
  selector: 'app-product-detail',
  templateUrl: './product-detail.html',
  styleUrl: './product-detail.css',
  standalone: true
})
export class ProductDetail {
  product = input<Product>();
  added = output<Product>();

  addToCart() {
    if (this.product()) {
      this.added.emit(this.product()!);
    }
  }
}

Template:

<!-- product-detail.html -->
@if (product()) {
  <div>
    <p>
      You selected:
      <strong>{{ product()!.title }}</strong>
    </p>
    <button (click)="addToCart()">Add to cart</button>
  </div>
}

Explanation:

In the parent (ProductList):

// product-list.ts
onAdded(product: Product) {
  alert(`${product.title} added to the cart!`);
}
<!-- product-list.html -->
<app-product-detail
  [product]="selectedProduct"
  (added)="onAdded($event)"
></app-product-detail>

Explanation:

Legacy note: Older Angular projects use:

@Output() added = new EventEmitter<Product>();

instead of added = output<Product>().


Template Reference Variables and viewChild

Sometimes you need direct access to a child component instance.

Template reference variable

<!-- product-list.html -->
<app-product-detail
  #detail
  [product]="selectedProduct"
  (added)="onAdded($event)"
></app-product-detail>

<p *ngIf="detail.product()">
  Detail says: {{ detail.product()!.title }}
</p>

Explanation:

Querying a child in TypeScript with viewChild

You can also get the child instance from the parent class:

// product-list.ts
import { Component, AfterViewInit, viewChild } from '@angular/core';
import { ProductDetail } from '../product-detail/product-detail';

@Component({
  selector: 'app-product-list',
  templateUrl: './product-list.html',
  styleUrl: './product-list.css',
  standalone: true,
  imports: [ProductDetail]
})
export class ProductList implements AfterViewInit {
  productDetail = viewChild(ProductDetail);

  ngAfterViewInit(): void {
    console.log('Detail product:', this.productDetail()?.product());
  }
}

Explanation:

Legacy note: Previously:

@ViewChild(ProductDetail) productDetail!: ProductDetail;

Change Detection Strategy

Angular automatically refreshes views when data changes. By default, it runs change detection for the entire component tree on each relevant event.

You can optimize this with ChangeDetectionStrategy.OnPush:

import { Component, ChangeDetectionStrategy } from '@angular/core';

@Component({
  selector: 'app-product-detail',
  templateUrl: './product-detail.html',
  styleUrl: './product-detail.css',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductDetail {
  // ...
}

Explanation:


Lifecycle Hooks Overview

Lifecycle hooks allow you to run custom logic at specific moments in a component’s life.

Common hooks:

Example:

import {
  Component,
  OnInit,
  OnDestroy,
  OnChanges,
  SimpleChanges,
  AfterViewInit
} from '@angular/core';

@Component({
  selector: 'app-product-detail',
  templateUrl: './product-detail.html',
  styleUrl: './product-detail.css',
  standalone: true
})
export class ProductDetail
  implements OnInit, OnDestroy, OnChanges, AfterViewInit {

  ngOnInit(): void {
    // Good place to fetch data or initialize values
    console.log('ProductDetail initialized');
  }

  ngOnChanges(changes: SimpleChanges): void {
    // React to input changes
    console.log('Changes:', changes);
  }

  ngAfterViewInit(): void {
    // Child components and view are ready
    console.log('View initialized');
  }

  ngOnDestroy(): void {
    // Cleanup: timers, subscriptions, listeners, etc.
    console.log('ProductDetail destroyed');
  }
}

Explanation:


Summary

In this chapter you have seen how, in Angular 20:


Revision #4
Created 2025-11-19 16:19:48 UTC by Carsten
Updated 2025-12-17 19:58:11 UTC by Carsten