The Ultimate Angular Cheat Sheet: A Complete Reference Guide

Introduction

Angular is a powerful TypeScript-based front-end web application framework maintained by Google. It provides a comprehensive solution for building single-page applications (SPAs) with its component-based architecture, powerful templating system, and robust tooling. Angular’s opinionated structure promotes consistent code organization, making it ideal for large-scale enterprise applications.

Core Concepts & Principles

Angular Building Blocks

Building BlockDescription
ComponentsThe fundamental UI building blocks in Angular; encapsulate data, HTML templates, and styling
ModulesContainers for organizing related components, directives, pipes, and services
ServicesReusable classes for sharing functionality across components through dependency injection
DirectivesAdd behavior to DOM elements (e.g., *ngIf, *ngFor)
PipesTransform displayed values within templates (e.g., formatting dates, filtering lists)
GuardsControl access to routes based on conditions
InterceptorsIntercept and modify HTTP requests and responses
DecoratorsMetadata annotations that configure Angular features (e.g., @Component, @Injectable)

Component Lifecycle Hooks

HookExecution TimingUse Case
ngOnChanges()Before ngOnInit and when input properties changeReact to input property changes
ngOnInit()Once, after the first ngOnChangesInitialize component after Angular sets inputs
ngDoCheck()During change detectionImplement custom change detection logic
ngAfterContentInit()After content projectionInitialize projected content
ngAfterContentChecked()After checking projected contentRespond to projected content changes
ngAfterViewInit()After component’s view is initializedAccess and manipulate child views
ngAfterViewChecked()After checking component’s viewRespond to view changes
ngOnDestroy()Before component is destroyedClean up subscriptions, event handlers, etc.

Setting Up Angular Projects

Installation & Project Creation

# Install Angular CLI
npm install -g @angular/cli

# Create a new Angular project
ng new my-app-name

# Add routing during creation
ng new my-app-name --routing

# Specify CSS preprocessor
ng new my-app-name --style=scss

# Generate components, services, etc.
ng generate component component-name
ng generate service service-name
ng generate module module-name
ng generate pipe pipe-name
ng generate directive directive-name
ng generate guard guard-name
ng generate interface interface-name
ng generate enum enum-name
ng generate class class-name

# Development server
ng serve
ng serve --open    # Opens browser automatically
ng serve --port 4201    # Custom port

# Build for production
ng build --configuration production

Angular Templates Syntax

Data Binding

<!-- Interpolation -->
<p>{{ expression }}</p>
<p>Hello, {{ user.name }}</p>

<!-- Property Binding -->
<img [src]="imageUrl">
<button [disabled]="isDisabled">Click me</button>

<!-- Event Binding -->
<button (click)="onClick()">Click me</button>
<input (keyup)="onKeyUp($event)">

<!-- Two-way Binding -->
<input [(ngModel)]="name">

Built-in Directives

<!-- Conditional Rendering -->
<div *ngIf="condition">Content to show if condition is true</div>
<div *ngIf="condition; else elseBlock">Content if true</div>
<ng-template #elseBlock>Content if false</ng-template>

<!-- List Rendering -->
<ul>
  <li *ngFor="let item of items; let i = index; trackBy: trackByFn">
    {{ i }} - {{ item.name }}
  </li>
</ul>

<!-- Switch Cases -->
<div [ngSwitch]="condition">
  <div *ngSwitchCase="'case1'">Content for case1</div>
  <div *ngSwitchCase="'case2'">Content for case2</div>
  <div *ngSwitchDefault>Default content</div>
</div>

<!-- Class Binding -->
<div [ngClass]="{'active': isActive, 'disabled': isDisabled}"></div>

<!-- Style Binding -->
<div [ngStyle]="{'color': textColor, 'font-size': fontSize + 'px'}"></div>

Template Reference Variables

<input #nameInput type="text">
<button (click)="greet(nameInput.value)">Greet</button>

Pipes

<!-- Built-in Pipes -->
{{ dateValue | date:'short' }}
{{ price | currency:'USD' }}
{{ text | uppercase }}
{{ text | lowercase }}
{{ number | number:'1.2-2' }}
{{ object | json }}
{{ array | slice:1:3 }}
{{ text | titlecase }}
{{ value | percent:'2.2-2' }}

