Part 2 — Structuring User Interfaces with Components
Structuring User Interfaces with Components
Angular applications are built from components —components: small, isolatedfocused unitsbuilding responsibleblocks forthat renderingeach own a part of the UIuser interface and handling its logic. Each component controls a section of the screen, can receive data, can output events, and participates in a hierarchical tree of parent and child components.behavior.
In this chapter you will learn:
- How
toancreateAngularcomponents20 component is structured - How
totheconnectCLIageneratescomponentcomponentstowithitstheHTMLnewtemplatenamingand CSSscheme - How to display
dynamic data
@if, @for, @switch syntax)
How components *ngIf, @Input, etc.) still appears and how to read it
You will also see short notes explaining how older Angular versions handled these tasks, so you understand both modern best practice and past conventions.
Anatomy of an Angular Component (Angular 20 Style)
AIn componentAngular consists20, of:when you generate a component, the CLI now uses simpler file names:
ATypeScript classproduct-list.ts(UIinsteadlogic)
<span class="editor-theme-code">.html</span>product-list.component.ts)
product-list.html
product-list.css (<spanNinja class="editor-theme-code">.css</span>Squad Blog)
Example: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.component.html',
styleUrl: './app.component.css',
imports: [RouterOutlet],
standalone: true
})
export class AppComponentApp {
title = 'World';
}
Key
Explanation:
selector: index.html.
| |
templateUrl |
/
styleUrl <span– class="editor-theme-code">template</span>
/
CSS files.
<spanimports: class="editor-theme-code">styles</span>[RouterOutlet] standalone: true – tells Angular that this Modernown and is not declared in an NgModule.
The class is named App (not AppComponent) to match the new naming style.
Legacy note:
In older Angular (v16+)versions, noteyou
Standalonewould componentstypically aresee:
app.component.ts
AppComponent
<spanstandalone: class="editor-theme-code">true and no imports</span> array Legacy note
Older projects used NgModules and did not include the <span class="editor-theme-code">imports</span> arraydeclared in theNgModules).
<span class="editor-theme-code">@Component</span> decorator.
Creating a Component with the CLI
InsideTo yourgenerate a feature component in Angular project folder:20:
ng generate component product-list
ThisWith generates:the new naming convention, this will create:
src/app/product-list/
product-list.component.ts
src/app/product-list/product-list.component.html
src/app/product-list/product-list.component.css
src/app/product-list/product-list.component.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:
ProductList (not ProductListComponent), consistent with the updated Angular 20 style guide.(Ninja Squad Blog)
This component does not import anything yet; we will add imports later when needed.
standalone: true makes this component directly usable in other components via the imports array.
To use yourthis newcomponent component,inside App, you import it intoand anotheradd component:it to the imports array:
// src/app/app.ts
import { ProductListComponentComponent } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { ProductList } from './product-list/product-list.component'list';
@Component({
selector: 'app-root',
imports: [RouterOutlet, ProductListComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.css',
standalone: true,
imports: [RouterOutlet, ProductList]
})
export class AppComponentApp {
title = 'World';
}
And placein itsapp.html:
<!-- src/app/app.html -->
<div class="content">
<app-product-list></app-product-list>
</div>
Explanation:
ProductList in App and putting it into imports makes Angular aware of the app-product-list selector in <app-product-list>, ProductList class.
Displaying Data in the Template
AngularComponent templates supportcan multiplerender waysvalues to bring component data intofrom the UI.class using interpolation or property binding.
Interpolation
<h1>Hello, {{ title }}</h1>
Explanation:
{{ title }} is interpolation; Angular evaluates title in the component instance and inserts its string value into the DOM.
Property Bindingbinding
<h1 [innerText]="title"></h1>
Important
Explanation:
Property
[innerText]="title" binds the DOM property innerText of the <h1> to the title property of the component.
The square brackets indicate one-way binding Angular’s Modern Control FlowFlow: Syntax (@if, @for, @switch)@switch
Angular 1717+ introduced a new built-in control-flow syntax that is:
Conditional rendering with @if
Example rendering)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:
@if decides whether the block of HTML should exist in the DOM at all.
If products.length > 0, Angular adds the <h1> to the DOM; otherwise, it adds the <p>.
Legacy note (pre-Angularolder 17)
<span class="editor-theme-code">*ngIf</span> was used: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 foundfound!</p>
}
</ul>
Why
Explanation:
<span@for class="editor-theme-code">(product of products; track</span> product.id) iterates over products and exposes each item as product.
track product.id tells Angular to use the id field to keep DOM nodes stable when items change, improving performance.
@empty defines what to show when products is It helps Angular identify which DOM element corresponds to which data item, improving rendering performance.
Legacy note
Older Angular templates used <span class="editor-theme-code">*ngFor</span>: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:
@switch (product.title) compares the title for each product.
@case defines what to render when the expression matches a specific value.
@default is rendered when no case matches.
Legacy note
Previously:note:
<div [ngSwitch]="product.title">
<pspan *ngSwitchCase="'Keyboard'">🎹</pspan>
<pspan *ngSwitchCase="'Microphone'">🎤</span>
<span *ngSwitchDefault>📦</pspan>
</div>
Again, [ngSwitch] and *ngSwitchCase are the older equivalents.
Adding Interactivity: Events andHandling User InputInteraction (Event Binding)
To reactsend information from the template back to userthe actions,component, Angular uses event bindingbindings.
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:
(click)="selectedProduct = product" listens for the browser’s click event and executes the assignment in the component instance.
selectedProduct becomes the currently clicked product, and the @if block below reacts by showing its title.
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:
<span class="editor-theme-code">(click)</span>— target event
All browser events are supported.
Styling Components
Class Binding
class based on whether the condition evaluates towill add or remove the<p[class.selected]="isSelected"></p>...condition..."selected
true or false.
You can passalso bind an entire object with boolean conditions:object:
currentClasses// =product-list.ts
isSelected(product: Product) {
selected:return true,this.selectedProduct?.id highlighted:=== falseproduct.id;
};
<pli
class="pill"
[class]class.selected]="currentClasses"isSelected(product)"
>
{{ product.title }}
</pli>
Style Bindingbinding
<p [style.color]="selectedProduct ? 'red'green' : 'inherit'"></p>
<p{{ [style.width.px]="120">selectedProduct ? 'Product chosen' : 'No product selected' }}
</p>
Object syntax:
currentStyles = {
color: 'red',
width: '100px'
};
<p [style]="currentStyles"></p>
View Encapsulation
Angular normally isolates component CSS:
encapsulation: ViewEncapsulation.Emulated // default
Other modes:
Example turning isolation off:
encapsulation: ViewEncapsulation.None
Use this carefully.
Component Communication
Components interact via:Explanation:
Inputs[style.color](data goesdownintocontrols achild)
PassingView Data Down (Input Binding)encapsulation
ChildBy component:default, Angular scopes CSS per component (Emulated mode), so styles from product-list.css will only affect that component’s template.
import { Component, inputViewEncapsulation } from '@angular/core';
import { Product } from '../product';
@Component({
selector: 'app-product-detail',
templateUrl: './product-detail.component.html',
styleUrl: './product-detail.css',
standalone: true,
encapsulation: ViewEncapsulation.Emulated // default
})
export class ProductDetailComponentProductDetail {
}
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>();
}
Parent template:Template:
<!-- product-detail.html -->
@if (product()) {
<p>
You selected:
<strong>{{ product()!.title }}</strong>
</p>
}
Explanation:
product = input<Product>() defines an input signal for this component.
In the template, product() reads the current value of that input.
The @if guard ensures we only render details when a product is actually provided.
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>
Required
Explanation:
[product]="selectedProduct" binds the parent’s selectedProduct property into the child’s product input.
Angular takes care of updating the child when the parent selection changes.
Legacy note: Previously, you would see:
@Input() product!: Product;
instead of product = .input.requiredinput<Product>();
Sending Eventsevents Upup (Outputwith Binding)output()
Child: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()!);
}
}
}
Parent:Template:
<!-- product-detail.html -->
@if (product()) {
<div>
<p>
You selected:
<strong>{{ product()!.title }}</strong>
</p>
<button (click)="addToCart()">Add to cart</button>
</div>
}
Explanation:
added = output<Product>() declares an output event that can carry a Product payload.
this.added.emit(...) triggers the event.
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>
ParentExplanation:
(added)="onAdded($event)" listens to the child’s added output.
$event contains the product emitted by addToCart().
The parent can now update a cart, fire analytics, or display a message.
Legacy note: Older Angular projects use:
onAdded(product: Product) {
alert(`${product.title} added to cart!`);
}
Legacy note
In older Angular:
@Input() product!: Product;
@Output() added = new EventEmitter<Product>();
instead of added = output<Product>().
Template Reference Variables and viewChild
UsedSometimes you need direct access to accessa achild component orinstance.
Template instance: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>
AExplanation:
#detail creates a template reference ProductDetail instance.
This reference exposes the public API of ProductDetail product()).
Accessing
Instead of using
Querying a templatechild variable,in weTypeScript with viewChild
You can queryalso get the child viainstance
<spanfrom class="editor-theme-code">viewChild</span>Parentparent component:class:
// product-list.ts
import { viewChild,Component, AfterViewInitAfterViewInit, viewChild } from '@angular/core';
import { ProductDetailComponentProductDetail } from '../product-detail/product-detail.component'detail';
@Component({
selector: 'app-product-list',
templateUrl: './product-list.html',
styleUrl: './product-list.css',
standalone: true,
imports: [ProductDetail]
})
export class ProductListComponentProductList implements AfterViewInit {
productDetail = viewChild(ProductDetailComponent)ProductDetail);
ngAfterViewInit(): void {
console.log('ChildDetail product:', this.productDetail()?.product());
}
}
Explanation:
viewChild(ProductDetail) tells Angular to look for a ProductDetail in this component’s view.
ngAfterViewInit is the hook where the child is guaranteed to be created and accessible.
Legacy notenote:
Previously:
@ViewChild(ProductDetailComponent)ProductDetail) productDetail!: ProductDetailComponent;ProductDetail;
Change Detection Strategy
Angular automatically refreshes views when data changes. By default, it runs change detection for the UIentire whencomponent ittree detectson changes.
each Tworelevant strategies are available:event.
You | |
|---|---|
ChangeDetectionStrategy.OnPush |
Enable :<span class="editor-theme-code">OnPush</span>
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:
With , <spanOnPushclass="editor-theme-code">OnPush</span>Angular dramaticallywill only re-check this component when:
This significantly improves performance for:in large and complex UIs.
Component Lifecycle Hooks Overview
Lifecycle hooks letallow you to run codecustom logic at specific moments.moments in a component’s life.
Common | ngOnInit – runs |
|---|---|
ngOnDestroy – runs right before Angular removes the component | |
ngOnChanges | |
ngAfterViewInit – runs after the view and child views have been initialized.
Example: | |
ngOnInit
export class ProductDetailComponent implements OnInit {
ngOnInit() {
console.log('Product:',View this.product()initialized');
}
}
Why not use the constructor?
Inputs are not yet assigned when the constructor runs.
ngOnDestroy
Used for cleanup:
export class ProductDetailComponent implements OnDestroy {
ngOnDestroy(): void {
// Cleanup: timers, subscriptions, listeners, etc.
console.log('ComponentProductDetail destroyed');
}
}
Common use cases:Explanation:
ClearingtimersEach hook gives you a predictable place to put specific kinds of logic:
ngOnInitinstead of doing heavy work in the constructor.UnsubscribingngOnDestroyfromtoRxJSreleasestreamsresources.ReleasingngOnChangeseventtolistenersreact to new input values.
ngAfterViewInit to work with child components or DOM elements that weren’t available earlier.
Alternative:
constructor(destroyRef: DestroyRef) {
destroyRef.onDestroy(() => {
// cleanup
});
}
ngOnChanges
Triggered when an input changes:
ngOnChanges(changes: SimpleChanges): void {
const product = changes['product'];
if (!product.isFirstChange()) {
console.log('Old:', product.previousValue);
console.log('New:', product.currentValue);
}
}
Modern alternative
Signals can track changes more elegantly (Chapter 7).
ngAfterViewInit
Useful for reading child component data:
ngAfterViewInit() {
console.log(this.productDetail()?.product());
}
Summary
In this chapter,chapter you learned:have seen how, in Angular 20:
HowComponents are generated with simpler file names likeproduct-list.ts,product-list.html,product-list.css.
standalone: true) and explicit imports have become the default way to @if, @for, @switch) replaces older structural directives in new code, while you still need to *ngIf, *ngFor and ngSwitch for legacy templates.
input() <span class="editor-theme-code">@if</span>output(), replacing <span@Input @Output<spanin class="editor-theme-code">@switch</span>viewChild