This project was generated with Angular CLI version 18.2.7.
For the full code and examples, refer to the GitHub repository.
- Introduction
- Prerequisites
- What is Angular
- Angular Cookbook (Latest Edition)
- Benefits of Angular
- Angular vs React
- Myths about Angular
- Creating an Angular Application
- Angular Components
- Creating Angular Components
- Data-Binding in Angular
- Passing Data from Parent to Child Component
- Event Listeners in Angular
- Creating a Counter Component
- Creating Routes in Angular
- Angular Services
- Making HTTP Calls in Angular Services
- Angular Directives
- Angular Pipes
- Outro
Angular development began in 2013 and has evolved into one of the most robust frameworks available today. This guide is inspired by the best practices and tools used by seasoned Angular experts, including content creators like John Papa and the Angular community. Whether you're a seasoned developer or a newcomer, this guide aims to provide a comprehensive overview of Angular.
Before diving in, ensure you are familiar with the following:
- Basics of HTML, CSS, JavaScript, and TypeScript.
- Git and basic programming concepts (loops, variables, functions, etc.).
- Installed tools:
Angular is a TypeScript-based open-source framework developed by Google, primarily for building dynamic, single-page web applications. It's characterized by:
- Support for single-page applications (SPAs).
- A large, active community and extensive documentation.
- Usage in several Google applications (e.g., Google Analytics).
The latest Angular Cookbook provides over 90 practical projects. Key topics include:
- Component communication
- Directives and control flows
- RxJS and NgRx for state management
- Routing, forms, animations
- Unit testing and performance optimization
Angular provides the following benefits:
- Faster development with less code.
- Built-in unit testing.
- Consistent coding standards due to its opinionated structure.
- Enhanced reusability through components, services, and pipes.
- Comprehensive CLI tools for faster code generation and deployment.
Feature | Angular | React |
---|---|---|
Type | Framework | Library |
CLI | Built-in | Requires additional tools |
Built-in tools | Many (e.g., forms, HTTP) | Few (requires external libraries) |
Opinionated | Yes | No |
- Hard to learn: Not true; modern Angular has extensive documentation and is user-friendly.
- Frequent breaking changes: Angular's biannual updates are mostly incremental.
- Slow performance: Recent benchmarks show Angular performs on par with Vue.js and React.
Use Angular CLI to create and manage projects efficiently:
- Ensure Node.js is installed. Verify with
node -v
andnpm -v
in your terminal. - Install the Angular CLI globally:
npm install -g @angular/[email protected] # OR npm install -g @angular/cli npm install -g @angular/[email protected]
- Verify the installation:
ng --version
- Create a new Angular project:
ng new first-ng-app
- Navigate to your project directory and serve the application:
Visit
cd first-ng-app ng serve
http://localhost:4200
to see your app running.
This generates a project structure with essential files. Use ng serve
to run the development server.
Components are the building blocks of Angular applications. A component consists of:
- TypeScript: Logic and data binding.
- HTML: The template.
- SCSS: Styles scoped to the component.
Run the Angular CLI command:
ng g c header # short form
ng generate component header # full form
# creates inside the `src/app` folder
# OR (in a nested directory)
ng g c components/header
# creates HeaderComponent
# inside the `src/app/components` folder
ng g c home
# creates the HomeComponent
This generates the component files:
header.component.ts
(logic)header.component.html
(template)header.component.scss
(styles)
//first-ng-app/src/app/components/header/header.component.html
HTML:
<h1>My first Angular app</h1>
first-ng-app/src/app/components/header/header.component.ts
TypeScript:
import { Component } from '@angular/core';
@Component({
selector: 'app-header',
standalone: true,
imports: [],
templateUrl: './header.component.html',
styleUrl: './header.component.scss',
})
export class HeaderComponent {
}
For a complete implementation, refer to the header component code in the GitHub repository.
Binding data between the TypeScript class of the component, and the component's template.
Data Binding with Modern Angular (with Signals):
- A signal is a wrapper around a value that notifies interested consumers when that value changes. Signals can contain any value, from primitives to complex data structures.
//first-ng-app/src/app/components/header/header.component.html
HTML:
- <h1>My first Angular app</h1>
+ <h1>{{ title() }}</h1>
//first-ng-app/src/app/components/header/header.component.ts
TypeScript:
- + import { Component } from '@angular/core';
+ import { Component, signal } from '@angular/core';
@Component({
selector: 'app-header',
standalone: true,
imports: [],
templateUrl: './header.component.html',
styleUrl: './header.component.scss',
})
export class HeaderComponent {
+ title = signal('My first Angular app');
}
Data Binding without Signals (traditional way):
//first-ng-app/src/app/components/header/header.component.html
HTML:
- <h1>My first Angular app</h1>
+ <h1>{{ title }}</h1>
//first-ng-app/src/app/components/header/header.component.ts
TypeScript:
import { Component } from '@angular/core';
@Component({
selector: 'app-header',
standalone: true,
imports: [],
templateUrl: './header.component.html',
styleUrl: './header.component.scss',
})
export class HeaderComponent {
+ title = 'My first Angular app';
}
Angular supports:
- One-way data binding: Display data in templates.
<p>{{ title }}</p>
- Two-way data binding: Sync data between the model and view.
<input [(ngModel)]="title" />
Passing data from parent to child component via Inputs. When you use a component, you commonly want to pass some data to it. A component specifies the data that it accepts by declaring inputs. We'll pass the greeting message from the AppComponent.
ng g c home
# generates in `src/app`
ng g c components/greeting
# generates in `src/app/components`
//first-ng-app/src/app/components/header/header.component.html
HTML:
+ <header>
+ <nav>
+ {{ title }}
+ </nav>
+ </header>
//first-ng-app/src/app/components/header/header.component.ts
TypeScript:
import { Component } from '@angular/core';
@Component({
selector: 'app-header',
standalone: true,
imports: [],
templateUrl: './header.component.html',
styleUrl: './header.component.scss',
})
export class HeaderComponent {
+ title = 'My first Angular app';
}
//first-ng-app/src/app/components/greeting/greeting.component.html
HTML:
<h2>Greetings!</h2>
+ <p>{{ message() }}</p>
//first-ng-app/src/app/components/greeting/greeting.component.ts
TypeScript:
- + import { Component } from '@angular/core';
+ import { Component, input } from '@angular/core';
@Component({
selector: 'app-greeting',
standalone: true,
imports: [],
templateUrl: './greeting.component.html',
styleUrl: './greeting.component.scss',
})
export class GreetingComponent {
+ message = input('Hello hello!');
}
//first-ng-app/src/app/home/home.component.html
HTML:
+ <app-greeting [message]="homeMessage()" />
//first-ng-app/src/app/home/home.component.ts
TypeScript:
- + import { Component } from '@angular/core';
+ import { Component, signal } from '@angular/core';
+ import { GreetingComponent } from '../components/greeting/greeting.component';
@Component({
selector: 'app-home',
standalone: true,
- imports: [],
+ imports: [GreetingComponent],
templateUrl: './home.component.html',
styleUrl: './home.component.scss',
})
export class HomeComponent {
+ homeMessage = signal('Hello, world!');
}
//first-ng-app/src/app/app.component.html
HTML:
+ <app-header />
//first-ng-app/src/app/app.component.ts
TypeScript:
import { Component } from '@angular/core';
+ import { HeaderComponent } from './components/header/header.component';
@Component({
selector: 'app-root',
standalone: true,
- imports: [],
+ imports: [HeaderComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
})
export class AppComponent {
+ title = 'first-ng-app';
}
//first-ng-app/src/app/home/home.component.html
HTML:
<app-greeting [message]="homeMessage()" />
+ <input
+ placeholder="Type something..."
+ type="text"
+ (keyup)="keyUpHandler($event)"
+ />
//first-ng-app/src/app/home/home.component.ts
TypeScript:
import { Component, signal } from '@angular/core';
import { GreetingComponent } from '../components/greeting/greeting.component';
@Component({
selector: 'app-home',
standalone: true,
imports: [GreetingComponent],
templateUrl: './home.component.html',
styleUrl: './home.component.scss',
})
export class HomeComponent {
homeMessage = signal('Hello, world!');
+
+ keyUpHandler(event: KeyboardEvent) {
+ console.log(`user pressed the ${event.key} key`);
+ }
}
Build a counter with increment, decrement, and reset functionality:
ng g c components/counter
//first-ng-app/src/app/components/counter/counter.component.html
HTML:
+ <p>Counter value: {{ counterValue() }}</p>
+
+ <div>
+ <button (click)="increment()">Increment</button>
+ <button (click)="reset()">Reset</button>
+ <button (click)="decrement()">Decrement</button>
+ </div>
//first-ng-app/src/app/components/counter/counter.component.ts
TypeScript:
- import { Component } from '@angular/core';
+ import { Component, signal } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
imports: [],
templateUrl: './counter.component.html',
styleUrl: './counter.component.scss',
})
export class CounterComponent {
+ counterValue = signal(0);
+ increment() {
+ this.counterValue.update((val) => val + 1);
+ }
+
+ decrement() {
+ this.counterValue.update((val) => val - 1);
+ }
+
+ reset() {
+ this.counterValue.set(0);
+ }
}
//first-ng-app/src/app/home/home.component.html
HTML:
<app-greeting [message]="homeMessage()" />
+ <app-counter />
<input
placeholder="Type something..."
type="text"
(keyup)="keyUpHandler($event)"
/>
//first-ng-app/src/app/home/home.component.ts
TypeScript:
import { Component, signal } from '@angular/core';
import { GreetingComponent } from '../components/greeting/greeting.component';
+ import { CounterComponent } from '../components/counter/counter.component';
@Component({
selector: 'app-home',
standalone: true,
- imports: [GreetingComponent],
+ imports: [GreetingComponent, CounterComponent],
templateUrl: './home.component.html',
styleUrl: './home.component.scss',
})
export class HomeComponent {
homeMessage = signal('Hello, world!');
keyUpHandler(event: KeyboardEvent) {
console.log(`user pressed the ${event.key} key`);
}
}
Angular is a single page application. Using routes, you can still define different pages that the user can navigate to. The browser only loads the bundles related to the route user has accessed. This significantly improves the performance of the app, and user experience.
Let's create a new route. Create another component (as a page) for the route:
ng g c todos
# this will be the page for todos' list
Create a component for each todo item:
ng g c components/todo-item
//first-ng-app/src/app/app.routes.ts
TypeScript:
import { Routes } from '@angular/router';
- export const routes: Routes = [];
+ export const routes: Routes = [
+ {
+ path: '',
+ pathMatch: 'full',
+ loadComponent: () => {
+ return import('./home/home.component').then((m) => m.HomeComponent);
+ },
+ },
+ {
+ path: 'todos',
+ loadComponent: () => {
+ return import('./todos/todos.component').then((m) => m.TodosComponent);
+ },
+ },
+ ];
//first-ng-app/src/app/app.component.html
HTML:
<app-header />
+ <main>
+ <router-outlet />
+ </main>
//first-ng-app/src/app/app.component.ts
TypeScript:
import { Component } from '@angular/core';
+ import { RouterOutlet } from '@angular/router';
import { HeaderComponent } from './components/header/header.component';
@Component({
selector: 'app-root',
standalone: true,
- imports: [HeaderComponent],
+ imports: [RouterOutlet, HeaderComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
})
export class AppComponent {
title = 'first-ng-app';
}
//first-ng-app/src/app/components/header/header.component.html
HTML:
<header>
<nav>
- {{ title }}
+ <span routerLink="/">{{ title }}</span>
+ <ul>
+ <li routerLink="/todos">Todos</li>
+ </ul>
</nav>
</header>
//first-ng-app/src/app/components/header/header.component.ts
TypeScript:
import { Component } from '@angular/core';
+ import { RouterLink } from '@angular/router';
@Component({
selector: 'app-header',
standalone: true,
- imports: [],
+ imports: [RouterLink],
templateUrl: './header.component.html',
styleUrl: './header.component.scss',
})
export class HeaderComponent {
title = 'My first Angular app';
}
Visit http://localhost:4200
to see Home page.
Visit http://localhost:4200/todos
to see Todos page.
Explore the complete routing setup in the repository’s source code.
Angular Services are used to encapsulate data, making HTTP calls, or performing any task that is not related directly to data rendering (in my opinion).
Creating an Angular Service:
ng g service services/todos
# creates todos.service.ts inside `src/app/services`
- Provide HTTP module/providers in the app config using provideHttpClient()
- Inject the HttpClient service
- Use the http methods
//first-ng-app/src/app/model/todo.type.ts
TypeScript:
+ export type Todo = {
+ userId: number;
+ completed: boolean;
+ title: string;
+ id: number;
+ };
//first-ng-app/src/app/services/todos.service.ts
TypeScript:
import { inject, Injectable } from '@angular/core';
+ import { Todo } from '../model/todo.type';
+ import { HttpClient } from '@angular/common/http';
@Injectable({
providedIn: 'root',
})
export class TodosService {
-
- constructor() {}
+ http = inject(HttpClient);
+ getTodosFromApi() {
+ const url = `https://jsonplaceholder.typicode.com/todos`;
+ return this.http.get<Array<Todo>>(url);
+ }
}
//first-ng-app/src/app/components/todo-item/todo-item.component.html
HTML:
+ <li [isCompleted]="todo().completed" >
+ <input
+ id="todo-{{ todo().id }}"
+ type="checkbox"
+ [checked]="todo().completed"
+ (change)="this.todoClicked()"
+ />
+ <label for="todo-{{ todo().id }}">{{ todo().title }}</label>
+ </li>
//first-ng-app/src/app/components/todo-item/todo-item.component.ts
TypeScript:
- import { Component } from '@angular/core';
+ import { Component, input, output } from '@angular/core';
+ import { Todo } from '../../model/todo.type';
@Component({
selector: 'app-todo-item',
standalone: true,
imports: [],
templateUrl: './todo-item.component.html',
styleUrl: './todo-item.component.scss',
})
export class TodoItemComponent {
+ todo = input.required<Todo>();
+ todoToggled = output<Todo>();
+
+ todoClicked() {
+ this.todoToggled.emit(this.todo());
+ }
}
//first-ng-app/src/app/todos/todos.component.html
HTML:
+ <h3>Todos List</h3>
+
+ <ul>
+ <app-todo-item (todoToggled)="updateTodoItem($event)" [todo]="todoItems()[0]" />
+ </ul>
//first-ng-app/src/app/todos/todos.component.ts
TypeScript:
- import { Component } from '@angular/core';
+ import { Component, inject, OnInit, signal } from '@angular/core';
+ import { TodosService } from '../services/todos.service';
+ import { Todo } from '../model/todo.type';
+ import { TodoItemComponent } from '../components/todo-item/todo-item.component';
@Component({
selector: 'app-todos',
standalone: true,
- imports: [],
+ imports: [TodoItemComponent],
templateUrl: './todos.component.html',
styleUrl: './todos.component.scss',
})
export class TodosComponent implements OnInit {
+ todoService = inject(TodosService);
+ todoItems = signal<Array<Todo>>([]);
+
+ ngOnInit(): void {
+ this.todoService
+ .getTodosFromApi()
+ .subscribe((todos) => {
+ this.todoItems.set(todos);
+ });
+ }
+
+ updateTodoItem(todoItem: Todo) {
+ this.todoItems.update((todos) => {
+ return todos.map((todo) => {
+ if (todo.id === todoItem.id) {
+ return {
+ ...todo,
+ completed: !todo.completed,
+ };
+ }
+ return todo;
+ });
+ });
+ }
}
//first-ng-app/src/app/app.config.ts
TypeScript:
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { provideHttpClient } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
+ provideHttpClient(),
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
],
};
Angular Directives allow you to add additional behavior to elements in our Angular applications.
- Components
- Attribute directives
[style.backgroundColor]
- Structural directives
*ngIf
or@if
,*ngFor
or@for
Let's create an Angular directive for completed todos:
ng g directive directives/highlight-completed-todo
//first-ng-app/src/app/todos/todos.component.ts
HTML:
<h3>Todos List</h3>
+ @if (!todoItems().length) {
+ <p>Loading...</p>
+ }
+
<ul>
- <app-todo-item (todoToggled)="updateTodoItem($event)" [todo]="todoItems()[0]" />
+ @for (todo of todoItems(); track todo.id) {
+ <app-todo-item (todoToggled)="updateTodoItem($event)" [todo]="todo" />
+ }
</ul>
//first-ng-app/src/app/directives/highlight-completed-todo.directive.ts
TypeScript:
- import { Directive } from '@angular/core';
+ import { Directive, input, effect, inject, ElementRef } from '@angular/core';
@Directive({
selector: '[appHighlightCompletedTodo]',
standalone: true,
})
export class HighlightCompletedTodoDirective {
-
- constructor() {}
+ isCompleted = input(false);
+ el = inject(ElementRef);
+ stylesEffect = effect(() => {
+ if (this.isCompleted()) {
+ this.el.nativeElement.style.textDecoration = 'line-through';
+ this.el.nativeElement.style.backgroundColor = '#d3f9d8';
+ this.el.nativeElement.style.color = '#6c757d';
+ } else {
+ this.el.nativeElement.style.textDecoration = 'none';
+ this.el.nativeElement.style.backgroundColor = '#fff';
+ this.el.nativeElement.style.color = '#000';
+ }
+ });
}
//first-ng-app/src/app/components/todo-item/todo-item.component.html
HTML:
- <li [isCompleted]="todo().completed" >
+ <li appHighlightCompletedTodo [isCompleted]="todo().completed" >
<input
id="todo-{{ todo().id }}"
type="checkbox"
[checked]="todo().completed"
(change)="this.todoClicked()"
/>
<label for="todo-{{ todo().id }}">{{ todo().title }}</label>
</li>
//first-ng-app/src/app/components/todo-item/todo-item.component.ts
TypeScript:
import { Component, input, output } from '@angular/core';
import { Todo } from '../../model/todo.type';
+ import { HighlightCompletedTodoDirective } from '../../directives/highlight-completed-todo.directive';
@Component({
selector: 'app-todo-item',
standalone: true,
- imports: [],
+ imports: [HighlightCompletedTodoDirective],
templateUrl: './todo-item.component.html',
styleUrl: './todo-item.component.scss',
})
export class TodoItemComponent {
todo = input.required<Todo>();
todoToggled = output<Todo>();
todoClicked() {
this.todoToggled.emit(this.todo());
}
}
Angular pipes are used to transform data right in the templates.
Let's create a todos filter pipe:
ng g pipe pipes/filter-todos
//first-ng-app/src/app/pipes/filter-todos.pipe.ts
TypeScript:
import { Pipe, PipeTransform } from '@angular/core';
+ import { Todo } from '../model/todo.type';
@Pipe({
name: 'filterTodos',
standalone: true,
})
export class FilterTodosPipe implements PipeTransform {
- transform(value: , ...args: unknown[]) unknown {
+ transform(todos: Todo[], searchTerm: string): Todo[] {
+ if (!searchTerm) {
+ return todos;
+ }
+ const text = searchTerm.toLowerCase();
+ return todos.filter((todo) => {
+ return todo.title.toLowerCase().includes(text);
+ });
}
}
//first-ng-app/src/app/components/todo-item/todo-item.component.html
HTML:
<li appHighlightCompletedTodo [isCompleted]="todo().completed" >
<input
id="todo-{{ todo().id }}"
type="checkbox"
[checked]="todo().completed"
(change)="this.todoClicked()"
/>
- <label for="todo-{{ todo().id }}">{{ todo().title }}</label>
+ <label for="todo-{{ todo().id }}">{{ todo().title | uppercase }}</label>
</li>
//first-ng-app/src/app/components/todo-item/todo-item.component.ts
TypeScript:
import { Component, input, output } from '@angular/core';
import { Todo } from '../../model/todo.type';
import { HighlightCompletedTodoDirective } from '../../directives/highlight-completed-todo.directive';
+ import { UpperCasePipe } from '@angular/common';
@Component({
selector: 'app-todo-item',
standalone: true,
- imports: [HighlightCompletedTodoDirective],
+ imports: [HighlightCompletedTodoDirective, UpperCasePipe],
templateUrl: './todo-item.component.html',
styleUrl: './todo-item.component.scss',
})
export class TodoItemComponent {
todo = input.required<Todo>();
todoToggled = output<Todo>();
todoClicked() {
this.todoToggled.emit(this.todo());
}
}
//first-ng-app/src/app/todos/todos.component.html
HTML:
<h3>Todos List</h3>
@if (!todoItems().length) {
<p>Loading...</p>
}
+ <form>
+ <label>Filter Todos</label>
+ <input
+ name="searchTerm"
+ [(ngModel)]="searchTerm"
+ placeholder="Search todos..."
+ />
+ </form>
+
<ul class="todos">
- @for (todo of todoItems(); track todo.id) {
+ @for (todo of todoItems() | filterTodos : searchTerm(); track todo.id) {
<app-todo-item (todoToggled)="updateTodoItem($event)" [todo]="todo" />
}
</ul>
//first-ng-app/src/app/todos/todos.component.ts
TypeScript:
import { Component, inject, OnInit, signal } from '@angular/core';
import { TodosService } from '../services/todos.service';
import { Todo } from '../model/todo.type';
+ import { catchError } from 'rxjs';
import { TodoItemComponent } from '../components/todo-item/todo-item.component';
+ import { FormsModule } from '@angular/forms';
+ import { FilterTodosPipe } from '../pipes/filter-todos.pipe';
@Component({
selector: 'app-todos',
standalone: true,
- imports: [TodoItemComponent],
+ imports: [TodoItemComponent, FormsModule, FilterTodosPipe],
templateUrl: './todos.component.html',
styleUrl: './todos.component.scss',
})
export class TodosComponent implements OnInit {
todoService = inject(TodosService);
todoItems = signal<Array<Todo>>([]);
+ searchTerm = signal('');
ngOnInit(): void {
this.todoService
.getTodosFromApi()
+ .pipe(
+ catchError((err) => {
+ console.log(err);
+ throw err;
+ })
+ )
.subscribe((todos) => {
this.todoItems.set(todos);
});
}
updateTodoItem(todoItem: Todo) {
this.todoItems.update((todos) => {
return todos.map((todo) => {
if (todo.id === todoItem.id) {
return {
...todo,
completed: !todo.completed,
};
}
return todo;
});
});
}
}
This guide captures the essence of Angular development. Explore more advanced topics and refine your skills through practical projects and community interaction.