<!-- Pipe Chaining -->
{{ dateValue | date:'fullDate' | uppercase }}

<!-- Parameterized Pipes -->
{{ dateValue | date:'MM/dd/yyyy' }}

Component Communication

Parent to Child: Input Properties

// Child component
@Component({...})
export class ChildComponent {
  @Input() data: string;
  @Input('aliasName') originalName: string;
}

// Parent template
<app-child [data]="parentData" [aliasName]="parentProperty"></app-child>

Child to Parent: Output Properties & EventEmitter

// Child component
@Component({...})
export class ChildComponent {
  @Output() dataChange = new EventEmitter<string>();
  
  sendToParent() {
    this.dataChange.emit('Data to parent');
  }
}

// Parent template
<app-child (dataChange)="handleData($event)"></app-child>

View Child and Content Child

@Component({...})
export class ParentComponent implements AfterViewInit {
  @ViewChild(ChildComponent) childComp: ChildComponent;
  @ViewChild('inputRef') inputElement: ElementRef;
  @ViewChildren(ChildComponent) childrenComps: QueryList<ChildComponent>;
  
  @ContentChild(ContentChildComponent) contentChild: ContentChildComponent;
  @ContentChildren(ContentChildComponent) contentChildren: QueryList<ContentChildComponent>;
  
  ngAfterViewInit() {
    // Access view child elements after view initialization
    this.childComp.someMethod();
    this.inputElement.nativeElement.focus();
  }
}

Angular Services & Dependency Injection

Creating and Injecting Services

// Service definition
@Injectable({
  providedIn: 'root'  // Application-wide singleton
})
export class DataService {
  getData() {
    return ['data1', 'data2'];
  }
}

// Component using the service
@Component({...})
export class MyComponent {
  data: string[];
  
  constructor(private dataService: DataService) {
    this.data = this.dataService.getData();
  }
}

Injection Providers & Hierarchies

// Module-level provider
@NgModule({
  providers: [
    DataService,
    { provide: API_URL, useValue: 'https://api.example.com' },
    { provide: LoggerService, useClass: ProductionLoggerService },
    { provide: CacheService, useFactory: cacheFactory, deps: [StorageService] }
  ]
})

// Component-level provider (instance only for this component and its children)
@Component({
  providers: [DataService]
})

Angular Routing

Basic Routing Configuration

// app-routing.module.ts
const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'products', component: ProductListComponent },
  { path: 'products/:id', component: ProductDetailComponent },
  { path: 'admin', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule) },
  { path: '**', component: NotFoundComponent }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Router Directives and Links

<!-- Router outlet where components will be rendered -->
<router-outlet></router-outlet>

<!-- Router links -->
<a routerLink="/products">Products</a>
<a [routerLink]="['/products', product.id]">{{ product.name }}</a>

<!-- Active link styles -->
<a routerLink="/products" routerLinkActive="active-link">Products</a>
<a routerLink="/admin" [routerLinkActiveOptions]="{exact: true}" routerLinkActive="active">Admin</a>

Route Parameters & Query Parameters

// Accessing route parameters
constructor(private route: ActivatedRoute) {}

ngOnInit() {
  // Snapshot approach
  const id = this.route.snapshot.paramMap.get('id');
  
  // Observable approach (recommended for params that can change while component is active)
  this.route.paramMap.subscribe(params => {
    const id = params.get('id');
  });
  
  // Query parameters
  this.route.queryParamMap.subscribe(params => {
    const page = params.get('page');
    const sort = params.get('sort');
  });
}

Router Navigation

constructor(private router: Router) {}

navigate() {
  // Simple navigation
  this.router.navigate(['/products']);
  
  // With params
  this.router.navigate(['/products', productId]);
  
  // With query params
  this.router.navigate(['/products'], { 
    queryParams: { page: 1, sort: 'name' }
  });
  
  // Relative navigation
  this.router.navigate(['details'], { relativeTo: this.route });
  
  // Preserve query params
  this.router.navigate(['/list'], { 
    queryParamsHandling: 'preserve' // or 'merge'
  });
}

Route Guards

// CanActivate guard
@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate {
  constructor(private authService: AuthService, private router: Router) {}
  
  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    if (this.authService.isLoggedIn()) {
      return true;
    }
    this.router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
    return false;
  }
}

