Introduction to Angular
Angular is a powerful, TypeScript-based front-end framework developed and maintained by Google. It enables developers to build dynamic, single-page web applications (SPAs) through a component-based architecture and powerful features like dependency injection, reactive programming, and comprehensive tooling. Angular’s opinionated structure helps teams maintain consistent code organization across large projects.
Core Angular Concepts
Key Building Blocks
Component | Description |
---|---|
Components | Basic building blocks that control views and include an HTML template, TypeScript class, and CSS styles |
Modules | Containers that organize related components, directives, pipes, and services |
Services | Reusable classes for data handling, business logic, and external interactions |
Directives | DOM manipulation tools that extend HTML with custom behaviors |
Pipes | Simple functions to transform display values within templates |
Templates | HTML with Angular-specific syntax for binding data and handling events |
Dependency Injection | Design pattern used to increase efficiency and modularity |
Component Lifecycle Hooks
ngOnChanges()
: Responds when Angular sets/resets data-bound input propertiesngOnInit()
: Initializes component after first ngOnChangesngDoCheck()
: Custom change detectionngAfterContentInit()
: After content projectionngAfterContentChecked()
: After checking projected contentngAfterViewInit()
: After initializing component’s viewsngAfterViewChecked()
: After checking component’s viewsngOnDestroy()
: Before instance destruction
Getting Started with Angular
Setting Up a New Project
# Install Angular CLI
npm install -g @angular/cli
# Create new project
ng new my-app-name
# Run development server
cd my-app-name
ng serve
Project Structure
my-app/
├── node_modules/
├── src/
│ ├── app/
│ │ ├── app.component.ts|html|css|spec.ts
│ │ └── app.module.ts
│ ├── assets/
│ ├── environments/
│ ├── favicon.ico
│ ├── index.html
│ ├── main.ts
│ ├── polyfills.ts
│ ├── styles.css
│ └── test.ts
├── angular.json
├── package.json
├── tsconfig.json
└── other config files...
Common CLI Commands
# Generate components
ng generate component component-name
ng g c component-name
# Generate services, modules, etc.
ng g service service-name
ng g module module-name
ng g directive directive-name
ng g pipe pipe-name
ng g class class-name
ng g interface interface-name
ng g enum enum-name
ng g guard guard-name
# Build for production
ng build --prod
# Run tests
ng test
ng e2e
Data Binding and Templates
Types of Data Binding
Type | Syntax | Description |
---|---|---|
Interpolation | {{ expression }} | One-way binding to display component data in templates |
Property Binding | [property]="expression" | One-way binding from component to HTML element property |
Event Binding | (event)="handler()" | One-way binding from HTML element to component |
Two-way Binding | [(ngModel)]="property" | Two-way binding between component and template |
Common Directives
Structural Directives
*ngIf="condition"
– Conditionally creates/removes elements*ngFor="let item of items"
– Creates elements for each item in a collection*ngSwitch
– Switches between alternative views
<div *ngIf="isVisible">Shown if true</div>
<ul>
<li *ngFor="let item of items; let i = index">
{{i}} - {{item.name}}
</li>
</ul>
<div [ngSwitch]="condition">
<div *ngSwitchCase="'case1'">Case 1</div>
<div *ngSwitchCase="'case2'">Case 2</div>
<div *ngSwitchDefault>Default case</div>
</div>
Attribute Directives
[ngClass]
– Adds/removes CSS classes[ngStyle]
– Sets inline styles[ngModel]
– Two-way data binding (requires FormsModule)
<div [ngClass]="{'active': isActive, 'disabled': isDisabled}">Content</div>
<div [ngStyle]="{'color': textColor, 'font-size': fontSize + 'px'}">Styled text</div>
<input [(ngModel)]="name" placeholder="Name">
Services and Dependency Injection
Creating a Service
// user.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root' // Service available application-wide
})
export class UserService {
private users = [...];
getUsers() {
return this.users;
}
}
Injecting Services
// user-list.component.ts
import { Component } from '@angular/core';
import { UserService } from '../user.service';
@Component({
selector: 'app-user-list',
templateUrl: './user-list.component.html'
})
export class UserListComponent {
users = [];
constructor(private userService: UserService) {
this.users = this.userService.getUsers();
}
}
Routing in Angular
Basic Setup
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';
import { ContactComponent } from './contact/contact.component';
const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'about', component: AboutComponent },
{ path: 'contact', component: ContactComponent },
{ path: '**', redirectTo: '' } // Wildcard route for 404
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
Router Navigation
<!-- Template navigation -->
<a routerLink="/about" routerLinkActive="active">About</a>
<!-- Dynamic navigation -->
<a [routerLink]="['/user', userId]">User Details</a>
// Programmatic navigation
import { Router } from '@angular/router';
constructor(private router: Router) {}
navigateToHome() {
this.router.navigate(['/home']);
}
Route Parameters
// Route definition
{ path: 'user/:id', component: UserDetailComponent }
// Accessing parameters
import { ActivatedRoute } from '@angular/router';
constructor(private route: ActivatedRoute) {
// Snapshot approach
const id = this.route.snapshot.paramMap.get('id');
// Observable approach (for parameter changes within same component)
this.route.paramMap.subscribe(params => {
const id = params.get('id');
});
}
Forms in Angular
Template-Driven Forms
// Import in module
import { FormsModule } from '@angular/forms';
@NgModule({
imports: [FormsModule]
})
<form #userForm="ngForm" (ngSubmit)="onSubmit(userForm.value)">
<div>
<label for="name">Name</label>
<input type="text" id="name" name="name" [(ngModel)]="user.name" required #name="ngModel">
<div *ngIf="name.invalid && (name.dirty || name.touched)">
Name is required
</div>
</div>
<button type="submit" [disabled]="userForm.invalid">Submit</button>
</form>
Reactive Forms
// Import in module
import { ReactiveFormsModule } from '@angular/forms';
@NgModule({
imports: [ReactiveFormsModule]
})
// Component
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
export class UserFormComponent {
userForm: FormGroup;
constructor(private fb: FormBuilder) {
this.userForm = this.fb.group({
name: ['', [Validators.required, Validators.minLength(2)]],
email: ['', [Validators.required, Validators.email]],
address: this.fb.group({
street: [''],
city: [''],
zip: ['']
})
});
}
onSubmit() {
console.log(this.userForm.value);
}
}
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
<div>
<label for="name">Name</label>
<input type="text" id="name" formControlName="name">
<div *ngIf="userForm.get('name').invalid && userForm.get('name').touched">
<div *ngIf="userForm.get('name').errors?.required">Name is required</div>
<div *ngIf="userForm.get('name').errors?.minlength">Name must be at least 2 characters</div>
</div>
</div>
<div formGroupName="address">
<h3>Address</h3>
<input type="text" formControlName="street" placeholder="Street">
<input type="text" formControlName="city" placeholder="City">
<input type="text" formControlName="zip" placeholder="ZIP">
</div>
<button type="submit" [disabled]="userForm.invalid">Submit</button>
</form>
HTTP and API Interaction
HttpClient Setup
// app.module.ts
import { HttpClientModule } from '@angular/common/http';
@NgModule({
imports: [HttpClientModule]
})
Making HTTP Requests
// data.service.ts
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class DataService {
private apiUrl = 'https://api.example.com/data';
constructor(private http: HttpClient) {}
getData(): Observable<any[]> {
return this.http.get<any[]>(this.apiUrl)
.pipe(
tap(data => console.log('Data fetched', data)),
catchError(this.handleError)
);
}
getItemById(id: number): Observable<any> {
return this.http.get<any>(`${this.apiUrl}/${id}`);
}
addItem(item: any): Observable<any> {
return this.http.post<any>(this.apiUrl, item, {
headers: new HttpHeaders({'Content-Type': 'application/json'})
});
}
updateItem(item: any): Observable<any> {
return this.http.put<any>(`${this.apiUrl}/${item.id}`, item);
}
deleteItem(id: number): Observable<any> {
return this.http.delete<any>(`${this.apiUrl}/${id}`);
}
searchItems(term: string): Observable<any[]> {
const params = new HttpParams().set('q', term);
return this.http.get<any[]>(this.apiUrl, { params });
}
private handleError(error: any) {
console.error('API error', error);
return [];
}
}
Using Services in Components
import { Component, OnInit } from '@angular/core';
import { DataService } from '../data.service';
@Component({
selector: 'app-data-list',
templateUrl: './data-list.component.html'
})
export class DataListComponent implements OnInit {
items = [];
loading = false;
error = null;
constructor(private dataService: DataService) {}
ngOnInit() {
this.getItems();
}
getItems() {
this.loading = true;
this.dataService.getData()
.subscribe({
next: (data) => {
this.items = data;
this.loading = false;
},
error: (err) => {
this.error = 'Failed to load data';
this.loading = false;
}
});
}
}
State Management with RxJS
Basic State Service
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class StateService {
private itemsSubject = new BehaviorSubject<any[]>([]);
items$: Observable<any[]> = this.itemsSubject.asObservable();
private loadingSubject = new BehaviorSubject<boolean>(false);
loading$: Observable<boolean> = this.loadingSubject.asObservable();
updateItems(items: any[]) {
this.itemsSubject.next(items);
}
setLoading(isLoading: boolean) {
this.loadingSubject.next(isLoading);
}
}
Using in Components
import { Component, OnInit } from '@angular/core';
import { StateService } from '../state.service';
import { DataService } from '../data.service';
import { Observable } from 'rxjs';
@Component({
selector: 'app-items',
template: `
<div *ngIf="loading$ | async">Loading...</div>
<ul>
<li *ngFor="let item of items$ | async">{{item.name}}</li>
</ul>
`
})
export class ItemsComponent implements OnInit {
items$: Observable<any[]>;
loading$: Observable<boolean>;
constructor(
private stateService: StateService,
private dataService: DataService
) {
this.items$ = this.stateService.items$;
this.loading$ = this.stateService.loading$;
}
ngOnInit() {
this.fetchData();
}
fetchData() {
this.stateService.setLoading(true);
this.dataService.getData().subscribe({
next: (data) => {
this.stateService.updateItems(data);
this.stateService.setLoading(false);
},
error: () => this.stateService.setLoading(false)
});
}
}
Testing Angular Applications
Component Testing
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserComponent } from './user.component';
import { UserService } from '../user.service';
import { of } from 'rxjs';
describe('UserComponent', () => {
let component: UserComponent;
let fixture: ComponentFixture<UserComponent>;
let userServiceSpy: jasmine.SpyObj<UserService>;
beforeEach(async () => {
const spy = jasmine.createSpyObj('UserService', ['getUsers']);
await TestBed.configureTestingModule({
declarations: [UserComponent],
providers: [{ provide: UserService, useValue: spy }]
}).compileComponents();
userServiceSpy = TestBed.inject(UserService) as jasmine.SpyObj<UserService>;
});
beforeEach(() => {
fixture = TestBed.createComponent(UserComponent);
component = fixture.componentInstance;
userServiceSpy.getUsers.and.returnValue(of([{id: 1, name: 'Test User'}]));
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should load users on init', () => {
expect(userServiceSpy.getUsers).toHaveBeenCalled();
expect(component.users.length).toBe(1);
expect(component.users[0].name).toBe('Test User');
});
});
Service Testing
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { DataService } from './data.service';
describe('DataService', () => {
let service: DataService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [DataService]
});
service = TestBed.inject(DataService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify(); // Verify no outstanding requests
});
it('should retrieve data from API', () => {
const mockData = [{id: 1, name: 'Item 1'}, {id: 2, name: 'Item 2'}];
service.getData().subscribe(data => {
expect(data).toEqual(mockData);
});
const req = httpMock.expectOne('https://api.example.com/data');
expect(req.request.method).toBe('GET');
req.flush(mockData);
});
});
Optimization Techniques
Performance Best Practices
Technique | Description |
---|---|
OnPush Change Detection | Use changeDetection: ChangeDetectionStrategy.OnPush to improve performance |
Lazy Loading | Load feature modules on demand with route configuration |
Pure Pipes | Use pure pipes for transformations where possible |
trackBy in ngFor | Add trackBy function to optimize list rendering |
Async Pipe | Use async pipe to automatically manage observable subscriptions |
Web Workers | Offload CPU-intensive tasks to web workers |
Server-Side Rendering | Use Angular Universal for SSR |
AOT Compilation | Always use ahead-of-time compilation for production |
Code Splitting and Lazy Loading
// app-routing.module.ts
const routes: Routes = [
{ path: '', component: HomeComponent },
{
path: 'admin',
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
},
{
path: 'products',
loadChildren: () => import('./products/products.module').then(m => m.ProductsModule)
}
];
Common Challenges and Solutions
Challenge | Solution |
---|---|
Circular Dependencies | Refactor code to use interface or service for shared functionality |
Memory Leaks | Unsubscribe from observables in ngOnDestroy lifecycle hook |
ExpressionChangedAfterItHasBeenCheckedError | Ensure changes happen before change detection or use setTimeout/ngZone |
Large Bundle Size | Implement lazy loading, code splitting, and tree shaking |
Slow Rendering | Use OnPush change detection, trackBy, and pure pipes |
Complex Forms | Break into smaller components with focused responsibility |
Authentication | Use route guards and HTTP interceptors |
State Management Complexity | Consider NgRx or simpler state management solutions |
Angular Best Practices
Structure and Organization
- Follow consistent naming conventions
- Keep components small and focused
- Organize by feature modules
- Separate business logic into services
Performance
- Use OnPush change detection
- Implement trackBy for ngFor
- Minimize DOM manipulation
- Lazy load feature modules
Security
- Use Angular’s built-in sanitization
- Implement proper authentication/authorization
- Protect against XSS and CSRF
Data Management
- Use RxJS effectively with proper operators
- Manage subscriptions to avoid memory leaks
- Consider state management patterns for complex apps
Testing
- Write unit tests for services and components
- Use TestBed for component testing
- Mock dependencies with jasmine spies
Resources for Further Learning
Official Resources
Community Resources
Tools and Libraries
- NgRx – State management
- Angular Material – UI component library
- Nx – Workspace tools for monorepo development
- Scully – Static site generator for Angular
- ng-bootstrap – Bootstrap components for Angular