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 How toand controlwhatdataisinshowntemplatesusing(withAngular’sthe moderncontrol-flow@if,syntax@for,@switchsyntax)- How components
communicatetalkwithto each other using inputs and outputs - How to style components and
encapsulate component-specificmanage CSS encapsulation - How
to work with componentlifecycle hooks How Angular handlesand change detection work at a high level- Where older syntax (
*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)of Atemplate()<span class="editor-theme-code">.html</span>product-list.component.tsAproduct-list.htmlproduct-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:
itsProperty'app-root'Purpose–`selector`theTagtagnameyouusedwill use intemplatesindex.html.`templateUrl`/
pointstyleUrl<span–class="editor-theme-code">template</span>Definesto theUIexternal`styleUrl`HTML
and/CSS files.
<spanimports:class="editor-theme-code">styles</span>[RouterOutlet]Defines– because Angular 16+ uses standalone components, every componentCSSexplicitly`imports`Standalonewhatcomponentsit needs (other components, directives, pipes).standalone: true– tells Angular that thisoneclassdependsstands onModernown and is not declared in an NgModule.- The class is named
App(notAppComponent) to match the new naming style.
Legacy note:
In older Angular (v16+)versions, noteyou
Standalonewould componentstypically aresee:
- File:
standard.app.component.ts - Class:
useAppComponent - No
<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:
- The class name is
ProductList(notProductListComponent), 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: truemakes this component directly usable in other components via theimportsarray.
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:
- Importing
ProductListinAppand putting it intoimportsmakes Angular aware of theapp-product-listselector inthethisHTML:template.
<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 evaluatestitlein 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.targetsfrom DOMthe properties,component notto HTMLthe attributes.
Angular’s Modern Control FlowFlow: Syntax (@if, @for, @switch)@switch
Angular 1717+ introduced a new built-in control-flow syntax that is:
Moremore readableFasterand ClosermoretoefficientJavaScriptthan Smallerthebundle size compared toolder directive-basedtemplates
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:
@ifdecides 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>
WhyExplanation:
<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 important
an empty array.
<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.@casedefines what to render when the expression matches a specific value.@defaultis 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’sclickevent and executes the assignment in the component instance.selectedProductbecomes the currently clicked product, and the@ifblock 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 eventThe right-hand expression — TypeScript code executed when clicked
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)single Outputsstyle(eventspropertygodynamically,upbasedfromonacomponentchild to a parent)state.
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
@ifguard 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’sselectedProductproperty into the child’sproductinput.- 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 aProductpayload.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’saddedoutput.$eventcontains the product emitted byaddToCart().- 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:
#detailcreates a template referencevariableto theProductDetailinstance.- This reference exposes the public API of
theProductDetailcomponent.(here:product()).
Accessing
You can Instead of usingQuerying a
templatechild variable,in weTypeScript with viewChildqueryalso 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 aProductDetailin this component’s view.ngAfterViewInitis 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>Angulardramaticallywill only re-check this component when:- An input reference changes
- An event handler on this component runs
- An observable bound in the template emits (via async pipe), etc.
-
This significantly improves performance
for:in large and complex UIs.Large listsData-heavy UIsDashboard-like interfaces
ComponentLifecycle Hooks OverviewLifecycle hooks
letallow you to runcodecustom logic at specificmoments.moments in a component’s life.HookCommon
Whenhooks:itngOnInit– runs
`ngOnInit`afterAfterthe component’s inputs areset;first set.ngOnDestroy– runs right before Angular removes the componentinitializedfrom`ngOnDestroy`theBeforeDOM.componentngOnChangesis–removedruns`ngOnChanges`Wheneverwhenever an inputvaluebinding changes.ngAfterViewInit– runs after the view and child views have been initialized.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`ngAfterViewInit`console.log('Changes:',Afterchanges);child} ngAfterViewInit(): void { // Child components andviewsview are readyngOnInitexport class ProductDetailComponent implements OnInit { ngOnInit() {console.log('Product:',Viewthis.product()initialized'); }}Why not use the constructor?Inputs arenot yet assignedwhen the constructor runs.ngOnDestroyUsed 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.ngAfterViewInitto work with child components or DOM elements that weren’t available earlier.
Alternative:
DestroyRef (modern, recommended)constructor(destroyRef: DestroyRef) { destroyRef.onDestroy(() => { // cleanup }); }ngOnChangesTriggered 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 alternativeSignals can track changes more elegantly (Chapter 7).ngAfterViewInitUseful 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 components (
standalone: true) and explicitimportshave become the default way tocreatestructurestandaloneancomponentsapp. HowThe modern control-flow syntax (@if,@for,@switch) replaces older structural directives in new code, while you still need toconnectunderstandtemplates,*ngIf,classes,*ngForandstylesngSwitchfor legacy templates.HowDatatoflowsuseintomoderncomponentscontrolviaflowinput()(and out via, replacing<span class="editor-theme-code">@if</span>output()<span@Inputclass="editor-theme-code">@for</span>and@Output,<spaninclass="editor-theme-code">@switch</span>)new code.How to displayClass andupdatestyledatabindings, How to handle events and user interactionsHow components communicate (inputs & outputs)How to use template reference variablesHow to manage CSSalong with viewencapsulationencapsulation, give you fine control over component-level CSS.HowTemplatetoreferenceoptimizevariablesupdatesandwithviewChildchangeletdetectionyou Howreachto hookdeeper into the component tree when necessary.- Change detection strategies and lifecycle hooks help you tune both performance and behavior.