// Route with guards
const routes: Routes = [
  { 
    path: 'admin', 
    component: AdminComponent,
    canActivate: [AuthGuard],
    canDeactivate: [CanDeactivateGuard],
    canLoad: [AuthGuard],
    resolve: {
      data: DataResolver
    }
  }
];

Forms in Angular

Template-Driven Forms

<!-- Template-driven form -->
<form #userForm="ngForm" (ngSubmit)="onSubmit(userForm.value)">
  <div>
    <label for="name">Name</label>
    <input id="name" name="name" [(ngModel)]="user.name" required #name="ngModel">
    <div *ngIf="name.invalid && (name.dirty || name.touched)">
      <div *ngIf="name.errors?.['required']">Name is required</div>
    </div>
  </div>
  
  <div>
    <label for="email">Email</label>
    <input id="email" name="email" [(ngModel)]="user.email" email #email="ngModel">
    <div *ngIf="email.invalid && (email.dirty || email.touched)">
      <div *ngIf="email.errors?.['email']">Invalid email format</div>
    </div>
  </div>
  
  <button type="submit" [disabled]="!userForm.valid">Submit</button>
</form>

Reactive Forms

// Component
import { FormBuilder, FormGroup, Validators, FormArray } from '@angular/forms';

@Component({...})
export class UserFormComponent implements OnInit {
  userForm: FormGroup;
  
  constructor(private fb: FormBuilder) {}
  
  ngOnInit() {
    this.userForm = this.fb.group({
      name: ['', [Validators.required, Validators.minLength(2)]],
      email: ['', [Validators.required, Validators.email]],
      address: this.fb.group({
        street: [''],
        city: [''],
        zipcode: ['', Validators.pattern(/^\d{5}$/)]
      }),
      skills: this.fb.array([
        this.fb.control('')
      ])
    });
  }
  
  get skills() {
    return this.userForm.get('skills') as FormArray;
  }
  
  addSkill() {
    this.skills.push(this.fb.control(''));
  }
  
  removeSkill(index: number) {
    this.skills.removeAt(index);
  }
  
  onSubmit() {
    if (this.userForm.valid) {
      console.log(this.userForm.value);
    }
  }
}
<!-- Reactive form template -->
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
  <div>
    <label for="name">Name</label>
    <input 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 formControlName="street" placeholder="Street">
    <input formControlName="city" placeholder="City">
    <input formControlName="zipcode" placeholder="Zipcode">
  </div>
  
  <div>
    <h3>Skills</h3>
    <div formArrayName="skills">
      <div *ngFor="let skill of skills.controls; let i = index">
        <input [formControlName]="i">
        <button type="button" (click)="removeSkill(i)">Remove</button>
      </div>
    </div>
    <button type="button" (click)="addSkill()">Add Skill</button>
  </div>
  
  <button type="submit" [disabled]="userForm.invalid">Submit</button>
</form>

Custom Validators

// Custom validator function
function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const forbidden = nameRe.test(control.value);
    return forbidden ? { forbiddenName: { value: control.value } } : null;
  };
}

// Using custom validator in form
this.userForm = this.fb.group({
  name: ['', [Validators.required, forbiddenNameValidator(/admin/i)]],
  // other fields...
});

// Custom async validator
function uniqueEmailValidator(userService: UserService): AsyncValidatorFn {
  return (control: AbstractControl): Observable<ValidationErrors | null> => {
    return userService.checkEmailExists(control.value).pipe(
      map(exists => exists ? { emailExists: true } : null),
      catchError(() => of(null))
    );
  };
}

// Using async validator
this.userForm = this.fb.group({
  email: ['', {
    validators: [Validators.required, Validators.email],
    asyncValidators: [uniqueEmailValidator(this.userService)],
    updateOn: 'blur'
  }],
  // other fields...
});

HTTP Client

Basic HTTP Requests

@Injectable({
  providedIn: 'root'
})
export class DataService {
  private apiUrl = 'https://api.example.com/items';
  
  constructor(private http: HttpClient) {}
  
  // GET request
  getItems(): Observable<Item[]> {
    return this.http.get<Item[]>(this.apiUrl);
  }
  
