Learn how to build modern web applications with Angular, one of the most powerful and feature-rich JavaScript frameworks. ๐
Getting Started ๐ฏ
First, let's set up our development environment and create our first Angular application.
# Install Angular CLI
npm install -g @angular/cli
# Create new project
ng new my-first-app
cd my-first-app
# Start development server
ng serve
Project Structure ๐
my-first-app/
โโโ src/
โ โโโ app/
โ โ โโโ app.component.ts
โ โ โโโ app.component.html
โ โ โโโ app.component.css
โ โ โโโ app.module.ts
โ โโโ assets/
โ โโโ index.html
โโโ angular.json
โโโ package.json
Components ๐งฉ
Creating a Component
ng generate component todo-list
// src/app/todo-list/todo-list.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-todo-list',
template: `
<div class="todo-list">
<h2>{{ title }}</h2>
<ul>
<li *ngFor="let todo of todos">
{{ todo.text }}
</li>
</ul>
</div>
`,
styles: [`
.todo-list {
max-width: 500px;
margin: 0 auto;
}
`]
})
export class TodoListComponent {
title = 'My Todo List';
todos = [
{ id: 1, text: 'Learn Angular' },
{ id: 2, text: 'Build an app' }
];
}
Data Binding ๐
Two-way Binding
// src/app/todo-form/todo-form.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-todo-form',
template: `
<div class="todo-form">
<input
[(ngModel)]="newTodo"
(keyup.enter)="addTodo()"
placeholder="Add new todo"
>
<button (click)="addTodo()">Add</button>
</div>
`
})
export class TodoFormComponent {
newTodo = '';
addTodo() {
if (this.newTodo.trim()) {
// Emit event to parent
this.newTodo = '';
}
}
}
Services ๐ง
Creating a Service
ng generate service todo
// src/app/todo.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
interface Todo {
id: number;
text: string;
completed: boolean;
}
@Injectable({
providedIn: 'root'
})
export class TodoService {
private apiUrl = 'https://api.example.com/todos';
constructor(private http: HttpClient) {}
getTodos(): Observable<Todo[]> {
return this.http.get<Todo[]>(this.apiUrl);
}
addTodo(todo: Omit<Todo, 'id'>): Observable<Todo> {
return this.http.post<Todo>(this.apiUrl, todo);
}
updateTodo(id: number, todo: Partial<Todo>): Observable<Todo> {
return this.http.patch<Todo>(`${this.apiUrl}/${id}`, todo);
}
deleteTodo(id: number): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/${id}`);
}
}
Dependency Injection ๐
// src/app/todo-list/todo-list.component.ts
import { Component, OnInit } from '@angular/core';
import { TodoService } from '../todo.service';
@Component({
selector: 'app-todo-list',
template: `
<div *ngIf="loading">Loading...</div>
<div *ngIf="error">{{ error }}</div>
<div *ngIf="!loading && !error">
<ul>
<li *ngFor="let todo of todos">
<input
type="checkbox"
[checked]="todo.completed"
(change)="toggleTodo(todo)"
>
{{ todo.text }}
<button (click)="deleteTodo(todo.id)">Delete</button>
</li>
</ul>
</div>
`
})
export class TodoListComponent implements OnInit {
todos: Todo[] = [];
loading = true;
error: string | null = null;
constructor(private todoService: TodoService) {}
ngOnInit() {
this.loadTodos();
}
loadTodos() {
this.todoService.getTodos().subscribe({
next: (todos) => {
this.todos = todos;
this.loading = false;
},
error: (err) => {
this.error = 'Failed to load todos';
this.loading = false;
}
});
}
toggleTodo(todo: Todo) {
this.todoService
.updateTodo(todo.id, {
completed: !todo.completed
})
.subscribe();
}
deleteTodo(id: number) {
this.todoService
.deleteTodo(id)
.subscribe(() => {
this.todos = this.todos.filter(t => t.id !== id);
});
}
}
Routing ๐ฃ๏ธ
// src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{ path: '', redirectTo: '/todos', pathMatch: 'full' },
{ path: 'todos', component: TodoListComponent },
{ path: 'todos/:id', component: TodoDetailComponent },
{ path: '**', component: NotFoundComponent }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
Forms ๐
Template-driven Forms
// src/app/todo-form/todo-form.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-todo-form',
template: `
<form #todoForm="ngForm" (ngSubmit)="onSubmit(todoForm)">
<div>
<label for="text">Todo Text:</label>
<input
id="text"
name="text"
[(ngModel)]="todo.text"
required
minlength="3"
#text="ngModel"
>
<div *ngIf="text.invalid && text.touched">
<div *ngIf="text.errors?.['required']">
Text is required
</div>
<div *ngIf="text.errors?.['minlength']">
Text must be at least 3 characters
</div>
</div>
</div>
<div>
<label>
<input
type="checkbox"
name="priority"
[(ngModel)]="todo.priority"
>
High Priority
</label>
</div>
<button type="submit" [disabled]="todoForm.invalid">
Add Todo
</button>
</form>
`
})
export class TodoFormComponent {
todo = {
text: '',
priority: false
};
onSubmit(form: NgForm) {
if (form.valid) {
console.log('Form submitted:', this.todo);
form.resetForm();
}
}
}
Reactive Forms
// src/app/todo-form/todo-form.component.ts
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-todo-form',
template: `
<form [formGroup]="todoForm" (ngSubmit)="onSubmit()">
<div>
<label for="text">Todo Text:</label>
<input id="text" formControlName="text">
<div *ngIf="text.invalid && text.touched">
<div *ngIf="text.errors?.['required']">
Text is required
</div>
<div *ngIf="text.errors?.['minlength']">
Text must be at least 3 characters
</div>
</div>
</div>
<div>
<label>
<input
type="checkbox"
formControlName="priority"
>
High Priority
</label>
</div>
<button type="submit" [disabled]="todoForm.invalid">
Add Todo
</button>
</form>
`
})
export class TodoFormComponent {
todoForm: FormGroup;
constructor(private fb: FormBuilder) {
this.todoForm = this.fb.group({
text: ['', [Validators.required, Validators.minLength(3)]],
priority: [false]
});
}
get text() {
return this.todoForm.get('text');
}
onSubmit() {
if (this.todoForm.valid) {
console.log('Form submitted:', this.todoForm.value);
this.todoForm.reset();
}
}
}
HTTP Interceptors ๐
// src/app/auth.interceptor.ts
import { Injectable } from '@angular/core';
import {
HttpInterceptor,
HttpRequest,
HttpHandler,
HttpEvent
} from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
intercept(
request: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
const token = localStorage.getItem('token');
if (token) {
const cloned = request.clone({
headers: request.headers.set(
'Authorization',
`Bearer ${token}`
)
});
return next.handle(cloned);
}
return next.handle(request);
}
}
State Management ๐ฆ
// src/app/store/todo.state.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
interface TodoState {
todos: Todo[];
loading: boolean;
error: string | null;
}
@Injectable({
providedIn: 'root'
})
export class TodoStore {
private state = new BehaviorSubject<TodoState>({
todos: [],
loading: false,
error: null
});
readonly todos$ = this.state.asObservable();
setState(newState: Partial<TodoState>) {
this.state.next({
...this.state.value,
...newState
});
}
addTodo(todo: Todo) {
const current = this.state.value;
this.setState({
todos: [...current.todos, todo]
});
}
removeTodo(id: number) {
const current = this.state.value;
this.setState({
todos: current.todos.filter(t => t.id !== id)
});
}
}
Testing ๐งช
// src/app/todo-list/todo-list.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TodoListComponent } from './todo-list.component';
import { TodoService } from '../todo.service';
import { of } from 'rxjs';
describe('TodoListComponent', () => {
let component: TodoListComponent;
let fixture: ComponentFixture<TodoListComponent>;
let todoService: jasmine.SpyObj<TodoService>;
beforeEach(async () => {
const spy = jasmine.createSpyObj('TodoService', ['getTodos']);
spy.getTodos.and.returnValue(of([]));
await TestBed.configureTestingModule({
declarations: [ TodoListComponent ],
providers: [
{ provide: TodoService, useValue: spy }
]
}).compileComponents();
todoService = TestBed.inject(TodoService) as jasmine.SpyObj<TodoService>;
});
beforeEach(() => {
fixture = TestBed.createComponent(TodoListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should load todos on init', () => {
const todos = [
{ id: 1, text: 'Test Todo', completed: false }
];
todoService.getTodos.and.returnValue(of(todos));
component.ngOnInit();
expect(component.todos).toEqual(todos);
expect(component.loading).toBeFalse();
});
});
Best Practices ๐
- Follow Angular style guide
- Use TypeScript features
- Implement proper error handling
- Use lazy loading for modules
- Optimize change detection
- Write maintainable code
- Use proper dependency injection
- Implement proper testing
- Follow accessibility guidelines
- Optimize performance
Additional Resources
Angular provides a robust framework for building large-scale applications. Its comprehensive feature set and strong conventions make it an excellent choice for enterprise applications. As you continue learning, explore more advanced features and best practices to build even better applications.