Components are Angular's atom: a TypeScript class, an HTML template, and a selector that makes them composable like HTML elements. Modern Angular components are standalone (no NgModules), state is signals, and templates use the built-in @if/@for control flow. This tutorial builds real components on Angular 20 — all code verified with ng build in the companion workspace.

Anatomy of a component
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<h1>Angular examples — geeksarray.com</h1>
<p>Cart: {{ cart().join(', ') || 'empty' }}</p>
`,
})
export class App {
readonly cart = signal<string[]>([]);
}
Four pieces:
@Componentdecorator — metadata connecting class to template.selector— the tag name (<app-root />) other templates use.template/templateUrl— inline for small components, separate.htmlfile when it grows. Same forstyles/styleUrl, which are scoped to the component by default — styles don't leak out.- The class — state and behavior the template binds to.
Since Angular 19, standalone is the default: any component can be used by any other by adding it to the imports array — no module registration layer. If you see NgModule in a tutorial, it predates the current model.
State with signals
export class App {
readonly productId = signal(1);
readonly showLifecycle = signal(true);
readonly cart = signal<string[]>([]);
onAddToCart(sku: string): void {
this.cart.update(items => [...items, sku]);
}
}
A signal is a reactive value: read it by calling it (cart()), replace with .set(x), derive-from-previous with .update(fn). When a signal changes, Angular re-renders exactly the views that read it — fine-grained reactivity that replaces zone-based guessing. computed() derives values that update automatically:
readonly cartCount = computed(() => this.cart().length);
Plain properties still work in templates, but signals are the current idiom and unlock Angular's zoneless future — new code should default to them.
Templates: binding and control flow
<button (click)="productId.set(productId() + 1)">Change product</button>
@if (showLifecycle()) {
<app-lifecycle-demo [productId]="productId()" />
}
@for (p of products; track p.sku) {
<app-product-card [product]="p" (addToCart)="onAddToCart($event)" />
}
The binding vocabulary: {{ expr }} interpolates text, [property]="expr" binds data in, (event)="handler($event)" binds events out, and [(ngModel)] two-ways form fields. The @if/@for/@switch blocks replaced *ngIf/*ngFor — no CommonModule import needed, better type narrowing, and @for requires track, which fixed a decade of accidental list-rendering performance bugs. @empty handles the zero-item case inline.
Composition: components using components
The app above composes three children; here's the shape of one:
@Component({
selector: 'app-product-card',
imports: [CurrencyPipe],
template: `
<div class="card">
<h4>{{ product().name }}</h4>
<p>{{ product().price | currency:'INR' }}</p>
<button [disabled]="product().stock === 0" (click)="addToCart.emit(product().sku)">
Add to cart
</button>
</div>
`,
})
export class ProductCard {
readonly product = input.required<Product>();
readonly addToCart = output<string>();
}
Data flows down via input(), events flow up via output() — the contract that makes components reusable. (That pattern gets its own deep dive in the nested components article.) Note imports: [CurrencyPipe] — a standalone component declares exactly what its template uses, which keeps bundles honest: unused imports are flagged, and the build tree-shakes per component.
Verified from the companion workspace:
Application bundle generation complete. [2.779 seconds]
Initial total | 268.13 kB
Component design guidelines that age well
- Smart vs presentational: components that fetch and orchestrate (pages) vs components that render inputs and emit outputs (cards, rows). Presentational components are trivially testable and reusable; keep most components in that camp.
- Small templates: when a template needs scrolling, extract children. The
importsarray makes extraction cheap. - Logic in the class, not the template:
@if (canCheckout())with acomputedbeats a three-condition template expression — testable and named. - Lifecycle awareness: initialization belongs in
ngOnInit, cleanup inngOnDestroy— the lifecycle article walks the full hook order with logged proof.
Components plus directives (behavior without templates) and services (shared state and data access) complete Angular's core triad. The full workspace for this series — every component here, building and running — is in the companion repository: npm install && ng serve.
Comments (0)
No comments yet — be the first to share your thoughts.