  // GET request with parameters
  getItemById(id: number): Observable<Item> {
    return this.http.get<Item>(`${this.apiUrl}/${id}`);
  }
  
  // POST request
  addItem(item: Item): Observable<Item> {
    return this.http.post<Item>(this.apiUrl, item);
  }
  
  // PUT request
  updateItem(id: number, item: Item): Observable<Item> {
    return this.http.put<Item>(`${this.apiUrl}/${id}`, item);
  }
  
  // DELETE request
  deleteItem(id: number): Observable<void> {
    return this.http.delete<void>(`${this.apiUrl}/${id}`);
  }
}

HTTP Headers & Parameters

import { HttpHeaders, HttpParams } from '@angular/common/http';

// With headers
getItems(): Observable<Item[]> {
  const headers = new HttpHeaders()
    .set('Content-Type', 'application/json')
    .set('Authorization', 'Bearer ' + this.authService.getToken());
    
  return this.http.get<Item[]>(this.apiUrl, { headers });
}

// With URL parameters
searchItems(term: string, page: number): Observable<Item[]> {
  const params = new HttpParams()
    .set('query', term)
    .set('page', page.toString())
    .set('limit', '10');
    
  return this.http.get<Item[]>(`${this.apiUrl}/search`, { params });
}

HTTP Interceptors

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  constructor(private authService: AuthService) {}
  
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // Clone the request and add auth header
    const authToken = this.authService.getToken();
    const authReq = req.clone({
      headers: req.headers.set('Authorization', `Bearer ${authToken}`)
    });
    
    // Pass the cloned request to the next handler
    return next.handle(authReq);
  }
}

// Register in AppModule
@NgModule({
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
  ]
})

Error Handling

getItems(): Observable<Item[]> {
  return this.http.get<Item[]>(this.apiUrl).pipe(
    catchError(this.handleError<Item[]>('getItems', []))
  );
}

private handleError<T>(operation = 'operation', result?: T) {
  return (error: HttpErrorResponse): Observable<T> => {
    console.error(`${operation} failed: ${error.message}`);
    
    // Send error to remote logging service
    this.logService.logError(error);
    
    // Let the app keep running by returning an empty result
    return of(result as T);
  };
}

RxJS in Angular

Common RxJS Operators

import { 
  map, filter, tap, catchError, switchMap, 
  debounceTime, distinctUntilChanged, mergeMap,
  concatMap, takeUntil, take, skip, throttleTime 
} from 'rxjs/operators';
import { of, from, Observable, Subject, combineLatest, forkJoin } from 'rxjs';

// Example usage in a search component
@Component({...})
export class SearchComponent implements OnInit, OnDestroy {
  searchTerms = new Subject<string>();
  results$: Observable<Result[]>;
  private destroy$ = new Subject<void>();
  
  constructor(private searchService: SearchService) {}
  
  ngOnInit() {
    this.results$ = this.searchTerms.pipe(
      // Wait 300ms after each keystroke
      debounceTime(300),
      
      // Ignore if same as previous search term
      distinctUntilChanged(),
      
      // Switch to new search observable each time the term changes
      switchMap(term => term 
        ? this.searchService.search(term)
        : of([])
      ),
      
      // Handle errors
      catchError(error => {
        console.error(error);
        return of([]);
      }),
      
      // Unsubscribe when component is destroyed
      takeUntil(this.destroy$)
    );
  }
  
  search(term: string): void {
    this.searchTerms.next(term);
  }
  
  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

Combining Observables

// combineLatest - emits when any source observable emits
combineLatest([observable1$, observable2$]).subscribe(([result1, result2]) => {
  console.log(result1, result2);
});

// forkJoin - waits for all observables to complete, then emits last values
forkJoin({
  users: this.userService.getUsers(),
  config: this.configService.getConfig()
}).subscribe(({ users, config }) => {
  console.log(users, config);
});

// merge - flattens multiple Observables together
merge(observable1$, observable2$).subscribe(result => {
  console.log(result); // Could be from either observable
});

Unsubscribing Patterns

// Option 1: Using takeUntil with a destroy Subject
@Component({...})
export class MyComponent implements OnInit, OnDestroy {
  private destroy$ = new Subject<void>();
  
