{{ productName }}
Price: {{ price }}
Category: {{ category }}
Angular is a complete frontend framework used to build dynamic web applications. Unlike a small library that solves only one problem, Angular gives developers a structured way to create full client-side applications with reusable components, navigation, forms, HTTP communication, and testing tools. It exists because large applications become difficult to manage when HTML, JavaScript, data flow, and UI behavior are not organized consistently. Angular solves this by providing clear patterns for application architecture.
In real life, Angular is often used in enterprise systems such as banking dashboards, analytics portals, HR management tools, healthcare systems, education platforms, and admin panels where maintainability matters as much as features. A team can divide the application into components, services, and routes, making the project easier to scale over time.
At its core, Angular is built around a few major ideas: components for UI pieces, templates for rendering data, data binding for connecting logic to the screen, directives for extending HTML behavior, services for reusable business logic, dependency injection for clean architecture, and routing for navigation. Modern Angular applications also commonly use standalone components, although older projects may still use NgModules. Both styles matter because you may encounter either in professional work.
Angular applications are usually created with the Angular CLI. A project contains source files, configuration files, and a main bootstrap process. The basic flow is simple: create a component, write its template, add logic in TypeScript, and display it in the browser.
A component usually has three parts: a TypeScript class, an HTML template, and optional CSS styles. The class stores data and methods. The template displays that data. Angular updates the UI when data changes.
When you see syntax like {{ title }}, that is interpolation. When you see [value], that is property binding. When you see (click), that is event binding. When both directions are combined in forms, it becomes two-way binding, commonly written with [(ngModel)].
Basic example:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `{{ title }}
Welcome to Angular.
`
})
export class AppComponent {
title = 'My First Angular App';
}Real-world example:
import { Component } from '@angular/core';
@Component({
selector: 'app-user-card',
template: `{{ user.name }}
Role: {{ user.role }}
Active: {{ user.active }}
`
})
export class UserCardComponent {
user = { name: 'Asha', role: 'Admin', active: true };
toggleStatus() {
this.user.active = !this.user.active;
}
}Advanced usage:
import { Component } from '@angular/core';
@Component({
selector: 'app-task-list',
template: `
- {{ task }}
`
})
export class TaskListComponent {
newTask = '';
tasks: string[] = ['Learn components', 'Understand binding'];
addTask() {
if (this.newTask.trim()) {
this.tasks.push(this.newTask);
this.newTask = '';
}
}
}ngModel need the correct Angular module or standalone import. Fix by importing required form features.{{ }} and (click) is Angular-specific. Fix by learning binding types carefully.Offline to Online when clicked.Build a simple employee welcome card component that displays an employee name, department, and active status, with a button to toggle the status.
Create a mini Angular dashboard section with one parent component and two child components: one showing profile data and one showing a live-updating task list.
main.ts) bootstraps the root module (AppModule).AppModule declares and imports other modules and components. Angular's modularity helps organize application parts.AppComponent) is loaded, and then, based on its template, it renders child components, forming a hierarchical component tree.// app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: '{{ title }}
Welcome to Angular!
',
styles: ['h1 { color: steelblue; }']
})
export class AppComponent {
title = 'My First Angular App';
}
{{ title }} is an interpolation, displaying the value of the title property from the component class.// child.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-child',
template: `
Child Component
Message from parent: {{ parentMessage }}
`
})
export class ChildComponent {
@Input() parentMessage: string = '';
@Output() childEvent = new EventEmitter();
sendMessage() {
this.childEvent.emit('Hello from child!');
}
}
// app.component.ts (Parent Component)
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `Parent Component
Message from child: {{ childMessage }}
`
})
export class AppComponent {
childMessage: string = '';
handleChildMessage(message: string) {
this.childMessage = message;
}
}
@Input() for parent-to-child communication and @Output() with EventEmitter for child-to-parent communication.// data.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
interface Post {
id: number;
title: string;
body: string;
}
@Injectable({
providedIn: 'root'
})
export class DataService {
private apiUrl = 'https://jsonplaceholder.typicode.com/posts';
constructor(private http: HttpClient) { }
getPosts(): Observable {
return this.http.get(this.apiUrl);
}
}
// app.component.ts
import { Component, OnInit } from '@angular/core';
import { DataService } from './data.service';
interface Post {
id: number;
title: string;
body: string;
}
@Component({
selector: 'app-root',
template: `Posts from API
{{ post.title }}
{{ post.body | slice:0:50 }}...
`
})
export class AppComponent implements OnInit {
posts: Post[] = [];
constructor(private dataService: DataService) { }
ngOnInit() {
this.dataService.getPosts().subscribe(data => {
this.posts = data;
});
}
}
DataService) with HttpClient to fetch data from a remote API. The component injects the service and subscribes to the observable returned by getPosts() to update its local state.HttpClientModule for HTTP requests, FormsModule for forms) into their AppModule or feature modules. This leads to runtime errors like 'Can't bind to 'ngModel' since it isn't a known property'.imports array of your @NgModule.ChangeDetectorRef.detectChanges() or wrap the operation in NgZone.run().ngOnInit vs. the constructor, or forgetting to clean up subscriptions in ngOnDestroy.ngOnInit for initialization logic that relies on inputs or services. Always unsubscribe from observables in ngOnDestroy to prevent memory leaks.GreetingComponent. It should display a personalized greeting like "Hello, [Your Name]!" where "Your Name" is passed as an @Input() from its parent component.GreetingComponent to include a button. When clicked, this button should emit an event (using @Output()) to its parent, indicating that the greeting was acknowledged. The parent component should then display a message like "Greeting acknowledged!".https://jsonplaceholder.typicode.com/users). Display each user's name and email in a list. Use an Angular service to handle the API call and a component to display the data.Angular CLI is the official command-line tool for creating, running, testing, and building Angular applications. Instead of manually configuring project folders, TypeScript settings, development servers, and build pipelines, developers use the CLI to automate these tasks. In real projects, teams rely on Angular CLI to standardize setup across machines, reduce configuration errors, and speed up development. It is commonly used in enterprise frontend teams where consistency, code generation, and reliable builds are important. Before installing Angular CLI, you need Node.js and npm because the CLI is distributed as an npm package. The most common setup path is installing Node.js first, verifying your environment, then installing the CLI globally so the ng command works anywhere on your system. Angular CLI also supports local project usage, which is useful when different projects require different Angular versions. Global installation gives convenience, while local installation improves version control. Understanding both approaches helps beginners work safely in real-world environments.
First, install Node.js LTS from the official Node.js website. This also installs npm. After installation, open a terminal and verify both tools using version commands. Next, install Angular CLI globally with npm using the -g flag. Global installation places the CLI in your system path so you can run ng from any directory. After installation, confirm success with ng version. If your system blocks the command, the issue is usually related to PATH configuration, permissions, or terminal restart requirements. On some systems, you may need to reopen the terminal after npm global installs. If permission errors appear on macOS or Linux, avoid unsafe sudo habits unless absolutely necessary and prefer correct npm directory configuration. Once installed, test the CLI by creating a new Angular app with ng new, then start the development server using ng serve.
# Basic environment check
node -v
npm -v# Basic global installation
npm install -g @angular/cli
ng version# Real-world setup: create and run a project
ng new company-portal
cd company-portal
ng serve --open# Advanced usage: use a specific CLI version without global install
npx @angular/cli@17 new versioned-app
cd versioned-app
npx ng serveThe first example checks whether Node.js and npm are ready. The second installs the Angular CLI globally. The third creates a real project and launches a local development server, usually available at http://localhost:4200. The advanced example uses npx to run a specific CLI version temporarily, which is useful in teams working with version-sensitive projects.
node -v and npm -v.ng command not found: Restart the terminal and verify npm global path settings.ng version.npx when version consistency matters.node -v and npm -v.ng version to confirm it works.starter-app and run it in the browser using ng serve --open.Set up a complete Angular development environment on your machine, create a project called employee-dashboard, and launch the default application successfully in the browser.
Install and test Angular CLI using both methods: global installation and npx. Compare when each approach is better for personal learning versus team-based projects.
Creating your first Angular app means setting up a complete frontend project using Angular’s official tooling and understanding the basic structure that Angular gives you out of the box. Angular exists to help developers build large web applications in a predictable way. Instead of manually wiring HTML, CSS, and JavaScript files together, Angular organizes code into reusable pieces called components. In real life, companies use Angular for admin panels, analytics dashboards, HR systems, banking portals, education platforms, and many other applications where maintainability matters. When you create your first app, you are not just generating files; you are starting a structured application with a build system, development server, testing setup, and a clear folder layout.
Before creating an app, you typically install Node.js, then install the Angular CLI globally with npm install -g @angular/cli. The CLI is Angular’s command-line tool for creating, running, and managing projects. The main sub-types involved in app creation are the development environment, the CLI commands, and the generated project structure. The environment includes Node.js, npm, and a code editor like VS Code. The commands include ng new to create a project, ng serve to run it locally, and ng generate for adding features later. The generated structure includes files like src/main.ts, which boots the app, and the root component files that display the first page.
First, verify Node.js is installed by running node -v and npm -v. Next, install the Angular CLI globally. Then create a new app using ng new first-angular-app. The CLI asks setup questions such as whether to enable routing and which stylesheet format to use. After creation, move into the project folder with cd first-angular-app. Start the development server using ng serve. Angular will compile the app and provide a local URL, usually http://localhost:4200. Open that in a browser to see the starter app. The app runs with live reload, so when you change a file and save it, the browser updates automatically.
The most important beginner files are the root component files. The TypeScript file defines behavior, the HTML file defines the view, and the CSS file controls styling. Editing these files is the fastest way to understand how Angular apps work.
Basic example: change the root template so the app displays a custom message.
My First Angular App
Angular is running successfully.
Real-world example: define data in the component and render it in the template.
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrl: './app.component.css'
})
export class AppComponent {
appName = 'Task Tracker';
version = '1.0.0';
}
{{ appName }}
Version: {{ version }}
Advanced usage: show a simple starter dashboard with reusable data.
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrl: './app.component.css'
})
export class AppComponent {
title = 'Employee Dashboard';
totalEmployees = 42;
status = 'Online';
}
{{ title }}
Employees: {{ totalEmployees }}
System Status: {{ status }}
cd your-app-name before ng serve.app.component.html and app.component.ts.app.component.ts and show both values in the template.Build a small Angular welcome page for a fictional company dashboard that shows the app title, current version, and a short description in the root component.
Create a first app that displays three pieces of dynamic information from the component class and style the page so it looks like a simple product landing screen.
src/ folder: This is where all your application's source code resides. It's the heart of your Angular project.app/ folder: Contains the main application logic, including components, services, modules, and routing configurations.assets/ folder: Stores static assets like images, icons, and other files that are directly served to the browser.environments/ folder: Holds environment-specific configuration files (e.g., development, production), allowing you to define different settings for various deployment targets.index.html: The main HTML file that serves as the entry point for your application.main.ts: The main TypeScript file that bootstraps your Angular application.styles.css (or .scss/.less): Global styles for your application.angular.json: Configuration file for the Angular CLI, defining project-specific settings, build options, and more.package.json: Defines project metadata, dependencies, and scripts, similar to other Node.js projects.tsconfig.json: TypeScript configuration file.ng new my-app, the Angular CLI sets up a predefined directory structure. Let's walk through the most important parts:ng new my-app, a folder named my-app is created. This is your project's root directory.angular.json (CLI configuration), package.json (dependencies and scripts), tsconfig.json (TypeScript compiler options), .gitignore (Git ignore rules), and others. These files configure the project's build process, dependencies, and development environment.node_modules/: This directory is automatically created and managed by npm (Node Package Manager) or Yarn. It contains all the third-party libraries and packages your project depends on.src/ Directory: This is where you'll spend most of your time. It contains all the application-specific code and assets.app/: This sub-directory contains the core logic of your application. When you generate components, services, or modules, they typically land here. For instance, app.component.ts, app.module.ts, and app-routing.module.ts are found here.assets/: This folder is for static assets. If you have images, JSON data, or other files that don't need to be compiled but should be available to your application, place them here.environments/: This folder contains environment-specific configuration files. By default, you'll see environment.ts (development configuration) and environment.prod.ts (production configuration). You can add more environments as needed.favicon.ico: The icon displayed in the browser tab.index.html: The single-page application's entry point. Angular injects your application's root component into this file.main.ts: This file bootstraps (starts) your Angular application. It imports the root module (usually AppModule) and tells Angular to load it.polyfills.ts: Imports various polyfills needed for older browsers to support modern JavaScript features.styles.css (or .scss/.less): Contains global styles that apply to your entire application.test.ts: Configures the testing environment.src/app structuremy-app/
├── src/
│ ├── app/
│ │ ├── app-routing.module.ts
│ │ ├── app.component.css
│ │ ├── app.component.html
│ │ ├── app.component.spec.ts
│ │ ├── app.component.ts
│ │ └── app.module.ts
│ ├── assets/
│ │ └── .gitkeep
│ ├── environments/
│ │ ├── environment.prod.ts
│ │ └── environment.ts
│ ├── favicon.ico
│ ├── index.html
│ ├── main.ts
│ ├── polyfills.ts
│ ├── styles.css
│ └── test.ts
├── angular.json
├── package.json
├── tsconfig.json
└── ... (other config files, node_modules)products feature. We'd typically generate a module and components within it.# Generate a 'products' module
ng generate module products --routing
# Generate a 'product-list' component inside the products module
ng generate component products/product-list
# Generate a 'product-detail' component inside the products module
ng generate component products/product-detailmy-app/
├── src/
│ ├── app/
│ │ ├── products/
│ │ │ ├── product-detail/
│ │ │ │ ├── product-detail.component.css
│ │ │ │ ├── product-detail.component.html
│ │ │ │ ├── product-detail.component.spec.ts
│ │ │ │ └── product-detail.component.ts
│ │ │ ├── product-list/
│ │ │ │ ├── product-list.component.css
│ │ │ │ ├── product-list.component.html
│ │ │ │ ├── product-list.component.spec.ts
│ │ │ │ └── product-list.component.ts
│ │ │ ├── products-routing.module.ts
│ │ │ └── products.module.ts
│ │ └── ... (other app files)
│ └── ... (other src files)shared module.# Generate a 'shared' module
ng generate module shared
# Generate a 'button' component inside the shared module
ng generate component shared/buttonmy-app/
├── src/
│ ├── app/
│ │ ├── shared/
│ │ │ ├── button/
│ │ │ │ ├── button.component.css
│ │ │ │ ├── button.component.html
│ │ │ │ ├── button.component.spec.ts
│ │ │ │ └── button.component.ts
│ │ │ └── shared.module.ts
│ │ └── ... (other app files)
│ └── ... (other src files)shared.module.ts can then be exported and imported into other feature modules like products.module.ts.app/ folder: Beginners often dump all components, services, and modules directly into src/app. This leads to a messy and unmaintainable structure as the application grows. products, users, auth) within the app/ folder. Use ng generate module feature-name.src/assets/ folder or a subfolder within it. You can then reference them in your templates relatively, e.g., ![]()
.angular.json or main.ts without knowing their purpose can break the build or application bootstrapping.src/app directory. Consult Angular documentation before making changes to root configuration files.AuthModule, ProductsModule, DashboardModule). This promotes clarity, lazy loading, and easier maintenance.SharedModule to house common components, directives, and pipes that are used across multiple feature modules. Import this SharedModule into other feature modules, but never import it into your AppModule directly to avoid circular dependencies and unnecessary bundling.CoreModule. Import the CoreModule only once in your AppModule.AppModule Lean: The AppModule should primarily be responsible for bootstrapping the application and importing feature modules. Avoid cluttering it with too many declarations, imports, or providers.environments/ for different configurations (API endpoints, feature flags) for development, staging, and production. Use ng serve --configuration=production or ng build --configuration=production.index.html, main.ts, and app.module.ts.src/app directory.AuthModule (for login/registration), ProductsModule (for listing and viewing products), and a CartModule (for managing the shopping cart). Ensure each module has at least one component, and the AuthModule includes a login component.SharedModule and move any common UI elements (e.g., a generic button component or a loading spinner component) from your feature modules into this SharedModule. Ensure the feature modules correctly import and use these shared components. Additionally, create a CoreModule and define a simple LoggerService as a singleton within it, then provide it in your AppModule. Angular Modules, commonly written as NgModule, are a way to organize related parts of an Angular application into cohesive blocks. They exist to help developers group components, directives, pipes, and services so the app remains structured as it grows. In real-life enterprise projects, modules are used to separate areas such as authentication, user management, reporting, and shared UI utilities. Although newer Angular versions emphasize standalone components, Angular Modules are still important because many existing codebases depend on them, and understanding them helps you maintain legacy and enterprise applications confidently.
A module defines what belongs together and what should be exposed to other parts of the app. The most common kinds are the root module, feature modules, shared modules, and core modules. The root module usually bootstraps the application. Feature modules hold functionality for one business area, such as orders or products. Shared modules contain reusable components, directives, and pipes that many features need. Core modules usually hold singleton services and app-wide infrastructure. Angular also uses imported modules such as BrowserModule, CommonModule, FormsModule, and router-related modules to enable platform and feature capabilities.
To create a module, Angular uses the @NgModule decorator. Inside it, several properties define behavior. declarations lists the components, directives, and pipes that belong to the module. imports brings in other modules whose exported members you want to use. exports makes selected declarations available to other modules. providers registers services, though many modern services use providedIn: 'root' instead. bootstrap is typically used only in the root module to start the main component.
A beginner should remember one practical rule: declare a component in exactly one module, import modules to use their exported features, and export only what other modules truly need. In feature modules, use CommonModule instead of BrowserModule.
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule],
bootstrap: [AppComponent]
})
export class AppModule {}import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ProductListComponent } from './product-list.component';
@NgModule({
declarations: [ProductListComponent],
imports: [CommonModule],
exports: [ProductListComponent]
})
export class ProductsModule {}import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { SearchBoxComponent } from './search-box.component';
import { HighlightPipe } from './highlight.pipe';
@NgModule({
declarations: [SearchBoxComponent, HighlightPipe],
imports: [CommonModule, FormsModule],
exports: [SearchBoxComponent, HighlightPipe]
})
export class SharedModule {}The first example shows the root module. The second shows a feature module for products. The third is a shared module that exposes reusable UI pieces and pipes to multiple feature modules.
BrowserModule in feature modules: use CommonModule instead.exports.providedIn patterns consistently.UsersModule with one component named UserListComponent and import CommonModule.SharedModule that declares and exports a custom pipe and one reusable button component.Organize a small shop application into modules: ProductsModule, CartModule, and SharedModule. Export reusable UI parts from the shared module and import them where necessary.
Design a modular structure for a school management app with areas for students, teachers, attendance, and reports. Decide which pieces belong in feature, shared, and core modules, and explain why.
Angular components are the main building blocks of an Angular application. A component controls a part of the screen called a view, and it combines three key pieces: logic written in TypeScript, a template written in HTML, and styling written in CSS or similar stylesheet formats. Components exist because large applications are easier to build when the user interface is broken into small, reusable, focused pieces. In real-world development, components are used for navigation bars, product cards, user profiles, dashboards, forms, tables, dialogs, and many other interface elements.
In Angular, a component is usually created with a decorator called @Component. This decorator tells Angular how the component should behave, what selector it uses, which template belongs to it, and which styles are applied. Components can be root components or child components. The root component is the top-level entry point of the app, while child components are nested inside other components to keep code organized and reusable. Another important idea is component communication. Parent components can pass data to children with inputs, and children can send events back with outputs. This design makes Angular apps modular and easier to maintain.
To understand a component, start with its syntax. First, import Component from Angular core. Next, add the @Component decorator. Inside it, define a selector, which is the custom HTML tag used to display the component. Then define a template or templateUrl for the HTML view, and optionally styles or styleUrl for appearance. Finally, create a TypeScript class that stores the data and methods used by the template.
A component works by binding class data to the template. For example, a property like title can be shown with interpolation using {{ title }}. A method can be called from a button click using event binding. When the user interacts with the interface, Angular updates the view automatically. This makes components ideal for dynamic applications.
import { Component } from '@angular/core';
@Component({
selector: 'app-welcome',
template: '{{ message }}
'
})
export class WelcomeComponent {
message = 'Welcome to Angular';
}import { Component } from '@angular/core';
@Component({
selector: 'app-user-card',
template: '
{{ name }}
Role: {{ role }}
Active: {{ isActive }}
'
})
export class UserCardComponent {
name = 'Aisha';
role = 'Frontend Developer';
isActive = true;
toggleStatus() {
this.isActive = !this.isActive;
}
}import { Component, Input } from '@angular/core';
@Component({
selector: 'app-product-item',
template: '
{{ productName }}
Price: {{ price }}
Category: {{ category }}
'
})
export class ProductItemComponent {
@Input() productName = '';
@Input() price = 0;
@Input() category = '';
} [productName]="'Laptop'"
[price]="1200"
[category]="'Electronics'">
UserCardComponent are easier to understand than vague names.app-greeting that displays your name and a welcome message.Build a simple profile card component that shows a user photo placeholder, name, job title, and a button that toggles whether the user is online or offline.
Create a parent component that displays three reusable child card components, each receiving different input data such as title, description, and price.
Components are the building blocks of Angular applications. A component controls a portion of the screen, combines HTML, TypeScript, and CSS, and lets you create reusable interface pieces such as navigation bars, product cards, dashboards, forms, and dialogs. Angular uses components so large applications can be split into smaller, maintainable units. In real-world projects, teams create components for nearly everything: headers, user profiles, tables, search bars, and feature pages. Each component usually has a class for logic, a template for markup, and optional styles for presentation.
When creating components in Angular, you commonly work with two forms: standalone components and module-based components. Standalone components are the modern Angular approach and can be used without declaring them inside an NgModule. Module-based components belong to a module and are still common in older or enterprise codebases. No matter which style you use, every component needs a selector, template or templateUrl, and often styleUrls. Angular then renders the component wherever its selector appears in another template.
To create a component, developers usually use the Angular CLI. The command ng generate component component-name or the short form ng g c component-name creates the needed files automatically. These typically include a TypeScript class file, HTML template, CSS file, and test file. Inside the TypeScript file, the @Component decorator defines metadata such as the selector and template. The selector is the custom HTML tag used to place the component in another template. The class holds data and methods used by the template.
For beginners, the workflow is simple: generate the component, define the selector, write the template, add styles if needed, and then use the selector inside a parent component. If using standalone components, import the child component into the parent component’s imports array. If using module-based architecture, declare the component in an NgModule. This connection is what allows Angular to recognize and render it correctly.
import { Component } from '@angular/core';
@Component({
selector: 'app-hello',
standalone: true,
template: `Hello Angular
My first component works.
`,
styles: [`h1 { color: crimson; }`]
})
export class HelloComponent {}This example creates a simple standalone component with inline template and styles.
import { Component } from '@angular/core';
@Component({
selector: 'app-user-card',
standalone: true,
templateUrl: './user-card.component.html',
styleUrls: ['./user-card.component.css']
})
export class UserCardComponent {
name = 'Aisha Khan';
role = 'Frontend Developer';
location = 'Lahore';
}
{{ name }}
{{ role }}
{{ location }}
This pattern is common in business apps where UI sections are separated into dedicated files.
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-task-list',
standalone: true,
imports: [CommonModule],
template: `Tasks
{{ task }}
`
})
export class TaskListComponent {
tasks = ['Design UI', 'Build component', 'Write tests'];
}This version shows a component using imported Angular features and dynamic rendering from class data.
app-user-card, you must use exactly that tag in the parent template.templateUrl or styleUrls path prevents the component from compiling correctly.product-card or login-form.app-welcome that displays a heading and a short message.app-product-card that shows a product name, price, and category using class properties.*ngFor.Build a small profile dashboard using three components: a header component, a user card component, and a task list component. Render all three inside a parent page component to practice component creation and composition.
Create a reusable employee card component and display multiple employee cards on a page by combining component creation with array-based rendering from a parent component.
Angular templates define what users see in the browser and how data from a component is displayed. A template is usually written in HTML, but Angular extends HTML with special syntax for binding values, reacting to events, conditionally showing content, and repeating UI blocks. Interpolation is one of the simplest and most important features inside templates. It uses double curly braces like {{ value }} to insert component data into the view. This exists so developers can connect application logic to the user interface without manually querying the DOM and setting text by hand. In real projects, interpolation is used everywhere: showing usernames in dashboards, totals in shopping carts, dates in booking apps, and statuses in admin panels. Templates are declarative, which means you describe what the UI should display based on current data, and Angular handles updating the DOM when values change. Angular templates commonly work with plain properties, string expressions, numbers, booleans, method results, and safe object access. Interpolation is best for rendering text content and attribute-like values that Angular allows, while property binding is better when interacting directly with DOM properties. Understanding this distinction is essential for writing clean Angular views.
Start with a component class that contains data. For example, define a property such as title = 'Inventory App'. In the component template, place {{ title }} where you want the value to appear. Angular reads the expression, evaluates it against the component instance, and renders the result. You can also combine static text and dynamic text, such as Welcome, {{ userName }}!. Interpolation supports simple expressions like math, string concatenation, ternary checks, and method calls, but heavy logic should stay in the component for readability and performance. You may also access nested object values like {{ product.name }}. If an object may be undefined, use safe navigation like {{ product?.name }} to avoid errors during rendering. Angular automatically updates interpolated values when component data changes, so if a property is updated after an API call or user action, the view refreshes without manual DOM manipulation.
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
{{ title }}
Welcome, {{ userName }}.
2 + 3 = {{ 2 + 3 }}
`
})
export class AppComponent {
title = 'Angular Demo';
userName = 'Aisha';
}import { Component } from '@angular/core';
@Component({
selector: 'app-order-summary',
template: `
Order #{{ order.id }}
Customer: {{ order.customerName }}
Total: ${{ order.total }}
Status: {{ order.isPaid ? 'Paid' : 'Pending' }}
`
})
export class OrderSummaryComponent {
order = { id: 1045, customerName: 'Jordan Lee', total: 249.99, isPaid: true };
}import { Component } from '@angular/core';
@Component({
selector: 'app-profile-card',
template: `
{{ getDisplayName() }}
Email: {{ user?.email || 'Not available' }}
Plan: {{ user?.plan?.toUpperCase() }}
`
})
export class ProfileCardComponent {
user = { firstName: 'Nina', lastName: 'Patel', email: '[email protected]', plan: 'pro' };
getDisplayName(): string {
return `${this.user.firstName} ${this.user.lastName}`;
}
}user.name before data loads can fail. Use user?.name when needed.In Stock or Out of Stock based on a boolean property.?..Build a simple account summary card that displays a customer’s full name, membership level, reward points, and current account status entirely with Angular interpolation.
Create a dashboard header that shows a greeting based on time of day, the logged-in user’s name, and a fallback message when user data has not loaded yet, while keeping template expressions clean and readable.
One-way data binding in Angular is the process of sending data in a single direction, usually from a component class to the template, or from the template into an event handler. It exists to keep user interfaces clear, predictable, and easy to debug. In real applications, one-way binding is used when you want to display a product name, show a logged-in user, bind an image source, set element attributes, apply CSS classes, or react to a button click. Angular supports several one-way patterns: interpolation with {{ }} for text output, property binding with [property] for DOM properties, attribute binding with [attr.name], class binding with [class.name], style binding with [style.name], and event binding with (event) for sending user actions back to the component. These are grouped under one-way binding because data or actions move in one direction at a time rather than syncing both sides automatically.
Interpolation is the simplest type. It inserts component values into template text, such as a title or price. Property binding is used when the target is a DOM property like src, disabled, or value. Event binding listens for actions like clicks, input, and change events, then calls component methods. Together, these tools let you build dynamic interfaces while keeping logic inside the component. This separation is important in enterprise Angular projects because it improves readability, testing, and maintenance.
Start with a component class that stores data as properties. Example: appName = 'Inventory Portal';. In the HTML template, use interpolation: {{ appName }} to display it. For DOM properties, wrap the property name in square brackets, such as [src]="logoUrl" or [disabled]="isSaving". For events, use parentheses like (click)="save()". Angular evaluates the expression inside the quotes in the context of the component. If the component value changes, Angular updates the template output automatically. This means you should store view-related state in the component and let the template read from it. For conditional styling, use class and style binding. Example: [class.active]="isActive" or [style.color]="textColor".
// basic example
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
{{ title }}
`
})
export class AppComponent {
title = 'Angular Dashboard';
logoUrl = 'assets/angular.png';
}// real-world example
import { Component } from '@angular/core';
@Component({
selector: 'product-card',
template: `
{{ productName }}
Price: {{ price }}
{{ stockMessage }}
`
})
export class ProductCardComponent {
productName = 'Mechanical Keyboard';
price = '$89';
outOfStock = false;
stockMessage = 'Available now';
addToCart() {
console.log('Added to cart');
}
}// advanced usage
import { Component } from '@angular/core';
@Component({
selector: 'status-panel',
template: `
{{ statusLabel }}
`
})
export class StatusPanelComponent {
isOnline = true;
get statusLabel() {
return this.isOnline ? 'System Online' : 'System Offline';
}
toggleStatus() {
this.isOnline = !this.isOnline;
}
}[src] instead of src="{{ imageUrl }}" when binding properties.[disabled]="isLoading".isLoading and userName.isSubmitted is true.isError property.Build a simple user profile card that shows a name, role, profile image, online status color, and a button that logs a message when clicked.
Create a server health widget that displays different labels, colors, and button states using interpolation, property binding, class binding, style binding, and event binding together.
Current Value: {{ myData }}
`// app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `Basic Two-Way Binding
Hello, {{ userName }}!
`,
styleUrls: ['./app.component.css']
})
export class AppComponent {
userName: string = 'Guest';
}
// app.module.ts (ensure FormsModule is imported)
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms'; // <-- Import FormsModule
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
FormsModule // <-- Add to imports array
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Real-World Example: User Profile Editor
This example simulates a user profile editor where changes are immediately reflected.
// user-profile.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-user-profile',
template: `User Profile Editor
Live Preview:
Name: {{ user.name }}
Email: {{ user.email }}
Bio: {{ user.bio }}
`,
styleUrls: ['./user-profile.component.css']
})
export class UserProfileComponent {
user = {
name: 'Jane Doe',
email: '[email protected]',
bio: 'Frontend developer passionate about Angular.'
};
}
// app.module.ts (assuming UserProfileComponent is declared and FormsModule is imported)
// ... (same as basic example, just ensure UserProfileComponent is declared)
//
Advanced Usage: Select Dropdown with ngModel
This demonstrates two-way binding with a select element to manage selected options.
// product-selector.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-product-selector',
template: `Product Selector
You selected: {{ selectedProduct ? selectedProduct.name : 'None' }} (ID: {{ selectedProduct ? selectedProduct.id : 'N/A' }})
`,
styleUrls: ['./product-selector.component.css']
})
export class ProductSelectorComponent {
products = [
{ id: 1, name: 'Laptop' },
{ id: 2, name: 'Keyboard' },
{ id: 3, name: 'Mouse' }
];
selectedProduct: any;
constructor() {
this.selectedProduct = this.products[0]; // Set initial selection
}
}
// app.module.ts (assuming ProductSelectorComponent is declared and FormsModule is imported)
// ... (same as basic example, just ensure ProductSelectorComponent is declared)
//
Common Mistakes
- Forgetting to import `FormsModule`: This is the most common mistake. If you use `[(ngModel)]` without importing `FormsModule` in your module, Angular will throw an error like "Can't bind to 'ngModel' since it isn't a known property of 'input'".
Fix: Add `import { FormsModule } from '@angular/forms';` and include `FormsModule` in the `imports` array of your `NgModule`.
- Using `ngModel` on non-form elements: `ngModel` is designed for form controls like ``, `
[ ] around the target DOM property, while event binding is denoted by parentheses ( ) around the target DOM event.[targetProperty]="sourceExpression". The targetProperty is a property of the DOM element (e.g., src for an ![]()
tag, value for an tag, or disabled for a button). The sourceExpression is typically a property or method from your component's TypeScript class. Angular evaluates this expression and binds its result to the target property.src attribute of an image:![]()
imageUrl would be a property in your component's class.(targetEvent)="handlerExpression()". The targetEvent is a standard DOM event (e.g., click, mouseover, submit). The handlerExpression() is a method defined in your component's TypeScript class that will be executed when the event fires. You can also pass the event object $event to your handler method, which contains information about the event.onButtonClick() would be a method in your component's class.// app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
Basic Property and Event Binding
![]()
Image visibility: {{ showImage ? 'Visible' : 'Hidden' }}
`,
styles: [`img { width: 200px; border: 1px solid #ccc; }`]
})
export class AppComponent {
imageUrl: string = 'https://angular.io/assets/images/logos/angular/angular.svg';
imageAlt: string = 'Angular Logo';
showImage: boolean = true;
toggleImageVisibility(): void {
this.showImage = !this.showImage;
}
}
[(ngModel)]) is a combination of property and event binding, understanding its underlying mechanism helps. Here, we'll simulate the components of two-way binding.// app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
Real-world Example: Form Input
id="userNameInput"
type="text"
[value]="userName"
(input)="onNameChange($event)">
Hello, {{ userName }}!
`,
styles: [`input { padding: 8px; margin-bottom: 10px; } button { padding: 10px 15px; }`]
})
export class AppComponent {
userName: string = '';
onNameChange(event: Event): void {
this.userName = (event.target as HTMLInputElement).value;
}
greetUser(): void {
alert(`Greetings, ${this.userName}!`);
}
}
// child.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-child',
template: `
Child Component
Message from parent: {{ messageFromParent }}
`
})
export class ChildComponent {
@Input() messageFromParent: string = ''; // Property binding target
@Output() childEvent = new EventEmitter(); // Event binding source
sendMessageToParent(): void {
this.childEvent.emit('Hello from child!');
}
}
// app.component.ts (Parent Component)
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
Advanced Usage: Custom Component Binding
Parent message: {{ parentMessage }}
Message from child: {{ messageFromChild }}
[messageFromParent]="parentMessage"
(childEvent)="handleChildMessage($event)">
`})
export class AppComponent {
parentMessage: string = 'Data for child component';
messageFromChild: string = 'No message yet';
handleChildMessage(message: string): void {
this.messageFromChild = message;
}
}
src="imageUrl" instead of [src]="imageUrl" for property binding, or click="myMethod()" instead of (click)="myMethod()" for event binding. Without the correct syntax, Angular treats them as literal string attributes/inline JavaScript, not as bindings to component properties/methods.$event to the handler, or assuming $event always has the same structure for all events. Remember to cast $event.target to the correct HTML element type (e.g., (event.target as HTMLInputElement).value) to access its specific properties.[ ] when you want to set a DOM property based on data from your component. Avoid string interpolation {{ }} for properties that can be directly bound (e.g., [src] is better than src="{{ imageUrl }}").( ) for all user interactions or custom component events. Keep event handler methods concise and focused on updating component state or emitting further events.$event Wisely: Pass $event to your handler only when you genuinely need information from the DOM event object (e.g., event.target.value, event.keyCode). Avoid unnecessary passing if the handler doesn't use it.@HostListener decorator in your component's TypeScript class, especially for listening to events on the host element itself.innerText to a component property. Add another button that, when clicked, changes the text of the first button.src and alt properties of the image to cycle through a predefined array of image URLs and descriptions.![]()
) whose src and alt attributes are bound to component properties.) and description () using interpolation.
3. Include an 'Add to Cart' button whose disabled property is bound to a boolean component property (e.g., isProductOutOfStock).
4. When the 'Add to Cart' button is clicked, trigger an event handler that logs a message to the console indicating the product has been added.
[class.out-of-stock]="isProductOutOfStock" on the card to visually indicate when the product is out of stock. Directives are one of Angular’s most important building blocks. A directive tells Angular how to change the appearance, behavior, or structure of elements in the DOM. In real applications, directives are used everywhere: showing or hiding alerts, repeating rows in a table, applying validation styles to forms, disabling buttons, and building reusable UI behavior such as tooltips or permission-based visibility. Angular includes built-in directives and also allows you to create custom ones for your own projects.
There are three major categories to understand. Component directives are directives with templates; every Angular component is technically a directive with its own view. Structural directives change the layout by adding or removing elements, such as *ngIf, *ngFor, and *ngSwitchCase. Attribute directives change the look or behavior of an existing element without recreating the structure, such as ngClass, ngStyle, and custom highlight directives.
A directive is applied in a template using normal HTML-like syntax. Structural directives usually use an asterisk, for example *ngIf="isLoggedIn". The asterisk is shorthand that Angular expands into template instructions behind the scenes. Attribute directives are added like regular attributes, for example [ngClass]="{active: isActive}".
To create a custom attribute directive, define a class with the @Directive decorator, provide a selector, inject the element or renderer if needed, and react to events or inputs. A custom directive becomes useful when the same UI behavior appears in multiple places. This keeps templates cleaner and improves reuse.
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
Products
No products found
- {{ product }}
`
})
export class AppComponent {
products = ['Laptop', 'Mouse', 'Keyboard'];
}import { Component } from '@angular/core';
@Component({
selector: 'app-status',
template: `
{{ message }}
`
})
export class StatusComponent {
isSaving = false;
saved = true;
hasError = false;
message = 'Changes saved successfully.';
}import { Directive, ElementRef, HostListener, Input } from '@angular/core';
@Directive({
selector: '[appHighlight]'
})
export class HighlightDirective {
@Input() appHighlight = 'yellow';
constructor(private el: ElementRef) {}
@HostListener('mouseenter') onEnter() {
this.el.nativeElement.style.backgroundColor = this.appHighlight;
}
@HostListener('mouseleave') onLeave() {
this.el.nativeElement.style.backgroundColor = null;
}
}
// template
// Hover over me
or another element.[appHighlight] for readability.*ngIf to show a login message only when a Boolean variable is true.*ngFor to display a list of five course names in an unordered list.[ngClass] to switch text color based on whether a score is passing or failing.Build a small task dashboard that lists tasks with *ngFor, shows a “No tasks available” message with *ngIf, and uses [ngClass] to highlight completed tasks.
Create a custom directive that changes text color on hover and accepts the hover color as an input so different elements can reuse the same directive with different values.
Structural directives in Angular change the structure of the DOM by adding, removing, or repeating elements. They exist because user interfaces are rarely static. In real applications, you may need to show a login message only when a user is signed out, display a list of products from an API, or switch between loading, success, and error screens. Angular solves these needs with structural directives such as *ngIf, *ngFor, and [ngSwitch]. *ngIf conditionally renders content, *ngFor repeats a template for each item in a collection, and ngSwitch displays one matching view from multiple options. These directives are called structural because they transform the layout itself, not just styles or attributes. You will usually see them in templates for dashboards, shopping carts, navigation menus, report tables, notification panels, and role-based screens.
ngIf is used when content should appear only if a condition is true. It also supports else templates for fallback content. ngFor loops through arrays and exposes useful local values like index, first, last, even, and odd. ngSwitch is helpful when one variable can represent multiple UI states, such as plan type, user role, or current status. Together, these directives make Angular templates expressive and dynamic.
1. ngIf: Write *ngIf="condition" on an element. If the condition is true, Angular creates that element in the DOM. If false, Angular removes it. You can also pair it with else using an ng-template.
2. ngFor: Write *ngFor="let item of items" to repeat an element for every value in an array. You can capture extra values like index as i or let isFirst = first.
3. ngSwitch: Add [ngSwitch]="value" to a wrapper, then place child elements with *ngSwitchCase and *ngSwitchDefault. Angular renders the matching case.
Remember that only one structural directive should be placed directly on the same host element. If needed, wrap content in a container element or ng-container.
Welcome back!
Please sign in.
{{ i + 1 }}. {{ product.name }} - {{ product.price }}
Loading data...
Data loaded successfully.
Something went wrong.
Unknown status.
export class DashboardComponent {
isLoggedIn = true;
status = 'success';
products = [
{ name: 'Keyboard', price: 49 },
{ name: 'Mouse', price: 25 },
{ name: 'Monitor', price: 199 }
];
} 0; else emptyState">
Order #{{ order.id }} - Row odd: {{ isOdd }}
No orders found.
*ngIf and *ngFor cannot both sit on the same tag. Fix it by wrapping one inside ng-container or another element.ngIf removes elements from the DOM, unlike CSS display:none. Use it when the element should not exist at all.*ngSwitchDefault, unexpected values show nothing. Always include a fallback.ng-container when you need structure without adding extra DOM nodes.user, product, and status.ngFor thoughtfully for styling, numbering, or separators.isAdmin is true.*ngFor and show their index numbers.ngSwitch for pending, approved, and rejected.Build a simple task dashboard that shows a loading message with ngIf, lists tasks with ngFor, and switches task priority labels using ngSwitch.
Create a product catalog that shows an empty-state template when no items exist, displays products in a loop, and uses a switch block to show different badges for in-stock, low-stock, and out-of-stock.
Attribute directives are a powerful feature in Angular that allow you to change the appearance or behavior of a DOM element, component, or another directive. Unlike structural directives (like *ngIf or *ngFor) which manipulate the DOM by adding or removing elements, attribute directives simply modify existing elements. Think of them as special HTML attributes that you can create to add custom functionality or styling. They are incredibly useful for tasks such as dynamically styling elements, adding event listeners, or even building complex interactive behaviors without directly manipulating the DOM through JavaScript. In a real-world Angular application, you might use attribute directives to highlight elements on hover, enable or disable input fields based on certain conditions, or apply custom validation styles to form controls. They promote reusability and encapsulation, allowing you to define a behavior once and apply it across many different elements in your application, leading to cleaner and more maintainable code.
The core concept behind attribute directives is that they are applied to an element as if they were standard HTML attributes. When Angular encounters an attribute directive on an element, it instantiates the directive's class and gives it control over that element. This control includes access to the element's host DOM element, allowing the directive to read its properties, modify its styles, or attach event listeners. Attribute directives are ideal for scenarios where you want to augment an element's existing behavior rather than completely restructure the DOM. For example, a directive could change the background color of a table row based on data, or add a tooltip to an icon. They operate by listening to events on their host element or by reacting to changes in their input properties.
Creating an attribute directive involves a few key steps:
ng generate directive (e.g., ng g d highlight). This creates a directive file (e.g., highlight.directive.ts) and automatically declares it in the nearest module.Directive, ElementRef, and HostListener from @angular/core.@Directive decorator takes a configuration object, where the selector property is crucial. This is how Angular identifies where to apply your directive. By convention, attribute directive selectors are often camelCase and prefixed with the app name (e.g., [appHighlight]). The square brackets indicate it's an attribute.ElementRef: In the directive's constructor, inject ElementRef. This gives you direct access to the DOM element that the directive is placed on. While direct DOM manipulation is generally discouraged in Angular, ElementRef is necessary for attribute directives to modify the host element.Renderer2 (recommended for safer DOM manipulation) or ElementRef.nativeElement directly to modify the element's style, class, or other properties. You can also use @HostListener to react to events on the host element, and @Input to pass data into the directive.This text will be highlighted.
.This directive changes the background color of an element on mouse enter and resets it on mouse leave.
import { Directive, ElementRef, HostListener, Input } from '@angular/core';
@Directive({
selector: '[appHighlight]'
})
export class HighlightDirective {
@Input() defaultColor: string = 'transparent';
@Input('appHighlight') highlightColor: string = 'yellow'; // Alias for the directive name
constructor(private el: ElementRef) { }
@HostListener('mouseenter') onMouseEnter() {
this.highlight(this.highlightColor || 'yellow');
}
@HostListener('mouseleave') onMouseLeave() {
this.highlight(this.defaultColor);
}
private highlight(color: string) {
this.el.nativeElement.style.backgroundColor = color;
}
}
Usage in template:
Highlight me on hover!
Highlight with custom colors.
This directive disables a button and changes its appearance based on an input boolean, useful for form submissions.
import { Directive, ElementRef, Input, Renderer2, OnChanges, SimpleChanges } from '@angular/core';
@Directive({
selector: '[appDisableButton]'
})
export class DisableButtonDirective implements OnChanges {
@Input('appDisableButton') isDisabled: boolean = false;
constructor(private el: ElementRef, private renderer: Renderer2) { }
ngOnChanges(changes: SimpleChanges): void {
if (changes['isDisabled']) {
if (this.isDisabled) {
this.renderer.setAttribute(this.el.nativeElement, 'disabled', 'true');
this.renderer.setStyle(this.el.nativeElement, 'opacity', '0.6');
this.renderer.setStyle(this.el.nativeElement, 'cursor', 'not-allowed');
} else {
this.renderer.removeAttribute(this.el.nativeElement, 'disabled');
this.renderer.removeStyle(this.el.nativeElement, 'opacity');
this.renderer.removeStyle(this.el.nativeElement, 'cursor');
}
}
}
}
Usage in template:
isSubmitting: {{ isSubmitting }}
A directive that adds a simple tooltip on hover, demonstrating more complex DOM manipulation and event handling.
import { Directive, ElementRef, HostListener, Input, Renderer2, OnInit, OnDestroy } from '@angular/core';
@Directive({
selector: '[appTooltip]'
})
export class TooltipDirective implements OnInit, OnDestroy {
@Input('appTooltip') tooltipText: string = '';
private tooltipElement: HTMLElement | null = null;
constructor(private el: ElementRef, private renderer: Renderer2) { }
ngOnInit() {
// Initial setup if needed
}
@HostListener('mouseenter') onMouseEnter() {
if (!this.tooltipText) return;
this.createTooltip();
this.setPosition();
}
@HostListener('mouseleave') onMouseLeave() {
this.destroyTooltip();
}
private createTooltip() {
this.tooltipElement = this.renderer.createElement('div');
this.renderer.addClass(this.tooltipElement, 'app-tooltip');
this.renderer.appendChild(this.tooltipElement, this.renderer.createText(this.tooltipText));
this.renderer.appendChild(document.body, this.tooltipElement);
this.renderer.setStyle(this.tooltipElement, 'position', 'absolute');
this.renderer.setStyle(this.tooltipElement, 'background-color', 'black');
this.renderer.setStyle(this.tooltipElement, 'color', 'white');
this.renderer.setStyle(this.tooltipElement, 'padding', '5px 10px');
this.renderer.setStyle(this.tooltipElement, 'border-radius', '4px');
this.renderer.setStyle(this.tooltipElement, 'z-index', '1000');
this.renderer.setStyle(this.tooltipElement, 'font-size', '12px');
}
private setPosition() {
if (!this.tooltipElement) return;
const hostRect = this.el.nativeElement.getBoundingClientRect();
const tooltipRect = this.tooltipElement.getBoundingClientRect();
const top = hostRect.top - tooltipRect.height - 5; // 5px above
const left = hostRect.left + (hostRect.width - tooltipRect.width) / 2;
this.renderer.setStyle(this.tooltipElement, 'top', `${top + window.scrollY}px`);
this.renderer.setStyle(this.tooltipElement, 'left', `${left + window.scrollX}px`);
}
private destroyTooltip() {
if (this.tooltipElement) {
this.renderer.removeChild(document.body, this.tooltipElement);
this.tooltipElement = null;
}
}
ngOnDestroy() {
this.destroyTooltip();
}
}
Usage in template:
Info Icon
Renderer2:this.el.nativeElement.style.backgroundColor = 'red'; directly.Renderer2 for safer, platform-agnostic DOM manipulations. Example: this.renderer.setStyle(this.el.nativeElement, 'background-color', 'red');.declarations array of an @NgModule.declarations array of the module where it will be used. The Angular CLI (ng g d) typically does this automatically, but if you create it manually, this step is crucial.selector: 'appHighlight' instead of selector: '[appHighlight]' for an attribute directive.[] to indicate they are attributes. Element directives (components) use just the name.Renderer2 for DOM Manipulation: As mentioned, Renderer2 provides an abstraction layer over the DOM, making your directives more robust and compatible with different rendering environments.@Input() for Configuration: Allow your directives to be configurable by exposing properties via @Input(). This makes them more flexible and reusable.ngOnDestroy to prevent memory leaks.app, my) for your directive selectors to avoid naming collisions with standard HTML attributes or other libraries.appTextColor that takes a color string as an @Input() and changes the text color of the host element to that color. If no color is provided, default to 'blue'.appBorderHighlight that adds a 2px solid 'red' border to an element when the mouse enters it, and removes the border when the mouse leaves.appClickCounter that displays the number of times its host element has been clicked. The count should be appended as text to the element or displayed in a small badge next to it.Interactive List Item: Create an attribute directive named appListItemInteraction. Apply this directive to a list of items (e.g.,
.selected) to the item when it's clicked, and remove it if clicked again. This class should apply a distinct visual style (e.g., a border or different background).@Input() to allow a custom hover color to be passed into the directive.Focus Indicator Directive: Build an attribute directive appFocusIndicator that works on form input fields (,
Services in Angular are classes designed to hold reusable logic, such as fetching API data, managing authentication, storing shared state, or handling logging. They exist because components should mainly control the user interface, not contain all application logic. In real projects, services are used for tasks like calling backend APIs in e-commerce apps, sharing user session data in dashboards, or centralizing notifications in admin panels. Dependency Injection, often called DI, is the mechanism Angular uses to provide these services where needed. Instead of manually creating objects with new, Angular creates and supplies them automatically. This makes applications easier to test, easier to maintain, and less tightly coupled. Angular supports different provider scopes such as root-level singleton services and more limited component-level providers. A service is usually created with the @Injectable() decorator, which marks it as available for DI. When configured with providedIn: 'root', Angular creates one shared instance for the entire application. This is the most common pattern for shared business logic.
First, create a service using Angular CLI with ng generate service services/logger. Angular generates a class and adds @Injectable(). Second, place reusable logic inside the service. Third, inject the service into a component through the constructor. Angular sees the constructor parameter type and provides the correct instance automatically. Fourth, call service methods from the component. This keeps components clean and focused. You can also register providers in a component if you want a separate service instance for that component tree. Constructor injection is the standard syntax: constructor(private logger: LoggerService) {}. The private keyword creates a class property automatically. Angular DI can inject services into other services as well, allowing layered design such as an API service using an auth service.
Basic example:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class LoggerService {
log(message: string): void {
console.log('[LOG]: ' + message);
}
}
import { Component } from '@angular/core';
import { LoggerService } from './logger.service';
@Component({
selector: 'app-home',
template: ''
})
export class HomeComponent {
constructor(private logger: LoggerService) {}
save(): void {
this.logger.log('Save button clicked');
}
}Real-world example:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class ProductService {
constructor(private http: HttpClient) {}
getProducts(): Observable {
return this.http.get('https://api.example.com/products');
}
}
import { Component, OnInit } from '@angular/core';
import { ProductService } from './product.service';
@Component({
selector: 'app-products',
template: '- {{ product.name }}
'
})
export class ProductsComponent implements OnInit {
products: any[] = [];
constructor(private productService: ProductService) {}
ngOnInit(): void {
this.productService.getProducts().subscribe(data => {
this.products = data;
});
}
} Advanced usage:
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class AuthService {
getToken(): string {
return 'secure-token';
}
}
@Injectable({ providedIn: 'root' })
export class ApiService {
constructor(private authService: AuthService) {}
getHeaders(): object {
return { Authorization: 'Bearer ' + this.authService.getToken() };
}
}new LoggerService() bypasses Angular DI. Fix: inject services through the constructor.providedIn: 'root' or another provider registration may not be available. Fix: add proper provider metadata.providedIn: 'root' for app-wide singleton services.GreetingService with a method that returns a welcome message, then inject it into a component and display the message.MathService with methods for addition and subtraction, then call those methods from a component.UserService that returns a hardcoded list of users and render them in a component using *ngFor.Build a simple task manager where a TaskService stores a list of tasks and a component displays them with a button to add a new task.
Create two components that use the same root-provided service to share and update a common message, then observe how both components stay synchronized.
In Angular, a service is a class that contains reusable logic, shared data handling, or communication with external systems such as APIs. Services exist so that components do not become overloaded with business rules, data fetching, validation helpers, logging, authentication checks, or state management tasks. In real applications, services are used for product catalogs, user authentication, order processing, notifications, analytics, and many other responsibilities that must be shared across different parts of the application.
Angular services are usually plain TypeScript classes decorated with @Injectable(). This decorator allows Angular’s dependency injection system to create and supply service instances where needed. The most common service scope is providedIn: 'root', which creates a singleton available throughout the app. You can also provide services in a feature module or component when you want a smaller scope. Broadly, services can be grouped into data services, utility services, state-sharing services, and integration services. Data services fetch or update backend data. Utility services hold reusable helper methods. State-sharing services keep values that multiple components read or update. Integration services wrap browser APIs, storage, or third-party libraries.
To create a service, Angular developers often use the CLI command ng generate service services/product. This creates a service file and test file. Inside the service class, add @Injectable({ providedIn: 'root' }) so Angular can inject it globally. Then write methods that perform useful work, such as returning data, calling an API with HttpClient, or storing application state. To use the service, inject it through a component constructor like constructor(private productService: ProductService) {}. After that, call its methods inside lifecycle hooks such as ngOnInit() or in response to user actions. If the service depends on another Angular service, such as HttpClient, inject that into the service constructor as well. This layered structure keeps components focused on presentation while services manage logic and data flow.
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class GreetingService {
getMessage(): string {
return 'Welcome to Angular services!';
}
}import { Component, OnInit } from '@angular/core';
import { GreetingService } from './greeting.service';
@Component({
selector: 'app-home',
template: '{{ message }}
'
})
export class HomeComponent implements OnInit {
message = '';
constructor(private greetingService: GreetingService) {}
ngOnInit(): void {
this.message = this.greetingService.getMessage();
}
}import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class ProductService {
private apiUrl = 'https://api.example.com/products';
constructor(private http: HttpClient) {}
getProducts(): Observable {
return this.http.get(this.apiUrl);
}
} import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class CartService {
private items: string[] = [];
addItem(item: string): void {
this.items.push(item);
}
getItems(): string[] {
return [...this.items];
}
clearCart(): void {
this.items = [];
}
}HttpClient, inject it in the constructor and ensure the required Angular providers are configured.AuthService, UserService, and NotificationService.TimeService with a method that returns the current time as a string, then display it in a component.NoteService that stores an array of notes and provides methods to add and list notes.ThemeService with a method that returns either light or dark mode text and show the value in the template.Build a simple shopping cart feature using a CartService. The service should add items, list items, and clear the cart. Connect it to one component that adds products and another component that displays the cart contents.
Create a service that combines API data fetching and local in-memory caching so repeated requests can reuse previously loaded data when appropriate.
Angular applications often need data from a server, such as products, users, orders, or reports. Angular provides this through the HttpClient service, which makes it easy to send HTTP requests and receive responses in a structured way. In real projects, this is used for login systems, loading dashboard data, submitting forms, updating records, and deleting resources. Instead of placing network logic directly inside components, Angular encourages using services so API code stays reusable, testable, and easier to maintain.
The main HTTP methods you will use are GET for reading data, POST for creating new records, PUT or PATCH for updating data, and DELETE for removing data. Angular returns responses as Observables, which means data can arrive asynchronously. This is important because API calls take time, and the UI must wait for the server response. A common pattern is to build an API service, inject HttpClient, define typed methods, and subscribe to those methods in a component. This creates cleaner architecture and helps teams scale applications safely.
First, enable HTTP support in your Angular application by providing the HTTP client in your app configuration. Next, import and inject HttpClient into a service. Then define methods that return typed Observables, such as a list of users or a single product. In the component, call the service method and subscribe to it, or bind it with the async pipe where appropriate. You can also send headers, query parameters, and request bodies when needed.
A beginner-friendly flow is: create an interface for the response shape, create a service, inject HttpClient, write a method like getUsers(), call that method in ngOnInit(), and store the returned data in a component property. For form submission, create a method like addUser(user) with POST. For updates, use put() or patch(). For deletion, use delete().
export interface User {
id: number;
name: string;
email: string;
}
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable({ providedIn: 'root' })
export class UserService {
private http = inject(HttpClient);
private apiUrl = 'https://jsonplaceholder.typicode.com/users';
getUsers() {
return this.http.get(this.apiUrl);
}
} import { Component, OnInit, inject } from '@angular/core';
import { UserService, User } from './user.service';
@Component({
selector: 'app-users',
template: '- {{ user.name }}
'
})
export class UsersComponent implements OnInit {
private userService = inject(UserService);
users: User[] = [];
ngOnInit() {
this.userService.getUsers().subscribe(data => {
this.users = data;
});
}
}addUser(user: { name: string; email: string }) {
return this.http.post(this.apiUrl, user);
}
updateUser(id: number, user: Partial) {
return this.http.patch(`${this.apiUrl}/${id}`, user);
}
deleteUser(id: number) {
return this.http.delete(`${this.apiUrl}/${id}`);
} import { catchError, retry } from 'rxjs/operators';
import { throwError } from 'rxjs';
getUsers() {
return this.http.get(this.apiUrl).pipe(
retry(1),
catchError(error => {
console.error('API error:', error);
return throwError(() => new Error('Failed to load users'));
})
);
} The basic example fetches data. The real-world example creates, updates, and deletes records. The advanced example adds retry logic and error handling so the application behaves more reliably in production.
map, catchError, and retry when appropriate.POST and log the response to the console.DELETE request.Build a simple employee directory page that loads employees from an API, displays them in a list, and lets the user add a new employee through a form.
Extend the employee directory so it supports loading indicators, error messages, and an edit feature using PATCH for partial updates.
Observables are one of the most important ideas in Angular because they help applications react to data over time instead of only handling one value at a time. In real projects, data rarely arrives all at once. A user types into a search box, an HTTP request returns later, a button emits click events, or a stream of live updates changes the UI continuously. Observables provide a clean way to model these asynchronous data flows. RxJS, short for Reactive Extensions for JavaScript, is the library Angular uses for this pattern. Angular relies on RxJS heavily in features such as HTTP requests, form value changes, route parameters, and event streams.
An Observable can emit zero, one, or many values, and it can finish or fail with an error. This makes it more flexible than a Promise, which resolves only once. Important related concepts include Observer, which receives values; Subscription, which starts execution and lets you stop listening; and Operators, which transform, filter, combine, or control streams. Common stream sources include of() for fixed values, from() for arrays or promises, interval() for repeated emissions, and Angular services such as HttpClient. Observables can be cold, meaning each subscription starts a new producer, or hot, meaning values are shared across subscribers. Beginners do not need to master all reactive theory immediately, but they should understand that Observables represent ongoing streams of data and are processed through operators using pipe().
To use an Observable, first create or receive one. Then subscribe to it so your code can react to emitted values. Inside subscribe(), you usually handle next values, possible error cases, and optional completion. In Angular, most stream transformations are done with pipe() and operators like map, filter, debounceTime, and switchMap. A simple mental model is: source stream -> operators -> subscription. If you manually subscribe inside a component, you often need to unsubscribe to avoid memory leaks, especially for long-lived streams like interval() or form changes. In templates, Angular's async pipe can manage subscriptions automatically, which is often safer.
import { of } from 'rxjs';
const numbers$ = of(1, 2, 3);
numbers$.subscribe(value => {
console.log('Value:', value);
});import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
@Component({
selector: 'app-search-box',
template: ''
})
export class SearchBoxComponent {
searchControl = new FormControl('');
ngOnInit() {
this.searchControl.valueChanges
.pipe(
debounceTime(300),
distinctUntilChanged()
)
.subscribe(value => {
console.log('Searching for:', value);
});
}
}import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { FormControl } from '@angular/forms';
import { switchMap, debounceTime, distinctUntilChanged } from 'rxjs/operators';
@Component({
selector: 'app-user-search',
template: ''
})
export class UserSearchComponent {
query = new FormControl('');
constructor(private http: HttpClient) {}
ngOnInit() {
this.query.valueChanges
.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(term => this.http.get('/api/users?search=' + term))
)
.subscribe(results => {
console.log(results);
});
}
}Forgetting to subscribe: an Observable usually does nothing until subscribed. Fix: call subscribe() or bind with the async pipe.
Not unsubscribing from long-lived streams: this can cause memory leaks. Fix: unsubscribe in ngOnDestroy or use the async pipe.
Using nested subscriptions: code becomes hard to maintain. Fix: use operators like switchMap or mergeMap.
Confusing Observables with Promises: Observables can emit many values and be cancelled. Fix: choose the right abstraction for the use case.
Use the async pipe in templates whenever possible.
Prefer operators in pipe() over deeply nested callback logic.
Name streams with a $ suffix, such as users$, to make code easier to read.
Handle errors explicitly with operators or subscription error callbacks.
Keep components thin and move stream logic into services when it grows.
Create an Observable using of() that emits three city names and log each one.
Build a small component with a FormControl and print input changes after a 500ms debounce.
Use interval() to emit a number every second and stop the subscription after five emissions.
Build a live product search input in Angular that listens to user typing, waits briefly with debounceTime, avoids duplicate searches with distinctUntilChanged, and sends the latest request with switchMap.
Create a component that combines two streams: one for a selected category and one for a search term, then updates displayed results whenever either value changes.
Routing in Angular is the feature that lets users move between different views without reloading the entire page. In a real application, this is how users go from a dashboard to a profile page, from a product list to product details, or from login to a protected area. Angular routing exists to create single-page application navigation that feels fast and organized. Instead of loading a new HTML file for every page, Angular swaps components inside a main layout based on the URL. This makes routing essential for enterprise apps where users expect bookmarkable URLs, browser back-button support, nested screens, and secure access control.
Angular routing is built around a few core ideas. A route maps a URL path such as /home or /products/:id to a component. Static routes point to fixed pages, parameterized routes pass dynamic values like IDs, redirect routes forward one path to another, wildcard routes catch unknown URLs, and child routes create nested layouts such as an admin section with its own menu. In modern Angular, routing is commonly configured with route arrays and either a dedicated routing module or application-level providers. The displayed component appears inside a <router-outlet>, which acts as a placeholder for route content.
First, create or confirm the components you want to navigate between, such as Home, About, and NotFound. Second, define a routes array. Each object usually includes path and component. Third, register the router in your application. In a standalone Angular app, use router providers in the application config. In older module-based apps, import the router module with forRoot(). Fourth, place <router-outlet> in the root template so Angular knows where to render matched components. Fifth, add links using routerLink instead of plain anchor tags when navigating inside the app. Sixth, use route parameters like :id for detail pages and read them from the activated route. Finally, add a wildcard route at the end to handle invalid URLs.
// Basic example: app.routes.ts
import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';
export const routes: Routes = [
{ path: '', redirectTo: 'home', pathMatch: 'full' },
{ path: 'home', component: HomeComponent },
{ path: 'about', component: AboutComponent }
];// App template
nav>
Home
About
// Real-world example with parameter and wildcard
import { Routes } from '@angular/router';
import { ProductListComponent } from './products/product-list.component';
import { ProductDetailComponent } from './products/product-detail.component';
import { NotFoundComponent } from './shared/not-found.component';
export const routes: Routes = [
{ path: 'products', component: ProductListComponent },
{ path: 'products/:id', component: ProductDetailComponent },
{ path: '**', component: NotFoundComponent }
];// Advanced usage: child routes
import { Routes } from '@angular/router';
import { AdminLayoutComponent } from './admin/admin-layout.component';
import { UsersComponent } from './admin/users.component';
import { SettingsComponent } from './admin/settings.component';
export const routes: Routes = [
{
path: 'admin',
component: AdminLayoutComponent,
children: [
{ path: 'users', component: UsersComponent },
{ path: 'settings', component: SettingsComponent }
]
}
];router-outlet: routes will match, but nothing appears. Add the outlet to the main template.href for internal navigation: this can reload the page. Use routerLink instead.** last.pathMatch: 'full' on empty redirects: redirects may behave unexpectedly. Add it for the default route./orders and /orders/15./home./users/:id and display the ID in the target component.Build a small company portal with routes for Dashboard, Employees, Employee Details, and a Not Found page. Add navigation links and make Employee Details read an ID from the URL.
Create an Admin area with child routes for Users and Reports, then add a default child redirect so visiting /admin automatically opens /admin/users.
Angular routing is the feature that lets users move between views without reloading the whole page. In real applications, this is how users open a dashboard, switch to a products page, view details, or go back to a settings screen while staying inside a single-page application. Angular provides this through the RouterModule, route configuration, the router-outlet placeholder, template navigation with routerLink, and programmatic navigation with the Router service. Router links exist because manually changing URLs with plain anchors is not enough for modern Angular apps that need route parameters, lazy-loaded modules, guards, nested routes, and active link styling.
The most common navigation styles are static links such as /home, dynamic links with parameters like /products/10, relative navigation from the current route, and code-based navigation after actions such as form submission or login. You will also often use routerLinkActive to style the current menu item and [queryParams] to send filters like ?category=books. These features are heavily used in admin panels, e-learning portals, banking dashboards, and e-commerce sites where users expect smooth page transitions and meaningful URLs.
First, define routes in a routing file. Each route maps a URL path to a component. Second, place in a layout component, usually AppComponent. Angular renders the matched component inside this outlet. Third, add links using routerLink instead of a normal href. Fourth, use arrays for dynamic or segmented routes, such as [routerLink]="['/products', product.id]". Fifth, if navigation must happen after logic runs, inject Router and call navigate() or navigateByUrl().
Syntax basics: routerLink="/about" creates a simple route. [routerLink]="['/users', userId]" builds a route from parts. routerLinkActive="active" adds a CSS class when the link matches the current route. [queryParams]="{ sort: 'price' }" appends query parameters. For code navigation, this.router.navigate(['/orders', id]) is the standard pattern. Beginners should remember that routes must exist in the route configuration before links can work.
// app.routes.ts
import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';
export const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'about', component: AboutComponent }
];
// advanced programmatic navigation
import { Component } from '@angular/core';
import { Router } from '@angular/router';
@Component({
selector: 'app-login',
template: ``
})
export class LoginComponent {
constructor(private router: Router) {}
login() {
const userId = 42;
this.router.navigate(['/dashboard'], { queryParams: { user: userId } });
}
}href can trigger full page reload behavior. Use routerLink for Angular routes. to the layout.products/:id, build links in the same order.[routerLinkActiveOptions]./orders/125 over vague paths.routerLink for dynamic routes because it is safer than manual string building.routerLinkActive to improve navigation clarity for users./users/1, /users/2, and /users/3.routerLinkActive so the current page is highlighted.Build a small store navigation system with links for Home, Products, Cart, and a dynamic Product Details page using product IDs.
Create a filterable product page that navigates with query parameters such as category and sort order, then preserve those values while moving between pages.
Route parameters let an Angular application pass dynamic values through the URL, such as a product ID, username, order number, or category slug. They exist so one route definition can serve many pieces of content without creating separate routes for every item. In real projects, route parameters are used in pages like /users/42, /products/abc-123, or /orders/9001. Angular reads these values from the router and makes them available inside a component, where they are commonly used to fetch data from an API or display a selected record. The two ideas beginners should understand are required route parameters and optional query parameters. A route parameter is part of the path, such as :id in products/:id, while query parameters appear after a question mark, such as /products/10?tab=reviews. Angular supports both because path parameters identify a resource, while query parameters usually control filtering, sorting, tabs, or view state.
First, define a route with a placeholder in the router configuration. Angular uses a colon to mark a parameter name, for example path: 'users/:id'. Second, navigate to that route using router links or programmatic navigation. Third, read the value in the destination component using ActivatedRoute. You can access a snapshot for a one-time read or subscribe to parameter changes if the same component stays active while the URL changes. Snapshot access is simple and useful when the component is recreated on navigation. Subscription is better when the route parameter may change without fully destroying the component. Query parameters are read from queryParamMap or set using the queryParams option during navigation.
import { Routes } from '@angular/router';
import { UserDetailComponent } from './user-detail.component';
export const routes: Routes = [
{ path: 'users/:id', component: UserDetailComponent }
];import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-user-detail',
template: 'User ID: {{ userId }}
'
})
export class UserDetailComponent {
userId = '';
constructor(private route: ActivatedRoute) {
this.userId = this.route.snapshot.paramMap.get('id') || '';
}
}A real-world example fetches data using the route parameter.
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ProductService } from './product.service';
@Component({
selector: 'app-product-page',
template: '{{ product?.name }}
'
})
export class ProductPageComponent implements OnInit {
product: any;
constructor(private route: ActivatedRoute, private productService: ProductService) {}
ngOnInit() {
const id = this.route.snapshot.paramMap.get('id') || '';
this.productService.getProduct(id).subscribe(data => this.product = data);
}
}Advanced usage combines path and query parameters and reacts to changes.
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
@Component({
selector: 'app-order-page',
template: 'Order: {{ orderId }} | Tab: {{ tab }}
'
})
export class OrderPageComponent implements OnInit {
orderId = '';
tab = 'summary';
constructor(private route: ActivatedRoute, private router: Router) {}
ngOnInit() {
this.route.paramMap.subscribe(params => {
this.orderId = params.get('id') || '';
});
this.route.queryParamMap.subscribe(params => {
this.tab = params.get('tab') || 'summary';
});
}
showInvoices() {
this.router.navigate(['/orders', this.orderId], { queryParams: { tab: 'invoices' } });
}
}Using the wrong parameter name. If the route is :id, reading productId returns null. Fix: keep route config and code names identical.
Using snapshot when parameters can change in the same component. Fix: subscribe to paramMap for reactive updates.
Confusing path parameters with query parameters. Fix: use paramMap for route path values and queryParamMap for values after ?.
Use meaningful parameter names like id, slug, or orderId.
Validate or sanitize parameter values before using them in service calls.
Prefer subscriptions when users can move between records without leaving the component.
Use query parameters for UI state such as filters or active tabs, not for primary resource identity.
Create a route /students/:id and display the student ID in a component.
Add a route /articles/:slug and read the slug using ActivatedRoute.
Build a page that reads a path parameter id and a query parameter tab together.
Build a simple product details page where the URL looks like /products/101. Read the product ID from the route, call a mock service, and display the product name, price, and description.
Create a reusable details component that reacts to changing route parameters and updates data without reloading the page. Add query parameters for switching between overview, reviews, and specifications tabs.
app-routing.module.tsimport { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DashboardComponent } from './dashboard/dashboard.component';
import { OverviewComponent } from './dashboard/overview/overview.component';
import { SettingsComponent } from './dashboard/settings/settings.component';
const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
{
path: 'dashboard',
component: DashboardComponent,
children: [
{ path: '', redirectTo: 'overview', pathMatch: 'full' }, // Default child route
{ path: 'overview', component: OverviewComponent },
{ path: 'settings', component: SettingsComponent }
]
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
dashboard.component.htmlDashboard
app-routing.module.ts (excerpt)const routes: Routes = [
// ... other routes
{
path: 'users/:id',
component: UserProfileComponent,
children: [
{ path: '', redirectTo: 'info', pathMatch: 'full' },
{ path: 'info', component: UserInfoComponent },
{ path: 'posts', component: UserPostsComponent },
{ path: 'messages', component: UserMessagesComponent }
]
}
];
user-profile.component.htmlUser Profile (ID: {{ userId }})
user-profile.component.tsimport { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-user-profile',
templateUrl: './user-profile.component.html',
styleUrls: ['./user-profile.component.css']
})
export class UserProfileComponent implements OnInit {
userId: string | null = null;
constructor(private route: ActivatedRoute) { }
ngOnInit(): void {
this.route.paramMap.subscribe(params => {
this.userId = params.get('id');
});
}
}
app-routing.module.tsconst routes: Routes = [
// ... other routes
{
path: 'admin',
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
}
];
admin-routing.module.ts (inside `admin` folder)import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AdminDashboardComponent } from './admin-dashboard/admin-dashboard.component';
import { ManageUsersComponent } from './manage-users/manage-users.component';
import { ManageProductsComponent } from './manage-products/manage-products.component';
const routes: Routes = [
{
path: '', // relative to '/admin'
component: AdminDashboardComponent,
children: [
{ path: '', redirectTo: 'users', pathMatch: 'full' },
{ path: 'users', component: ManageUsersComponent },
{ path: 'products', component: ManageProductsComponent }
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AdminRoutingModule { }
Guards and authentication are essential parts of modern Angular applications. Authentication answers the question, who is the user? Guards answer, is this user allowed to access this route right now? In real applications such as admin dashboards, banking portals, healthcare systems, learning platforms, and company intranets, you must prevent unauthorized users from opening protected pages. Angular route guards give you a clean way to control navigation before a route loads, while authentication logic typically works with login APIs, tokens, sessions, and user roles.
Angular provides several guard types. CanActivate decides whether a route can be opened. CanActivateChild protects child routes under a parent. CanDeactivate checks whether the user can leave a page, useful for unsaved forms. CanMatch decides whether a route definition should match at all, often preferred in newer Angular apps. Some projects also use role-based checks, where authenticated users may still be blocked unless they have roles such as admin or manager.
The usual flow is simple. First, create an authentication service that stores login state and token data. Second, create a guard that checks whether the user is logged in. Third, attach the guard to routes. Fourth, redirect blocked users to a login page. In Angular, guards can return boolean, UrlTree, Promise, or Observable. Returning true allows navigation. Returning false blocks it. Returning a UrlTree is often better because Angular can redirect immediately.
Authentication is commonly implemented by sending login credentials to an API, receiving a token, and storing it safely. A simple demo may use localStorage, but production apps often combine secure token handling, refresh strategies, and HTTP interceptors. Guards should not replace backend security; they improve frontend control only.
import { Injectable } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
@Injectable({ providedIn: 'root' })
export class AuthService {
isLoggedIn(): boolean {
return !!localStorage.getItem('token');
}
}
export const authGuard: CanActivateFn = () => {
const router = new Router();
const auth = new AuthService();
return auth.isLoggedIn() ? true : router.parseUrl('/login');
};import { Routes } from '@angular/router';
import { DashboardComponent } from './dashboard.component';
import { LoginComponent } from './login.component';
import { authGuard } from './auth.guard';
export const routes: Routes = [
{ path: 'login', component: LoginComponent },
{ path: 'dashboard', component: DashboardComponent, canActivate: [authGuard] }
];import { inject, Injectable } from '@angular/core';
import { CanActivateFn, ActivatedRouteSnapshot, Router } from '@angular/router';
@Injectable({ providedIn: 'root' })
export class AuthService {
currentUser() {
return { loggedIn: true, role: 'admin' };
}
}
export const roleGuard: CanActivateFn = (route: ActivatedRouteSnapshot) => {
const auth = inject(AuthService);
const router = inject(Router);
const user = auth.currentUser();
const requiredRole = route.data['role'];
if (user.loggedIn && user.role === requiredRole) return true;
return router.parseUrl('/unauthorized');
};import { CanDeactivateFn } from '@angular/router';
export interface PendingChanges {
hasUnsavedChanges(): boolean;
}
export const unsavedGuard: CanDeactivateFn = (component) => {
return component.hasUnsavedChanges() ? confirm('Leave without saving?') : true;
}; UrlTree for cleaner navigation control.Observable or Promise correctly.inject() in modern Angular projects.AuthService with login(), logout(), and isLoggedIn() methods using browser storage./profile route so only logged-in users can open it, otherwise redirect to /login./admin route that only allows users with the admin role.Build a small Angular app with Login, Dashboard, Admin, and Unauthorized pages. Store a fake token and role after login, protect Dashboard with an auth guard, and protect Admin with a role guard.
Extend the app so that if a user tries to open a protected page while logged out, the app saves the attempted URL and redirects the user back to that page after successful login.
Forms in Angular are the main way users send information into an application. They are used in login pages, registration screens, checkout flows, profile editors, search filters, and admin dashboards. A form collects input, validates it, displays errors, and prepares clean data to submit to a server. Angular provides a structured approach so developers do not have to manually manage every field, error message, and change event. This makes form-heavy applications easier to build and maintain.
Angular supports two major form styles: template-driven forms and reactive forms. Template-driven forms are simpler and are useful for small or beginner-friendly forms because much of the logic is declared directly in the HTML template using directives such as ngModel. Reactive forms are more explicit and scalable. They define form structure and validation in TypeScript using classes like FormGroup, FormControl, and FormBuilder. In real-world enterprise projects, reactive forms are often preferred because they are easier to test, extend, and manage for complex workflows.
Both approaches share common concepts. A form contains controls such as text inputs, checkboxes, selects, and textareas. Each control can hold a value and track state like touched, dirty, valid, and invalid. Validation can be built-in, such as required fields and email format, or custom for business rules like password strength or age limits. Angular automatically updates the UI when form state changes, which helps create responsive and user-friendly experiences.
To use template-driven forms, import FormsModule into your Angular module or standalone component imports. Then add form fields with [(ngModel)] for two-way data binding and assign each input a name attribute. Angular will register those controls automatically.
To use reactive forms, import ReactiveFormsModule. In TypeScript, create a FormGroup containing one or more FormControl objects. In the template, connect the form using [formGroup] and each field using formControlName.
The basic syntax flow is: create the form structure, bind it to the template, add validators, show validation messages, and finally handle submission with a method like onSubmit().
// Basic template-driven example
// app.component.html
<form #userForm="ngForm" (ngSubmit)="save()">
<input name="username" [(ngModel)]="user.username" required />
<button type="submit">Save</button>
</form>
// app.component.ts
user = { username: '' };
save() {
console.log(this.user);
}// Real-world reactive login form
import { FormBuilder, Validators } from '@angular/forms';
loginForm = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(6)]]
});
onSubmit() {
if (this.loginForm.valid) {
console.log(this.loginForm.value);
}
}
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
<input formControlName="email" />
<input type="password" formControlName="password" />
<button type="submit">Login</button>
</form>// Advanced usage with custom validation
import { AbstractControl, ValidationErrors } from '@angular/forms';
function noAdminName(control: AbstractControl): ValidationErrors | null {
return control.value === 'admin' ? { reservedName: true } : null;
}
profileForm = this.fb.group({
displayName: ['', [Validators.required, noAdminName]],
age: [null, [Validators.required, Validators.min(18)]]
});FormsModule for template-driven forms and ReactiveFormsModule for reactive forms.name attribute: template-driven inputs need it so Angular can register the control.form.valid before sending data to an API.ngModel and reactive bindings on the same control.Build a user registration form that collects full name, email, password, and country, validates the fields, and prints the submitted data to the console only when the form is valid.
Create a reactive form with dynamic skills fields where users can add multiple skills, validate that at least one exists, and prevent duplicate skill names.
Template Driven Forms are Angular forms built mainly inside the HTML template instead of defining every control in TypeScript. They exist to make form creation simple, readable, and fast for common use cases such as contact forms, login pages, profile editors, and small administrative panels. Angular provides directives like ngForm, ngModel, and validation attributes so developers can bind input values, track touched or dirty state, and validate fields with minimal code. In real projects, template driven forms are useful when form structure is straightforward and you want the template to clearly show the relationship between fields, validation messages, and submission behavior. The main ideas are simple: every form element needs a name, two-way binding is usually done with [(ngModel)], and Angular automatically creates a form model behind the scenes. These forms also support built-in validators such as required, minlength, maxlength, and pattern checks. You can also use local template references to inspect state such as valid, invalid, touched, untouched, dirty, and pristine. Compared with reactive forms, template driven forms are easier for beginners because there is less TypeScript setup, but they are usually better for smaller and less dynamic forms. To use them, you must import FormsModule in the Angular feature or root module. Once enabled, Angular listens to the form and synchronizes data between the view and component model. This gives you a clean way to collect user input and respond to submit events while keeping templates expressive and easy to maintain.
Start by importing FormsModule. Then create a component property such as user = { name: '', email: '' }. In the template, create a form and assign a local reference like #userForm="ngForm". Add input fields with a unique name and bind them using [(ngModel)]. Angular now registers each field with the form. Add validators directly in HTML, for example required or minlength="3". To inspect a field, create another local reference such as #nameCtrl="ngModel". You can then show errors only when the control is invalid and touched. Finally, listen to submission with (ngSubmit) on the form. This event is better than a raw click on the button because it respects form behavior. Disable the submit button using [disabled]="userForm.invalid" to prevent incomplete submissions.
// app.module.ts
import { FormsModule } from '@angular/forms';
// component.ts
export class LoginComponent {
user = { email: '', password: '' };
submitForm() {
console.log(this.user);
}
}
// component.html
<form #loginForm="ngForm" (ngSubmit)="submitForm()">
<input type="email" name="email" [(ngModel)]="user.email" required #emailCtrl="ngModel">
<input type="password" name="password" [(ngModel)]="user.password" required minlength="6" #passwordCtrl="ngModel">
<button [disabled]="loginForm.invalid">Login</button>
</form>export class FeedbackComponent {
feedback = { name: '', topic: '', message: '' };
send() {
console.log('Feedback submitted', this.feedback);
}
}
<form #feedbackForm="ngForm" (ngSubmit)="send()">
<input name="name" [(ngModel)]="feedback.name" required>
<select name="topic" [(ngModel)]="feedback.topic" required>
<option value="bug">Bug</option>
<option value="feature">Feature</option>
</select>
<textarea name="message" [(ngModel)]="feedback.message" required minlength="10"></textarea>
<button [disabled]="feedbackForm.invalid">Send</button>
</form>export class ProfileComponent {
profile = { username: '', phone: '' };
save(form: any) {
if (form.valid) {
console.log('Saved', this.profile);
form.resetForm();
}
}
}
<form #profileForm="ngForm" (ngSubmit)="save(profileForm)">
<input name="username" [(ngModel)]="profile.username" required pattern="^[a-zA-Z0-9_]+$" #usernameCtrl="ngModel">
<input name="phone" [(ngModel)]="profile.phone" required pattern="^[0-9]{10}$" #phoneCtrl="ngModel">
<button [disabled]="profileForm.invalid">Save Profile</button>
</form>Forgetting to import FormsModule. Fix: add it to your module imports.
Missing the name attribute on inputs. Fix: every ngModel field inside a form needs a unique name.
Showing validation errors too early. Fix: check states like touched or dirty before displaying messages.
Using click events instead of (ngSubmit). Fix: handle form submission on the form element itself.
Use template driven forms for simple to moderately complex forms.
Keep model objects organized so bindings remain readable.
Disable submit buttons for invalid forms and provide clear validation feedback.
Use meaningful field names and local references for maintainability.
Build a login form with email and password using ngModel and required validation.
Create a registration form with name, phone, and city, then disable submit until all fields are valid.
Add a feedback form that shows error messages only after the user touches invalid fields.
Build a student enrollment form with fields for full name, email, course selection, and comments. Validate required fields, prevent invalid submission, and reset the form after a successful save.
Create a profile update form that validates username with a pattern, tracks dirty and touched states, and displays different messages for required, minlength, and pattern errors.
FormControl, FormGroup, and FormArray. A FormControl tracks the value and validation status of an individual form input element. A FormGroup aggregates multiple FormControl instances into a single unit, managing their values and validation status collectively. For example, a user registration form might be a FormGroup containing FormControls for 'username', 'email', and 'password'. Finally, a FormArray is used to manage an array of FormControl or FormGroup instances, ideal for dynamic lists of items like multiple phone numbers or addresses. Each of these classes extends AbstractControl, which provides common properties and methods for managing form state, such as value, status, valid, invalid, dirty, touched, and methods like setValue(), patchValue(), and reset().ReactiveFormsModule into your Angular module (usually AppModule).ReactiveFormsModule to the imports array of your @NgModule decorator.FormControl to represent a single input field. You can pass an initial value and an array of validators.myControl = new FormControl('initial value', Validators.required);FormGroup and pass an object where keys are form control names and values are FormControl or nested FormGroup instances.myForm = new FormGroup({
username: new FormControl('', Validators.required),
email: new FormControl('', [Validators.required, Validators.email])
});FormArray and pass an array of FormControl or FormGroup instances.aliases = new FormArray([
new FormControl(''),
new FormControl('')
]);[formControl] directive for individual controls, [formGroup] for form groups, and formArrayName for form arrays to link your template elements to your programmatic form model. For FormGroup, use formControlName on individual input elements.(ngSubmit) event on the element and access the form's value via myForm.value.A simple form with one input field and basic validation.
// app.component.ts
import { Component } from '@angular/core';
import { FormControl, Validators } from '@angular/forms';
@Component({
selector: 'app-root',
template: `
Name is required.
Form Value: {{ nameControl.value }}
Form Status: {{ nameControl.status }}
`
})
export class AppComponent {
nameControl = new FormControl('', Validators.required);
onSubmit() {
console.log('Form Submitted!', this.nameControl.value);
}
}
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ReactiveFormsModule } from '@angular/forms'; // <-- Import this
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
ReactiveFormsModule // <-- Add to imports
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }A user profile form with nested groups and multiple validations.
// profile.component.ts
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl, Validators, FormBuilder, FormArray } from '@angular/forms';
@Component({
selector: 'app-profile',
template: `
Form Value: {{ profileForm.value | json }}
Form Status: {{ profileForm.status }}
`
})
export class ProfileComponent implements OnInit {
profileForm!: FormGroup;
constructor(private fb: FormBuilder) { }
ngOnInit(): void {
this.profileForm = this.fb.group({
firstName: ['', Validators.required],
lastName: ['', Validators.required],
address: this.fb.group({
street: [''],
city: ['']
}),
aliases: this.fb.array([
this.fb.control('')
])
});
}
get aliases() {
return this.profileForm.get('aliases') as FormArray;
}
addAlias() {
this.aliases.push(this.fb.control(''));
}
onSubmit() {
console.warn(this.profileForm.value);
if (this.profileForm.valid) {
alert('Profile saved!' + JSON.stringify(this.profileForm.value));
}
}
}Using custom asynchronous validators.
// async-username.validator.ts
import { AbstractControl, AsyncValidatorFn, ValidationErrors } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { map, catchError, debounceTime, switchMap, first } from 'rxjs/operators';
import { UserService } from './user.service'; // Assume this service exists
export function existingUsernameValidator(userService: UserService): AsyncValidatorFn {
return (control: AbstractControl): Promise | Observable => {
if (!control.value) {
return of(null); // Don't validate empty values
}
return control.valueChanges.pipe(
debounceTime(500), // Wait for user to stop typing
switchMap(value => userService.checkUsernameExists(value)), // Make API call
map(isTaken => (isTaken ? { existingUsername: true } : null)),
first(), // Take only the first emission after debounce
catchError(() => of(null)) // Handle API errors gracefully
);
};
}
// user.service.ts (mock service)
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { delay } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class UserService {
private existingUsernames = ['admin', 'john.doe'];
checkUsernameExists(username: string): Observable {
const isTaken = this.existingUsernames.includes(username.toLowerCase());
return of(isTaken).pipe(delay(1000)); // Simulate API call delay
}
}
// signup.component.ts
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import { existingUsernameValidator } from './async-username.validator';
import { UserService } from './user.service';
@Component({
selector: 'app-signup',
template: `
Form Status: {{ signupForm.status }}
`
})
export class SignupComponent implements OnInit {
signupForm!: FormGroup;
constructor(private userService: UserService) { }
ngOnInit(): void {
this.signupForm = new FormGroup({
username: new FormControl(
'',
[Validators.required, Validators.minLength(3)],
[existingUsernameValidator(this.userService)] // Async validator
)
});
}
onSubmit() {
if (this.signupForm.valid) {
console.log('User registered:', this.signupForm.value);
}
}
} ReactiveFormsModule: This is a very common oversight. Without it, Angular won't recognize [formControl], [formGroup], etc., leading to template errors. ReactiveFormsModule to the imports array of your @NgModule.formControlName without [formGroup] or [formArrayName] context: formControlName must always be nested within an element that has a [formGroup] or formArrayName directive. form.invalid, it will be disabled during the pending state, which might not be desired. form.invalid && !form.pending, or provide visual feedback for the pending state.FormBuilder: For creating complex forms, FormBuilder provides a more concise and readable syntax for instantiating FormControl, FormGroup, and FormArray.valueChanges and statusChanges: Subscribe to these observables to react to form changes in real-time, enabling dynamic UI updates, conditional logic, or auto-saving features.FormArray to manage a list of existing to-do items. Add buttons to dynamically add new to-do inputs and remove existing ones. Each to-do item should be a FormControl.FormGroup to ensure that both password fields match.Build a simple 'Product Editor' component. This component should display a reactive form for editing a product. The form should include: 'Product Name' (required, min length 5), 'Price' (required, must be a positive number), 'Description' (optional), and a 'Category' (dropdown with at least 3 options). Use FormGroup and appropriate validators. Pre-fill the form with some dummy product data when the component initializes.
Extend the 'Product Editor' form. Add a 'Tags' field that allows users to add multiple tags to a product. This should be implemented using a FormArray of FormControls. Include functionality to add new tags and remove individual tags. Additionally, implement an asynchronous validator for the 'Product Name' field that checks if a product name already exists in a mock database (simulate an HTTP call with a delay). Display appropriate feedback for pending and taken product names.
Name is required.
// app.component.ts
import { Component } from '@angular/core';
import { NgForm } from '@angular/forms';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
onSubmit(form: NgForm) {
console.log('Form submitted!', form.value);
}
}
// app.component.ts
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
registrationForm!: FormGroup;
ngOnInit() {
this.registrationForm = new FormGroup({
'username': new FormControl(null, [Validators.required, Validators.minLength(3)]),
'email': new FormControl(null, [Validators.required, Validators.email]),
'password': new FormControl(null, [
Validators.required,
Validators.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/)
])
});
}
onRegister() {
if (this.registrationForm.valid) {
console.log('Registration data:', this.registrationForm.value);
// Send data to backend
} else {
console.log('Form is invalid. Please check the fields.');
this.registrationForm.markAllAsTouched(); // Mark all fields as touched to show errors
}
}
}
// app.component.ts
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl, Validators, AbstractControl, ValidatorFn } from '@angular/forms';
// Custom validator function
export function passwordMatchValidator(): ValidatorFn {
return (control: AbstractControl): { [key: string]: any } | null => {
const password = control.get('password');
const confirmPassword = control.get('confirmPassword');
if (password && confirmPassword && password.value !== confirmPassword.value) {
return { 'passwordsMatch': true };
}
return null;
};
}
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
passwordForm!: FormGroup;
ngOnInit() {
this.passwordForm = new FormGroup({
'password': new FormControl(null, Validators.required),
'confirmPassword': new FormControl(null, Validators.required)
}, { validators: passwordMatchValidator() }); // Apply custom validator at FormGroup level
}
onSubmitPassword() {
if (this.passwordForm.valid) {
console.log('Password changed!', this.passwordForm.value);
} else {
console.log('Form is invalid.');
this.passwordForm.markAllAsTouched();
}
}
}
Custom validators in Angular are functions or classes that check whether form input follows rules that are specific to your application. Built-in validators such as required, minLength, and email are useful, but real applications often need more control. For example, a company may require passwords to contain a number and a symbol, usernames to avoid reserved words, or booking dates to fall within a valid business range. That is where custom validators become important.
Angular uses custom validators in both reactive forms and template-driven forms. In reactive forms, validators are commonly written as functions that receive an AbstractControl and return either null when valid or an error object when invalid. There are two major forms: synchronous validators, which run immediately, and asynchronous validators, which are used when validation depends on server data, such as checking whether an email already exists.
Custom validators are widely used in signup forms, internal business tools, payment workflows, inventory dashboards, and HR portals. They improve data quality, reduce server-side errors, and provide users with fast feedback before submission.
To create a basic custom validator in Angular, define a function. The function should accept an AbstractControl. Inside it, read the field value and test it against your rule. If the value passes, return null. If it fails, return an object like { invalidRule: true }. Angular stores that object in the control's errors property.
You can attach the validator when creating a FormControl or inside a FormGroup. For reusable validation, you can also create validator factories. These are functions that take configuration values and return validator functions. For example, a minimum age validator might accept the age limit as a parameter.
For form-level validation, apply the validator to the whole FormGroup instead of a single field. This is useful for matching password and confirm password fields.
import { AbstractControl, ValidationErrors } from '@angular/forms';
export function noSpacesValidator(control: AbstractControl): ValidationErrors | null {
const value = control.value as string;
if (value && value.includes(' ')) {
return { noSpaces: true };
}
return null;
}import { FormControl, FormGroup, Validators } from '@angular/forms';
import { noSpacesValidator } from './validators';
profileForm = new FormGroup({
username: new FormControl('', [Validators.required, noSpacesValidator])
});
Username cannot contain spaces.
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
export function forbiddenNamesValidator(names: string[]): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const value = (control.value || '').toLowerCase();
return names.includes(value) ? { forbiddenName: true } : null;
};
}registerForm = new FormGroup({
username: new FormControl('', [forbiddenNamesValidator(['admin', 'root'])])
});import { AbstractControl, ValidationErrors, ValidatorFn, FormGroup } from '@angular/forms';
export const passwordMatchValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
const group = control as FormGroup;
const password = group.get('password')?.value;
const confirmPassword = group.get('confirmPassword')?.value;
return password === confirmPassword ? null : { passwordMismatch: true };
};
accountForm = new FormGroup({
password: new FormControl(''),
confirmPassword: new FormControl('')
}, { validators: passwordMatchValidator });null or an object. Use { customError: true }, not false.FormGroup, not on one input.passwordMismatch instead of generic labels.minLength.test, guest, and system.Build a registration form with three custom rules: username cannot contain spaces, password must include at least one number, and confirm password must match password. Display a separate error message for each failed rule.
Create an asynchronous custom validator that checks whether a username is already taken by simulating an API call with RxJS and applying the result to a reactive form field.
DatePipe, CurrencyPipe, UpperCasePipe, and DecimalPipe are pure pipes. Impure pipes, on the other hand, are executed during every change detection cycle, regardless of whether the input value or parameters have changed. This is necessary for pipes that depend on values that might change internally (like an array that has items added or removed without changing the array reference) or external factors (like the current time for an AsyncPipe). An example of an impure pipe is the AsyncPipe, which subscribes to an observable or promise and returns the latest value it has emitted. Another common example is a custom filter pipe that needs to re-evaluate its filtering logic whenever an item is added or removed from an array. Due to their frequent execution, impure pipes can have a performance impact if not used carefully. Angular also distinguishes between stateful and stateless pipes. A stateless pipe is one that doesn't maintain any internal state and produces the same output for the same input. A stateful pipe, like AsyncPipe, maintains internal state (e.g., its subscription to an observable) and manages its lifecycle. While Angular doesn't explicitly categorize pipes as stateful/stateless in its documentation, understanding this concept helps in grasping how pipes operate and when they might trigger re-renders or updates.|) after the data expression you want to transform, followed by the pipe's name. If the pipe accepts arguments, you pass them after a colon (:) following the pipe name. Multiple pipes can be chained together, with the output of one pipe becoming the input of the next. The syntax looks like this: {{ value | pipeName[:arg1[:arg2 ...]] | anotherPipe }}. Let's break down the components:value: This is the data you want to transform. It can be a component property, a literal value, or the result of a function call.| (Pipe operator): This symbol tells Angular to apply a pipe to the preceding value.pipeName: The name of the pipe you want to use (e.g., date, currency, uppercase).:arg1[:arg2 ...] (Optional arguments): Some pipes accept one or more arguments to modify their behavior. These are separated by colons.{{ 'hello world' | uppercase | slice:0:5 }} would first convert 'hello world' to 'HELLO WORLD', and then slice it to 'HELLO'.@Pipe() decorator and implement the PipeTransform interface. The transform method is where your transformation logic resides. It takes the input value as its first argument, followed by any additional pipe arguments. You then need to declare your custom pipe in an Angular module (typically AppModule) within the declarations array.
Original date: {{ today }}
Formatted date: {{ today | date }}
Short date: {{ today | date:'shortDate' }}
Full date: {{ today | date:'fullDate' }}
Original text: {{ message }}
Uppercase text: {{ message | uppercase }}
Lowercase text: {{ message | lowercase }}
Original number: {{ price }}
Currency: {{ price | currency }}
Currency with specific code: {{ price | currency:'EUR':'symbol':'1.2-2' }}
// app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
today: Date = new Date();
message: string = 'Hello Angular Pipes!';
price: number = 1234.5678;
}
import { Pipe, PipeTransform } from '@angular/core';
interface Hero {
name: string;
power: string;
}
@Pipe({
name: 'heroFilter',
pure: false // This pipe needs to re-evaluate on every change if the array itself doesn't change reference
})
export class HeroFilterPipe implements PipeTransform {
transform(heroes: Hero[], searchTerm: string): Hero[] {
if (!heroes || !searchTerm) {
return heroes;
}
searchTerm = searchTerm.toLowerCase();
return heroes.filter(hero =>
hero.name.toLowerCase().includes(searchTerm) ||
hero.power.toLowerCase().includes(searchTerm)
);
}
}
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms'; // Required for ngModel
import { AppComponent } from './app.component';
import { HeroFilterPipe } from './hero-filter.pipe';
@NgModule({
declarations: [
AppComponent,
HeroFilterPipe // Declare your custom pipe here
],
imports: [
BrowserModule,
FormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
{{ hero.name }} ({{ hero.power }})
import { Component } from '@angular/core';
interface Hero {
name: string;
power: string;
}
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
heroes: Hero[] = [
{ name: 'Superman', power: 'Flight' },
{ name: 'Batman', power: 'Gadgets' },
{ name: 'Wonder Woman', power: 'Super Strength' },
{ name: 'The Flash', power: 'Super Speed' }
];
searchHero: string = '';
}
Chained Pipes:
{{ longText | slice:0:20 | uppercase }}...
Async Pipe with Observable:
Current time: {{ time$ | async | date:'mediumTime' }}
// app.component.ts
import { Component, OnInit } from '@angular/core';
import { Observable, interval, map } from 'rxjs';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
longText: string = 'This is a very long text that needs to be truncated and then converted to uppercase.';
time$!: Observable;
ngOnInit() {
this.time$ = interval(1000).pipe(map(() => new Date()));
}
}
pure: false if absolutely necessary (e.g., when filtering an array where only its contents change, not its reference).declarations array of an Angular module (e.g., AppModule or a feature module) before they can be used. Forgetting this step will result in template compilation errors, stating that the pipe could not be found.DatePipe, CurrencyPipe, SlicePipe, etc.) can achieve the desired transformation. They are well-tested and optimized.null, undefined, or empty input values. This prevents runtime errors and makes your application more robust.currency pipe to display the price in USD and the decimal pipe to format quantities with two decimal places.ReverseStringPipe that takes a string as input and returns the string with its characters in reverse order. Apply it to a message in your component's template.date pipe to display the current date in two different formats: 'short' and 'longDate'. Also, display the current time using the 'shortTime' format.slice pipe to truncate the content to the first 100 characters, followed by '...' for preview. titlecase pipe on the author's name to ensure consistent capitalization. date pipe to 'MM/dd/yyyy HH:mm'.SearchHighlightPipe. This pipe should take a main string and a search term as input. It should return the main string with all occurrences of the search term wrapped in ... tags. You'll need to use DomSanitizer to bypass security and render the HTML safely in your template. Consider making it case-insensitive. Angular built in pipes are template tools that transform displayed values without changing the original data source. They exist so developers can format dates, currencies, percentages, text, JSON, and list output directly in HTML templates instead of writing repetitive formatting logic in components. In real applications, pipes are used everywhere: showing order totals as currency, formatting timestamps in dashboards, displaying usernames in uppercase, limiting long lists with slicing, and presenting API responses cleanly during debugging. Angular includes several commonly used built in pipes such as DatePipe, UpperCasePipe, LowerCasePipe, TitleCasePipe, CurrencyPipe, DecimalPipe, PercentPipe, SlicePipe, JsonPipe, KeyValuePipe, AsyncPipe, and I18nPluralPipe. A pipe is applied with the | symbol inside interpolation or property binding. Some pipes accept parameters after colons, which lets you control format style. Pipes improve readability, reduce repeated code, and keep templates expressive. However, they should mainly be used for presentation, not heavy business logic. Understanding built in pipes is essential because they are among the most practical Angular features for everyday UI development.
The basic syntax is {{ value | pipeName }}. If a pipe needs options, use {{ value | pipeName:param1:param2 }}. For example, {{ price | currency:'USD' }} formats a number as US dollars. Text pipes include uppercase, lowercase, and titlecase. Number pipes include decimal, percent, and currency. Utility pipes include json for debugging, slice for arrays or strings, keyvalue for objects, and async for Observables or Promises. The async pipe is especially important because it subscribes automatically in the template and unsubscribes when the component is destroyed, reducing manual subscription code. Date formatting is also common, such as {{ createdAt | date:'medium' }}. When choosing a pipe, ask one question: do I want to display the same data in a more useful format? If yes, a pipe is often the right template tool.
{{ username | uppercase }}
{{ today | date:'fullDate' }}
{{ amount | currency:'USD' }}
{{ score | percent }}import { Component } from '@angular/core';
@Component({
selector: 'app-basic-pipes',
templateUrl: './basic-pipes.component.html'
})
export class BasicPipesComponent {
username = 'angular learner';
today = new Date();
amount = 2499.99;
score = 0.87;
}
{{ product.name | titlecase }}
Price: {{ product.price | currency:'USD':'symbol':'1.2-2' }}
Discount: {{ product.discount | percent:'1.0-0' }}
Added: {{ product.createdAt | date:'short' }}
Preview: {{ product.description | slice:0:40 }}...
product = {
name: 'wireless mechanical keyboard',
price: 129.5,
discount: 0.15,
createdAt: new Date(),
description: 'compact keyboard with rgb lighting and hot-swappable switches'
};
- {{ item.title | titlecase }}
{{ pair.key }}: {{ pair.value }}
{{ apiResponse | json }}tasks$ = this.taskService.getTasks();
settings = { theme: 'dark', language: 'en', notifications: true };
apiResponse = { status: 'ok', count: 3 };async in templates for simple stream rendering.async for Observables shown directly in templates.json only for debugging, not production-facing UI.Build a small order summary card that displays customer name, order total as currency, discount as percent, order date in readable format, and a short preview of notes using the slice pipe.
Create a task dashboard that loads tasks from an Observable using the async pipe and also displays an application settings object using the keyvalue pipe.
Custom pipes in Angular let you transform data directly inside templates in a clean and reusable way. Angular already includes built-in pipes such as date, currency, uppercase, and json, but real applications often need formatting rules specific to the business. For example, you may want to shorten long product names, format user roles, mask part of an email address, or convert internal status codes into friendly labels. A custom pipe solves this by placing transformation logic in one dedicated class instead of repeating string manipulation across components. This improves readability, reuse, and maintainability. Pipes are commonly used in dashboards, admin panels, e-commerce apps, reporting systems, and internal enterprise tools where the same formatting rules appear in many screens.
Angular supports two important kinds of custom pipes: pure and impure. A pure pipe runs only when Angular detects a pure input change, such as a new primitive value or a new object reference. This is the default and is best for performance. An impure pipe runs during every change detection cycle, which can be useful when working with mutable arrays or objects, but it can become expensive if overused. Most custom pipes should remain pure unless there is a specific reason not to. A pipe can also accept parameters, which makes it more flexible. For instance, you can pass a maximum length, a replacement text, or a formatting mode.
To create a custom pipe, generate or write a class and decorate it with @Pipe. The decorator needs a pipe name, which is the identifier used in templates. The class must implement the PipeTransform interface and define the transform() method. This method receives the input value as the first argument, followed by any optional parameters. Inside it, return the transformed result. Once declared in a standalone component import list or a module, the pipe can be used with the pipe operator | inside templates.
The basic syntax is {{ value | pipeName }}. If parameters are needed, write {{ value | pipeName:param1:param2 }}. Beginners should remember that pipes are mostly for display formatting, not for complex business workflows or server-side processing. Keep the transformation focused and predictable.
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'titleCaseWords'
})
export class TitleCaseWordsPipe implements PipeTransform {
transform(value: string): string {
if (!value) return '';
return value
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' ');
}
}
{{ 'angular custom pipes' | titleCaseWords }}
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'maskEmail'
})
export class MaskEmailPipe implements PipeTransform {
transform(value: string): string {
if (!value || !value.includes('@')) return value;
const [name, domain] = value.split('@');
const visible = name.slice(0, 2);
return visible + '***@' + domain;
}
}
Customer: {{ '[email protected]' | maskEmail }}
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'truncateText'
})
export class TruncateTextPipe implements PipeTransform {
transform(value: string, limit: number = 20, suffix: string = '...'): string {
if (!value) return '';
return value.length > limit ? value.slice(0, limit) + suffix : value;
}
}
{{ product.description | truncateText:30:'...' }}
The first example shows a basic formatting pipe. The second reflects a real business need: masking personal data. The third demonstrates advanced usage with parameters, which is common in enterprise apps.
reverseText that reverses a string.statusLabel that converts values like pending, approved, and rejected into user-friendly labels.shortName that limits a full name to a maximum number of characters.Build a small employee directory view where names are title-cased, email addresses are masked, and long job descriptions are truncated using your custom pipes.
Create a custom pipe that formats file sizes such as bytes into KB, MB, and GB, and make it accept a parameter for decimal precision.
Lifecycle hooks in Angular are special methods that let you respond to important moments in a component or directive life, from creation to destruction. They exist because Angular manages component instances, input updates, rendering, and cleanup behind the scenes. Hooks give developers safe, predictable places to run logic such as loading data, reacting to changed inputs, initializing third-party libraries, checking projected content, working with child views, and releasing resources. In real applications, lifecycle hooks are used in dashboards that fetch API data when a page opens, form components that react when parent inputs change, charts that render only after the view is ready, and subscription-based screens that must clean up memory when users navigate away.
Angular provides several commonly used hooks. ngOnChanges runs when an input property changes and is especially useful for components that receive dynamic data from parents. ngOnInit runs once after Angular first initializes input values, making it a common place for startup logic. ngDoCheck allows custom change detection logic, though it should be used carefully because it runs often. ngAfterContentInit and ngAfterContentChecked relate to projected content passed with . ngAfterViewInit and ngAfterViewChecked are tied to the component view and child views, often used with @ViewChild. Finally, ngOnDestroy runs before Angular removes the component, which is the right time to unsubscribe from observables, clear timers, and detach listeners.
To use a hook, create a component class and add the matching method name. You may also implement Angular interfaces like OnInit or OnDestroy for clarity and type safety. The usual order is: constructor, ngOnChanges, ngOnInit, ngDoCheck, content hooks, view hooks, and later ngOnDestroy when the component is removed. The constructor is not a lifecycle hook; it is only for basic dependency injection and simple setup. Beginners often place business logic there, but Angular data such as @Input() values may not be ready yet.
Use ngOnChanges(changes: SimpleChanges) when you must inspect previous and current input values. Use ngOnInit() for one-time initialization. Use ngAfterViewInit() when the template and child references are fully available. Use ngOnDestroy() for cleanup. Avoid heavy work in hooks that run repeatedly, especially ngDoCheck and checked hooks, because they can hurt performance.
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-user-card',
template: '{{ userName }}
'
})
export class UserCardComponent implements OnChanges, OnInit {
@Input() userName = '';
ngOnChanges(changes: SimpleChanges): void {
console.log('Input changed:', changes['userName']);
}
ngOnInit(): void {
console.log('Component initialized');
}
}import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-price-tag',
template: 'Final Price: {{ finalPrice }}
'
})
export class PriceTagComponent implements OnChanges {
@Input() price = 0;
@Input() discount = 0;
finalPrice = 0;
ngOnChanges(changes: SimpleChanges): void {
this.finalPrice = this.price - this.discount;
}
}import { AfterViewInit, Component, ElementRef, OnDestroy, ViewChild } from '@angular/core';
@Component({
selector: 'app-live-search',
template: ''
})
export class LiveSearchComponent implements AfterViewInit, OnDestroy {
@ViewChild('box') box!: ElementRef;
private timerId: any;
ngAfterViewInit(): void {
this.timerId = setInterval(() => {
console.log(this.box.nativeElement.value);
}, 1000);
}
ngOnDestroy(): void {
clearInterval(this.timerId);
}
}ngOnInit().ngOnDestroy().ngAfterViewInit() for input change logic: Use ngOnChanges() when parent data can change many times.ngDoCheck and checked hooks lightweight.OnInit and OnDestroy for readability.ngOnDestroy() as mandatory for anything long-running.ngOnChanges() instead of manual DOM checks.@Input() message and log old and new values using ngOnChanges().ngOnInit() and displays it.ngAfterViewInit() and stops in ngOnDestroy().Build a product details component that receives product data from a parent, recalculates a discounted price when inputs change, focuses a quantity field after the view loads, and cleans up any running interval when the component is removed.
Create a parent-child setup where the parent updates stock data every few seconds, and the child uses the correct lifecycle hooks to detect updates, refresh displayed totals, and avoid memory leaks during navigation.
In Angular, components are designed to be reusable, isolated building blocks. However, real applications require components to work together. A parent component may need to send data into a child component, and the child may need to notify the parent when something happens, such as a button click, form update, or item selection. Angular solves this with @Input() and @Output(). @Input() allows a parent to pass values down into a child, while @Output() allows a child to emit custom events upward to the parent. This pattern appears everywhere in production systems: product cards receiving product data, modal components notifying when they close, pagination controls reporting page changes, and form widgets emitting submitted values. The main goal is clean, predictable communication without direct component manipulation. A child should not fetch parent data by itself, and a parent should not reach into the child to control internal behavior unnecessarily. Instead, Angular encourages one-way data flow: data moves down through inputs, and events move up through outputs. The most common sub-types here are simple value inputs, aliased inputs, event outputs using EventEmitter, and outputs that send payload data such as objects, numbers, or form values. Understanding this pattern is essential because it makes components easier to test, reuse, and maintain in larger applications.
First, create a child component. Import Input, Output, and EventEmitter from @angular/core. Add @Input() before a property to mark it as data that can be received from a parent. Add @Output() before an EventEmitter property to expose an event. In the parent template, bind data into the child using square brackets like [product]="selectedProduct". Listen to child events using parentheses like (added)="onAdded($event)". The child emits data by calling this.added.emit(value). Use inputs for values the child needs to display or process, and outputs for user actions or state changes the parent should handle.
// child: user-card.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-user-card',
template: `
Name: {{ name }}
`
})
export class UserCardComponent {
@Input() name = '';
@Output() selected = new EventEmitter();
selectUser() {
this.selected.emit(this.name);
}
}
// parent template
// real-world product card
// child: product-card.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-product-card',
template: `
{{ product.title }}
4f{{ product.price }}
`
})
export class ProductCardComponent {
@Input() product!: { id: number; title: string; price: number };
@Output() add = new EventEmitter();
addToCart() {
this.add.emit(this.product.id);
}
}
// parent template
[product]="item"
(add)="handleAddToCart($event)">
// advanced usage with aliases and object payload
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-status-toggle',
template: ``
})
export class StatusToggleComponent {
@Input('buttonLabel') label = 'Status';
@Input() active = false;
@Output() changed = new EventEmitter<{ active: boolean; changedAt: Date }>();
toggle() {
this.active = !this.active;
this.changed.emit({ active: this.active, changedAt: new Date() });
}
}
// parent template
[buttonLabel]="'Notifications'"
[active]="notificationsEnabled"
(changed)="onStatusChanged($event)">
@Input() or @Output() will not participate in Angular binding. Add the correct decorator.EventEmitter.product, isDisabled, or title.saved, closed, or selected.Build a reusable app-rating component that receives the current rating and maximum stars through inputs, then emits the new selected rating to the parent when a star is clicked.
Create a parent dashboard with multiple child filter components. Each child should receive configuration through inputs and emit selected filter values upward. Combine all emitted values in the parent to update a visible results summary.
ViewChild and ContentChild are Angular decorators used to query and access elements, directives, template references, or child components from a parent component class. They exist because sometimes a component needs controlled access to something inside its own template or to content projected into it with . In real applications, this is useful for focusing an input after load, calling a public method on a child component, reading a directive instance, or interacting with projected content such as a custom card title. The key difference is scope. ViewChild searches inside the component’s own view template. ContentChild searches inside content provided by the parent and projected into the component. Understanding this difference is critical for building reusable UI components such as modals, tabs, form wrappers, and layout containers.
Angular queries can target a component class, directive, template reference variable, or special tokens like ElementRef and TemplateRef. If you need access to something declared directly in the component’s HTML file, use ViewChild. If your component uses and needs access to something passed into that slot, use ContentChild. These decorators are resolved during Angular lifecycle processing, so timing matters. View queries are commonly safe in ngAfterViewInit, while content queries are commonly safe in ngAfterContentInit.
To use @ViewChild(), first place a child component, directive, or element inside the template. Then assign either a template reference such as #box or query by class type. In the component class, import ViewChild and declare a property like @ViewChild('box') box!: ElementRef;. Angular fills that property after the view is initialized. For @ContentChild(), create a reusable component with . Then project markup into it from a parent. Inside the reusable component, import ContentChild and query the projected element, directive, or reference. Angular resolves it after content projection is initialized.
Another important detail is the read option. Sometimes Angular finds a matching element, but you want a specific representation such as ElementRef instead of a directive instance. You can use { read: ElementRef }. Also note the static option with ViewChild, mainly relevant when access is needed earlier in the lifecycle. In most dynamic cases, static: false works and the value becomes available in ngAfterViewInit.
// Basic ViewChild example
@Component({
selector: 'app-parent',
template: ``
})
export class ParentComponent {
@ViewChild('searchBox') searchBox!: ElementRef;
focusInput() {
this.searchBox.nativeElement.focus();
}
}// Real-world ViewChild with child component
@Component({
selector: 'app-player',
template: `Video player ready
`
})
export class PlayerComponent {
play() { console.log('Playing video'); }
}
@Component({
selector: 'app-dashboard',
template: ` `
})
export class DashboardComponent {
@ViewChild(PlayerComponent) player!: PlayerComponent;
start() {
this.player.play();
}
}// Advanced ContentChild example
@Directive({ selector: '[cardTitle]' })
export class CardTitleDirective {}
@Component({
selector: 'app-card',
template: ` `
})
export class CardComponent implements AfterContentInit {
@ContentChild(CardTitleDirective, { read: ElementRef }) title!: ElementRef;
ngAfterContentInit() {
console.log('Projected title:', this.title.nativeElement.textContent);
}
}
@Component({
selector: 'app-root',
template: `Monthly Report
Revenue details
`
})
export class AppComponent {}, use ContentChild.ngAfterViewInit and ContentChild in ngAfterContentInit. and use ContentChild to detect a projected title element.Build a reusable modal component that projects a custom title and body with , reads the projected title using ContentChild, and exposes open() and close() methods that a parent calls through ViewChild.
Create a tab container component that uses ContentChild or related content queries to detect projected tab headers, while the parent uses ViewChild to activate the first tab programmatically after initialization.
State management in Angular is the process of storing, updating, and sharing application data in a predictable way. State can be anything your app needs to remember, such as a logged-in user, shopping cart items, selected filters, form progress, or API-loaded records. Without a clear approach, components may duplicate data, pass values through many layers, or become difficult to maintain. In real applications like dashboards, ecommerce platforms, admin panels, and collaboration tools, state management helps teams keep data synchronized across many screens and interactions.
In Angular, state is often handled at different levels. Local component state is data used only inside one component, such as a toggle or selected tab. Shared service state is data stored in an injectable service and accessed by multiple components. Reactive state commonly uses RxJS tools such as BehaviorSubject or Observable so components react automatically when data changes. In larger apps, developers may use store libraries such as NgRx, but beginners should first understand service-based state because it teaches the fundamentals clearly.
Start by identifying where the data belongs. If only one component needs it, keep it in that component. If many components need it, move it into a service. Then decide how updates should happen. A common beginner-friendly pattern is to keep private state in a service and expose it through an observable. Components subscribe to that observable and update the UI automatically.
Typical flow: create a service, define a private BehaviorSubject, expose a public observable with asObservable(), and add methods such as addItem(), removeItem(), or clear(). This keeps write operations controlled and prevents components from changing state directly. The syntax is simple: initialize the subject with a starting value, read the current value with getValue(), then push a new value with next().
Basic example: shared counter state.
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class CounterService {
private countSubject = new BehaviorSubject(0);
count$ = this.countSubject.asObservable();
increment() {
this.countSubject.next(this.countSubject.getValue() + 1);
}
} import { Component } from '@angular/core';
import { CounterService } from './counter.service';
@Component({
selector: 'app-counter',
template: `Count: {{ counterService.count$ | async }}
`
})
export class CounterComponent {
constructor(public counterService: CounterService) {}
}Real-world example: cart state shared across product and header components.
@Injectable({ providedIn: 'root' })
export class CartService {
private cartSubject = new BehaviorSubject([]);
cart$ = this.cartSubject.asObservable();
addItem(item: string) {
const current = this.cartSubject.getValue();
this.cartSubject.next([...current, item]);
}
} Advanced usage: derived state such as total items.
import { map } from 'rxjs/operators';
totalItems$ = this.cart$.pipe(map(items => items.length));setUser(), addItem(), and resetFilters().light or dark mode and display it in two different components.Build a small shopping cart feature where one component lists products and another component shows the current cart count using a shared Angular service.
Extend the cart state so it stores objects with name and price, then create derived state for total item count and total cost using observables.
NgRx is a state management library for Angular inspired by Redux and built around RxJS. It exists to help teams manage application state in a predictable, centralized way, especially when apps grow beyond a few simple components. In small Angular projects, passing data with @Input(), services, and local component state is often enough. But in larger products such as admin dashboards, e-commerce systems, booking platforms, or banking apps, many parts of the interface need shared, synchronized data. NgRx solves this by introducing a single flow for state updates: components dispatch actions, reducers create new state, and selectors read data efficiently. Effects handle side effects such as API calls.
The main pieces are the Store, Actions, Reducers, Selectors, and Effects. The Store is the central container for state. Actions are plain objects that describe something that happened, such as loading products or logging in. Reducers are pure functions that take the current state and an action, then return a new state without mutating the old one. Selectors are reusable functions that read and derive slices of state. Effects listen for actions and run asynchronous work like HTTP requests, then dispatch success or failure actions. Real-world teams use NgRx when they need traceable state changes, easier debugging, stronger architecture, and consistent patterns across many developers.
A typical NgRx flow starts when a user clicks a button. The component does not directly change shared state. Instead, it dispatches an action using the Store service. For example, a product page may dispatch loadProducts(). An effect catches that action, calls an API service, and then dispatches either loadProductsSuccess({ products }) or loadProductsFailure({ error }). The reducer receives the success or failure action and returns a new state object. Finally, components use selectors to subscribe to the updated data from the Store.
Important beginner syntax includes createAction, props, createReducer, on, createFeatureSelector, and createSelector. Actions define event names and optional payloads. Reducers map actions to state transitions. Selectors prevent components from knowing the full store structure. Effects use RxJS operators like ofType, map, switchMap, and catchError.
// Basic example: actions and reducer
import { createAction, props, createReducer, on } from '@ngrx/store';
export const increment = createAction('[Counter] Increment');
export const reset = createAction('[Counter] Reset');
export interface CounterState {
count: number;
}
export const initialState: CounterState = { count: 0 };
export const counterReducer = createReducer(
initialState,
on(increment, state => ({ ...state, count: state.count + 1 })),
on(reset, state => ({ ...state, count: 0 }))
);// Real-world example: selector and component usage
import { createFeatureSelector, createSelector } from '@ngrx/store';
export interface ProductState {
products: string[];
loading: boolean;
}
export const selectProductState = createFeatureSelector('products');
export const selectProducts = createSelector(selectProductState, state => state.products);
export const selectLoading = createSelector(selectProductState, state => state.loading);
// component
products$ = this.store.select(selectProducts);
loading$ = this.store.select(selectLoading); // Advanced usage: effect with API call
loadProducts$ = createEffect(() =>
this.actions$.pipe(
ofType(loadProducts),
switchMap(() =>
this.productService.getAll().pipe(
map(products => loadProductsSuccess({ products })),
catchError(error => of(loadProductsFailure({ error: error.message })))
)
)
)
);[Products Page] Load Products.Build a small product catalog feature where a button dispatches an action to load products, an effect fetches them from a service, a reducer stores them, and a component displays them with a loading indicator.
Extend the product catalog so users can filter products by category using selectors without changing the original stored product list.
Lazy loading modules in Angular means loading a feature only when the user actually needs it instead of bundling every screen into the first page load. This exists to make applications start faster, reduce the initial JavaScript bundle, and improve the user experience, especially in enterprise apps with many dashboards, admin pages, reports, and settings areas. In real projects, a user may log in and visit only a few areas, so loading every feature upfront wastes bandwidth and time. Angular commonly uses lazy loading with the router, where navigating to a path triggers downloading the related feature module. There are two common organizational patterns you will see: eagerly loaded modules, which are available at startup, and lazy loaded feature modules, which are fetched on demand. In newer Angular applications, lazy loading can also be applied to standalone route components, but module-based lazy loading remains important because many business apps still use NgModules and route-based feature separation.
To use lazy loading, you normally create a feature module with its own routing module, then register that feature in the root routes with loadChildren. Angular waits until the route is visited, then imports the module dynamically. This keeps the root app lighter and encourages clearer boundaries between features such as admin, billing, users, and analytics.
First, create a feature module such as OrdersModule and an accompanying OrdersRoutingModule. Inside the feature routing file, define child routes for components belonging only to that feature. Next, in the main application routing file, add a route like { path: 'orders', loadChildren: () => import('./orders/orders.module').then(m => m.OrdersModule) }. This syntax uses dynamic import, so Angular generates a separate bundle for that feature. When the user visits /orders, the bundle is downloaded and the feature routes become active.
A good beginner rule is this: root routing points to features, feature routing points to components. Also remember that a lazy loaded module should usually declare only its own feature components and import shared utilities from a shared module when needed. If you need faster second-page navigation, Angular also supports preloading strategies such as PreloadAllModules, which loads lazy modules after the app becomes stable.
// app-routing.module.ts
const routes: Routes = [
{ path: '', component: HomeComponent },
{
path: 'orders',
loadChildren: () => import('./orders/orders.module').then(m => m.OrdersModule)
}
];// orders-routing.module.ts
const routes: Routes = [
{ path: '', component: OrdersListComponent },
{ path: ':id', component: OrderDetailsComponent }
];// real-world preloading
@NgModule({
imports: [RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })],
exports: [RouterModule]
})
export class AppRoutingModule {}// advanced guard on lazy route
const routes: Routes = [
{
path: 'admin',
canLoad: [AdminGuard],
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
}
];component instead of loadChildren for a feature route. Fix: use loadChildren when the goal is route-based lazy loading.AppModule. Fix: do not eagerly import the same module you want Angular to lazy load.ProductsModule and lazy load it from the root router using loadChildren.id parameter.PreloadAllModules and test navigation behavior after the first load.Build a small business portal with a home page loaded eagerly and two lazy loaded feature modules named employees and reports. Each feature should have its own routing module and at least two pages.
Protect a lazy loaded admin module so unauthorized users cannot load its code, then compare this behavior with a normal route guard applied only after the module loads.
Environment configuration in Angular is the process of preparing your machine and project so Angular applications can be created, run, tested, and built reliably. It exists because Angular depends on a toolchain, not just a single file. In real projects, developers need Node.js for package management, the Angular CLI for scaffolding and builds, a code editor, and separate configuration values for development and production. This setup is used in startups, enterprise dashboards, e-commerce frontends, and internal admin tools where teams need consistent builds across machines.
The main parts of Angular environment configuration are system setup and app-level environment files. System setup usually includes Node.js, npm, Git, and the Angular CLI. At the project level, Angular commonly uses files such as src/environments/environment.ts and environment.prod.ts to store values like API base URLs, feature flags, or debug settings. During builds, Angular can replace one file with another depending on the target configuration. This allows the same codebase to behave differently in development, staging, and production without manual edits.
First, install a stable LTS version of Node.js. Angular CLI depends on Node and npm to download packages and execute scripts. After installation, verify with node -v and npm -v.
Next, install Angular CLI globally using npm install -g @angular/cli. Verify it with ng version. Then create a new app using ng new angular-env-demo. Move into the folder and run ng serve to start the development server.
Inside the project, Angular uses configuration files such as angular.json, package.json, tsconfig.json, and the environment files. In environment files, you export plain objects. Example keys include production, apiUrl, and appName. In your code, import the environment object and read its values. For production builds, use ng build --configuration production so Angular applies the production file replacement defined in angular.json.
npm install -g @angular/cli
ng new angular-env-demo
cd angular-env-demo
ng serve// src/environments/environment.ts
export const environment = {
production: false,
apiUrl: 'http://localhost:3000/api',
appName: 'Angular Env Demo'
};
// src/environments/environment.prod.ts
export const environment = {
production: true,
apiUrl: 'https://api.example.com',
appName: 'Angular Env Demo'
};// src/app/services/config.service.ts
import { Injectable } from '@angular/core';
import { environment } from '../../environments/environment';
@Injectable({ providedIn: 'root' })
export class ConfigService {
apiUrl = environment.apiUrl;
isProduction = environment.production;
}
// src/app/app.component.ts
import { Component } from '@angular/core';
import { ConfigService } from './services/config.service';
@Component({
selector: 'app-root',
template: '{{ title }}
API: {{ api }}
'
})
export class AppComponent {
title = 'Environment Demo';
api = this.config.apiUrl;
constructor(private config: ConfigService) {}
}apiUrl, production, and appName.apiUrl and appName to your environment files and display them in a component.Build a small Angular app that displays the application name, current API URL, and whether the build is development or production by reading values from environment configuration files.
Add a custom staging configuration with its own environment file and build command, then show a visible banner in the app when the staging configuration is active.
catchError operator: This is the most common way to handle errors from observables, especially HTTP requests. When an observable emits an error, catchError intercepts it and allows you to return a new observable, throw a new error, or return a default value. This prevents the error from propagating further down the observable chain and potentially crashing your application.retry and retryWhen operators: These operators are used to automatically re-subscribe to a failed observable a specified number of times or based on custom logic, respectively. This is useful for transient network issues.ErrorHandler service: This is a global error handler that catches all uncaught exceptions thrown by the application. You can extend this service to provide custom logging, display user-friendly error messages, or send errors to an external monitoring service. This is the last line of defense for errors not handled locally.try/catch blocks are still relevant for synchronous code within components or services.catchErrorimport { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class DataService {
constructor(private http: HttpClient) { }
getUsers(): Observable {
return this.http.get('/api/users').pipe(
catchError(this.handleError)
);
}
private handleError(error: HttpErrorResponse) {
if (error.status === 0) {
// A client-side or network error occurred. Handle it accordingly.
console.error('An error occurred:', error.error);
} else {
// The backend returned an unsuccessful response code.
// The response body may contain clues as to what went wrong.
console.error(
`Backend returned code ${error.status}, ` +
`body was: ${error.error}`);
}
// Return an observable with a user-facing error message.
return throwError(() => new Error('Something bad happened; please try again later.'));
}
} ErrorHandlerimport { ErrorHandler, Injectable, Injector } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { NotificationService } from './notification.service'; // Assume this service exists
@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
constructor(private injector: Injector) { }
handleError(error: any): void {
const notificationService = this.injector.get(NotificationService);
if (error instanceof HttpErrorResponse) {
// Server-side errors
console.error('Backend error:', error.status, error.message);
notificationService.showError(`Server Error: ${error.status} - ${error.message}`);
} else {
// Client-side errors (e.g., JavaScript errors in components)
console.error('Client-side error:', error.message);
notificationService.showError('An unexpected error occurred. Please try again.');
}
// Log the error to an external service or console
console.error('Error caught by GlobalErrorHandler:', error);
}
}
// In your AppModule:
import { NgModule, ErrorHandler } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { GlobalErrorHandler } from './global-error-handler.service';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule
],
providers: [
{ provide: ErrorHandler, useClass: GlobalErrorHandler }
],
bootstrap: [AppComponent]
})
export class AppModule { }import { Injectable } from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor,
HttpErrorResponse
} from '@angular/common/http';
import { Observable, throwError, timer } from 'rxjs';
import { catchError, retryWhen, mergeMap, finalize } from 'rxjs/operators';
import { NotificationService } from './notification.service'; // Assume this service exists
@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
constructor(private notificationService: NotificationService) {}
intercept(request: HttpRequest, next: HttpHandler): Observable> {
let handled: boolean = false;
return next.handle(request).pipe(
retryWhen(errors => errors.pipe(
mergeMap((error, i) => {
const retryAttempt = i + 1;
// Retry only for specific status codes (e.g., 5xx for server errors)
if (error instanceof HttpErrorResponse && (error.status === 503 || error.status === 504) && retryAttempt <= 3) {
console.warn(`Retry attempt ${retryAttempt} for ${request.url}`);
this.notificationService.showInfo(`Retrying request (${retryAttempt}/3)...`);
return timer(1000 * retryAttempt); // Exponential backoff
}
// If not retriable, throw the error
return throwError(() => error);
})
)),
catchError((error: HttpErrorResponse) => {
let errorMessage = '';
if (error.error instanceof ErrorEvent) {
// Client-side error
errorMessage = `Error: ${error.error.message}`;
} else {
// Server-side error
errorMessage = `Error Code: ${error.status}
Message: ${error.message}`;
if (error.status === 401) {
// Handle unauthorized specifically, e.g., redirect to login
this.notificationService.showError('Session expired or unauthorized. Please log in again.');
// router.navigate(['/login']); // Example redirect
handled = true;
} else if (error.status === 404) {
this.notificationService.showError('Resource not found.');
handled = true;
}
}
console.error(errorMessage);
if (!handled) {
this.notificationService.showError('An API error occurred: ' + errorMessage.split('\n')[0]);
}
return throwError(() => new Error(errorMessage));
}),
finalize(() => {
// Perform cleanup or hide loading indicators here
console.log('Request complete (interceptor)');
})
);
}
}
// In your AppModule providers:
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { ErrorInterceptor } from './error.interceptor';
@NgModule({
// ...
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true }
],
// ...
})
export class AppModule { } catchError or subscribe Error Handlers: Beginners sometimes use catchError(() => of(null)) or an empty error callback in subscribe() without logging or informing the user. This hides critical issues, making debugging difficult and leading to a poor user experience. throwError after catchError when necessary: If you handle an error with catchError but still want to propagate it to a global error handler or a subsequent subscribe error callback, you must use throwError(() => new Error(...)). Simply returning of(null) will complete the observable successfully, effectively hiding the error.throwError.catchError in every component, an HTTP interceptor, and a global ErrorHandler can lead to errors being handled multiple times or not in the intended order. This makes the logic complex and hard to maintain.catchError in services for specific API call error transformations or retries, and the global ErrorHandler as a fallback for uncaught exceptions.catchError in Services: Apply catchError within your services to transform raw backend errors into more meaningful, application-specific error messages or to provide fallback data. This decouples error details from components.ErrorHandler for Uncaught Exceptions: Extend Angular's ErrorHandler to catch all unhandled errors. This is crucial for logging errors to a remote service (e.g., Sentry, Bugsnag) and displaying a generic, user-friendly message for unexpected failures.retry or retryWhen for Transient Errors: For operations prone to temporary network glitches (e.g., on mobile networks), use these RxJS operators strategically to improve resilience./api/non-existent). Use the catchError operator to log the HTTP error to the console and then return an observable emitting an empty array of([]) so that the component subscribed to it doesn't break.ErrorHandler from the examples to differentiate between HttpErrorResponse errors and generic JavaScript errors. For HTTP errors, log the status code and message. For other errors, log the stack trace. Display a simple alert message to the user for both types of errors.jsonplaceholder.typicode.com/users). Implement the following error handling: import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class MathService {
add(a: number, b: number): number {
return a + b;
}
subtract(a: number, b: number): number {
return a - b;
}
}
import { MathService } from './math.service';
describe('MathService', () => {
let service: MathService;
beforeEach(() => {
service = new MathService();
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should add two numbers correctly', () => {
expect(service.add(2, 3)).toBe(5);
expect(service.add(-1, 1)).toBe(0);
});
it('should subtract two numbers correctly', () => {
expect(service.subtract(5, 2)).toBe(3);
expect(service.subtract(10, 20)).toBe(-10);
});
});
import { Component, OnInit } from '@angular/core';
import { UserService } from './user.service'; // Assume this service exists
@Component({
selector: 'app-welcome',
template: '{{ welcomeMessage }}
',
})
export class WelcomeComponent implements OnInit {
welcomeMessage: string = '';
constructor(private userService: UserService) {}
ngOnInit(): void {
this.welcomeMessage = this.userService.getWelcomeMessage();
}
}
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class UserService {
getWelcomeMessage(): string {
return 'Welcome, User!';
}
}
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { WelcomeComponent } from './welcome.component';
import { UserService } from './user.service';
describe('WelcomeComponent', () => {
let component: WelcomeComponent;
let fixture: ComponentFixture;
let userServiceStub: Partial; // Use Partial for a stub
beforeEach(async () => {
// Create a stub for UserService
userServiceStub = {
getWelcomeMessage: () => 'Welcome, Test User!',
};
await TestBed.configureTestingModule({
declarations: [WelcomeComponent],
providers: [{ provide: UserService, useValue: userServiceStub }],
}).compileComponents();
fixture = TestBed.createComponent(WelcomeComponent);
component = fixture.componentInstance;
fixture.detectChanges(); // Trigger ngOnInit and data binding
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should display welcome message from UserService', () => {
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('p')?.textContent).toContain('Welcome, Test User!');
});
});
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { delay } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class DataService {
getData(): Observable {
return of('Data from server').pipe(delay(100)); // Simulate async delay
}
}
import { fakeAsync, tick } from '@angular/core/testing';
import { DataService } from './data.service';
describe('DataService', () => {
let service: DataService;
beforeEach(() => {
service = new DataService();
});
it('should fetch data asynchronously', fakeAsync(() => {
let result: string | undefined;
service.getData().subscribe(data => {
result = data;
});
expect(result).toBeUndefined(); // Data not yet arrived
tick(100); // Simulate passage of 100ms
expect(result).toBe('Data from server'); // Data should have arrived
}));
});
npm install -g @angular/clicd my-angular-appng build --configuration productionng build --proddist// directory.dist// folder will be created (or updated) in your project root. This folder contains all the static assets required to run your application: HTML, CSS, JavaScript bundles, images, etc.dist// folder can then be uploaded to any static web server (e.g., Apache, Nginx), a cloud storage service (e.g., AWS S3, Azure Blob Storage), or a hosting platform (e.g., Netlify, Vercel, Firebase Hosting). For single-page applications, it's crucial to configure your web server to redirect all unknown paths to your index.html file to enable client-side routing.ng buildng build --configuration productionnpm install -g firebase-toolsfirebase loginfirebase initdist/ and select 'Yes' for rewriting all URLs to index.html.ng build --configuration productionfirebase deploy --only hostingangular.json for different environments or needs. For example, a 'staging' configuration:// angular.json (excerpt)
{
"projects": {
"my-angular-app": {
"architect": {
"build": {
"configurations": {
"production": {
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
]
},
"staging": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.staging.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false
}
}
}
}
}
}
}ng build --configuration stagingng build without the --configuration production flag. This results in a larger, slower application with debugging tools and unoptimized code. Always use ng build --configuration production for live environments.index.html, refreshing a deep link (e.g., /users/1) will result in a 404 error. Ensure your server correctly handles fallbacks to index.html for all non-existent paths.environment.ts files (e.g., environment.staging.ts, environment.prod.ts) and that your build configuration correctly uses the right one via fileReplacements in angular.json. Failure to do so can lead to your production app connecting to a development API or using incorrect settings.--configuration production. It compiles your templates and components at build time, leading to faster rendering and smaller bundles. Avoid JIT (Just-in-Time) compilation for production builds.src/environments/environment.ts for development settings and src/environments/environment.prod.ts for production. Configure angular.json to swap these files during the build process, allowing you to manage API endpoints, feature flags, and other environment-specific variables effectively.webpack-bundle-analyzer (integrated with Angular CLI's --stats-json flag) to identify and optimize large modules or unnecessary dependencies.dist/ folders for both builds.environment.ts and environment.prod.ts with different URLs. Build the application for production and verify that the correct URL is displayed in the built output.angular.json, create a new build configuration named 'test-env' that uses a third environment file environment.test.ts. Build your application using this new configuration.Performance optimization in Angular is the process of making applications render faster, respond smoothly, and use browser resources efficiently. It exists because modern Angular apps often manage many components, API calls, lists, forms, and user interactions. Without optimization, an application may feel slow, rerender too often, consume too much memory, or delay user actions. In real-world enterprise systems such as dashboards, admin panels, ecommerce frontends, and analytics tools, performance affects user satisfaction, accessibility, SEO, and infrastructure cost.
Angular performance mainly involves change detection, rendering efficiency, bundle size, lazy loading, and reactive data flow. A common optimization type is using OnPush change detection so Angular checks a component only when its inputs change, an event occurs, or an observable emits. Another important type is list optimization with trackBy, which helps Angular reuse DOM elements instead of rebuilding them. Routing optimization often uses lazy loading so feature modules or standalone routes are loaded only when needed. Data optimization includes using RxJS operators such as debounceTime, distinctUntilChanged, and switchMap to prevent unnecessary API calls. Template optimization focuses on avoiding expensive method calls directly in HTML and preferring cached values or observables.
Start by identifying slow areas. Use Angular DevTools and browser performance tools to inspect component updates and rendering time. Next, reduce unnecessary change detection by setting a component to ChangeDetectionStrategy.OnPush. This is useful when data flows through inputs or observables. Then optimize repeated lists with trackBy so Angular can recognize stable item identities. After that, split large applications with lazy-loaded routes to reduce initial bundle size. For user input that triggers search or filtering, avoid firing requests on every keystroke; instead, use reactive forms with RxJS operators. Finally, avoid heavy logic in templates, unsubscribe safely when needed, and prefer pure pipes or computed values for repeated transformations.
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'app-counter',
template: '{{ count }}
',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
count = 0;
increment() {
this.count++;
}
}import { Component } from '@angular/core';
@Component({
selector: 'app-users',
template: '{{ user.name }} '
})
export class UsersComponent {
users = [{ id: 1, name: 'Ava' }, { id: 2, name: 'Noah' }];
trackById(index: number, user: { id: number }) {
return user.id;
}
}searchControl = new FormControl('');
results$ = this.searchControl.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(term => this.api.search(term || ''))
);const routes: Routes = [
{
path: 'admin',
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
}
];debounceTime, distinctUntilChanged, and switchMap.async pipe instead of manual subscriptions where possible.*ngFor and add a trackBy function based on product ID.ChangeDetectionStrategy.OnPush and test how input updates affect rendering.Build a small employee directory page that loads employees from a service, supports debounced search, displays results with trackBy, and lazy-loads the directory feature route.
Refactor a slow dashboard page by identifying at least three rendering bottlenecks, then improve it using OnPush, lazy loading, and observable-based UI updates.
The final project is where you bring Angular concepts together into one complete application. Instead of learning components, routing, forms, services, and HTTP in isolation, you apply them to a realistic product that looks and behaves like a professional frontend app. In real life, Angular is widely used for admin dashboards, inventory systems, HR tools, booking portals, and analytics panels. A strong final project should reflect that environment by solving a practical business problem with a clear structure. A great choice is a task management dashboard or project tracker because it naturally uses lists, detail views, forms, filters, validation, reusable components, and service-based data handling.
Your project should include key Angular building blocks: standalone or module-based components, navigation with the router, service classes for logic and data access, reactive forms for user input, HTTP calls for fetching or simulating backend data, and state updates through RxJS patterns. You should also think about user experience: loading indicators, empty states, error handling, and responsive layouts. Sub-types inside the final project usually appear as feature areas: dashboard pages, detail pages, shared UI components, data services, route guards, and form workflows. This separation mirrors real enterprise development, where code is organized by responsibility rather than placed into one large file.
Start by defining the app goal. For example, build a Project Tracker app that lets users view projects, create tasks, edit task status, and filter work by priority. Next, plan the routes such as /dashboard, /projects, /projects/:id, and /tasks/new. Then create feature components for each page and shared components like a header, task card, and loading spinner.
After the UI structure is ready, add a service to centralize task and project data. Even if you use mock data or a JSON API, keep all fetch, create, update, and delete logic in the service. Then build reactive forms with validation rules such as required fields, minimum length, and valid due dates. Connect the form submit action to service methods. Finally, improve the app with filtering, sorting, route parameters, and clear user feedback messages.
import { Component } from '@angular/core';
@Component({
selector: 'app-dashboard',
template: `
Project Dashboard
Welcome to the final project.
`
})
export class DashboardComponent {}import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class TaskService {
constructor(private http: HttpClient) {}
getTasks(): Observable {
return this.http.get('https://api.example.com/tasks');
}
} import { Component, OnInit } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import { TaskService } from './task.service';
@Component({
selector: 'app-task-form',
template: `
`
})
export class TaskFormComponent implements OnInit {
taskForm = this.fb.group({
title: ['', [Validators.required, Validators.minLength(3)]],
priority: ['medium', Validators.required]
});
constructor(private fb: FormBuilder, private taskService: TaskService) {}
ngOnInit(): void {}
submit(): void {
if (this.taskForm.valid) {
console.log(this.taskForm.value);
}
}
}Build a Project Tracker application where users can create tasks, view all projects, filter tasks by priority, and navigate to a project detail page.
Extend the project by adding status-based analytics, such as total completed tasks, overdue items, and a progress percentage for each project.