  ngOnInit() {
    this.someObservable$.pipe(
      takeUntil(this.destroy$)
    ).subscribe(data => {
      // Handle data
    });
  }
  
  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

// Option 2: Manually tracking subscriptions
@Component({...})
export class MyComponent implements OnDestroy {
  private subscriptions = new Subscription();
  
  someMethod() {
    const sub = this.someObservable$.subscribe(data => {
      // Handle data
    });
    this.subscriptions.add(sub);
  }
  
  ngOnDestroy() {
    this.subscriptions.unsubscribe();
  }
}

// Option 3: Using the async pipe in templates
@Component({
  template: `<div *ngFor="let item of items$ | async">{{item.name}}</div>`
})
export class MyComponent {
  items$: Observable<Item[]>;
  
  constructor(private dataService: DataService) {
    this.items$ = this.dataService.getItems();
  }
}

State Management

Component State

@Component({...})
export class CounterComponent {
  count = 0;
  
  increment() {
    this.count++;
  }
  
  decrement() {
    this.count--;
  }
}

Services for Shared State

@Injectable({
  providedIn: 'root'
})
export class CartService {
  private items: Product[] = [];
  private cartSubject = new BehaviorSubject<Product[]>([]);
  
  cart$ = this.cartSubject.asObservable();
  
  addToCart(product: Product) {
    this.items = [...this.items, product];
    this.cartSubject.next(this.items);
  }
  
  removeFromCart(productId: number) {
    this.items = this.items.filter(item => item.id !== productId);
    this.cartSubject.next(this.items);
  }
  
  getItems() {
    return this.items;
  }
  
  clearCart() {
    this.items = [];
    this.cartSubject.next(this.items);
  }
}

NgRx Store

// Actions
export const increment = createAction('[Counter] Increment');
export const decrement = createAction('[Counter] Decrement');
export const reset = createAction('[Counter] Reset');
export const incrementBy = createAction(
  '[Counter] Increment By',
  props<{ amount: number }>()
);

// Reducer
export const initialState = 0;

export const counterReducer = createReducer(
  initialState,
  on(increment, state => state + 1),
  on(decrement, state => state - 1),
  on(reset, state => 0),
  on(incrementBy, (state, { amount }) => state + amount)
);

// Selectors
export const selectCount = (state: AppState) => state.count;

// Store in component
@Component({...})
export class CounterComponent {
  count$: Observable<number>;
  
  constructor(private store: Store<AppState>) {
    this.count$ = this.store.select(selectCount);
  }
  
  increment() {
    this.store.dispatch(increment());
  }
  
  decrement() {
    this.store.dispatch(decrement());
  }
  
  reset() {
    this.store.dispatch(reset());
  }
  
  incrementBy(amount: number) {
    this.store.dispatch(incrementBy({ amount }));
  }
}

Testing Angular Applications

Component Testing

// component.spec.ts
describe('CounterComponent', () => {
  let component: CounterComponent;
  let fixture: ComponentFixture<CounterComponent>;
  
  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [CounterComponent]
    }).compileComponents();
    
    fixture = TestBed.createComponent(CounterComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });
  
  it('should create', () => {
    expect(component).toBeTruthy();
  });
  
  it('should increment count', () => {
    const initialValue = component.count;
    component.increment();
    expect(component.count).toBe(initialValue + 1);
  });
  
  it('should render count value', () => {
    component.count = 42;
    fixture.detectChanges();
    const compiled = fixture.nativeElement;
    expect(compiled.querySelector('.count-value').textContent).toContain('42');
  });
  
  it('should call increment when increment button is clicked', () => {
    spyOn(component, 'increment');
    const button = fixture.nativeElement.querySelector('.increment-button');
    button.click();
    expect(component.increment).toHaveBeenCalled();
  });
});

Service Testing

// service.spec.ts
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();
  });
  
  it('should be created', () => {
    expect(service).toBeTruthy();
  });
  
  it('should get items', () => {
    const mockItems = [{ id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }];
    
    service.getItems().subscribe(items => {
      expect(items).toEqual(mockItems);
    });
    
    const req = httpMock.expectOne('https://api.example.com/items');
    expect(req.request.method).toBe('GET');
    req.flush(mockItems);
  });
  
  it('should handle errors', () => {
    service.getItems().subscribe({
      next: () => fail('should have failed'),
      error: error => {
        expect(error.status).toBe(404);
      }
    });
    
    const req = httpMock.expectOne('https://api.example.com/items');
    req.flush('Not found', { status: 404, statusText: 'Not Found' });
  });
});

Testing with Mocks

// Mock service
const mockDataService = {
  getItems: jasmine.createSpy('getItems').and.returnValue(of([
    { id: 1, name: 'Item 1' },
    { id: 2, name: 'Item 2' }
  ]))
};

// Test with mocked service
describe('ItemListComponent', () => {
  let component: ItemListComponent;
  let fixture: ComponentFixture<ItemListComponent>;
  
  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [ItemListComponent],
      providers: [
        { provide: DataService, useValue: mockDataService }
      ]
    }).compileComponents();
    
    fixture = TestBed.createComponent(ItemListComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });
  
  it('should fetch items on init', () => {
    expect(mockDataService.getItems).toHaveBeenCalled();
    expect(component.items.length).toBe(2);
  });
});

Performance Optimization

Change Detection Strategies

@Component({
  selector: 'app-item',
  template: '...',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ItemComponent {
  @Input() item: Item;
  
  // Component will only update when:
  // 1. Input references change (not just properties of the input)
  // 2. Component events trigger
  // 3. Observable with async pipe emits
  // 4. Manually triggering change detection
}

TrackBy Function for NgFor

<div *ngFor="let item of items; trackBy: trackByItemId">
  {{ item.name }}
</div>
trackByItemId(index: number, item: Item): number {
  return item.id;
}

Pure Pipes for Transformations

@Pipe({
  name: 'filter',
  pure: true  // Default is true
})
export class FilterPipe implements PipeTransform {
  transform(items: any[], field: string, value: any): any[] {
    if (!items) return [];
    if (!value) return items;
    
    return items.filter(item => item[field] === value);
  }
}

Lazy Loading Modules

// 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 Angular Best Practices

Architecture

  1. Follow Angular Style Guide: Use the official Angular style guide for consistent code structure
  2. Modular Design: Organize by feature modules, shared modules, and core modules
  3. Smart vs. Presentational Components:
    • Smart (container) components: Fetch data, manage state
    • Presentational components: Display data, emit events

Performance

  1. Use OnPush Change Detection for pure components
  2. Lazy load modules for large applications
  3. Implement trackBy functions with *ngFor
  4. Virtual scrolling for long lists with ScrollingModule
  5. Optimize bundle size with tree-shaking

State Management

  1. Services with Observables for simpler applications
  2. NgRx/Redux pattern for complex applications
  3. Keep component state minimal and lift shared state up

Security

  1. Always sanitize user input
  2. Use Angular’s built-in XSS protection
  3. Implement proper authentication and authorization
  4. Use HttpInterceptors for adding auth tokens
  5. Avoid direct DOM manipulation when possible

Common Challenges and Solutions

ChallengeSolution
Circular DependenciesUse forwardRef() in constructor params or redesign service relationships
Complex FormsUse FormBuilder with nested FormGroups and FormArrays
Memory LeaksAlways unsubscribe from observables, use takeUntil operator
Large Bundle SizeImplement lazy loading, code splitting, tree-shaking
Slow Change DetectionUse OnPush strategy, pure pipes, detach components when needed
Server-Side RenderingImplement Angular Universal for SEO and performance
IE11 CompatibilityAdd polyfills and limit use of modern JavaScript features
DebuggingUse Angular DevTools, Augury, or Redux DevTools
Testing ComponentsUse TestBed, shallow rendering, and component harnesses
Mobile PerformanceOptimize bundle size, minimize DOM operations, use PWA features

Resources for Further Learning

Official Documentation

Books

  • “Angular: Up and Running” by Shyam Seshadri
  • “Angular in Action” by Jeremy Wilken
  • “NgRx in Angular” by Gion Kunz

Courses and Tutorials

  • Angular University (angular-university.io)
  • Pluralsight Angular Path
  • Udemy – Angular: The Complete Guide

Tools

  • Angular DevTools
  • Augury
  • NgRx DevTools
  • Angular Language Service
Scroll to Top