Lesson 05-Angular Deep Dive into Components

Component Basics

Defining a Component

Components are the core building blocks of an Angular application, encapsulating the view (template), styles, and logic (class). The @Component decorator is used to define a component.

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <h1>Welcome to Angular!</h1>
    <button (click)="onButtonClick()">Click me!</button>
  `,
  styles: [`
    h1 {
      color: blue;
    }
  `]
})
export class AppComponent {
  onButtonClick() {
    console.log('Button clicked!');
  }
}

Component Tree

Angular applications are structured as a tree of components, with the root component typically being AppComponent. Child components can be nested within parent component templates.

<!-- app.component.html -->
<app-child></app-child>
// child.component.ts
@Component({
  selector: 'app-child',
  template: `<h2>I'm a child component.</h2>`
})
export class ChildComponent { }

Component Templates

Template Syntax

Angular templates support various syntaxes, including interpolation ({{ expression }}), property binding ([property]="expression"), event binding ((event)="method($event)"), and directives.

<!-- app.component.html -->
<h1>{{ title }}</h1>
<input [(ngModel)]="name" placeholder="Enter your name">
<button (click)="logName()">Log Name</button>

*ngIf and *ngFor

The ngIf directive is used for conditional rendering, while ngFor is used for list rendering.

<!-- app.component.html -->
<div *ngIf="showMessage; else noMessage">
  Hello, {{ name }}!
</div>
<ng-template #noMessage>
  Please enter your name.
</ng-template>

<ul>
  <li *ngFor="let item of items">{{ item }}</li>
</ul>

Component Interaction

Data Binding

Parent components pass data to child components using property binding, while child components emit events to parents using event binding.

<!-- parent.component.html -->
<app-child [data]="parentData" (childEvent)="handleChildEvent($event)"></app-child>
// child.component.ts
@Input() data: any;
@Output() childEvent = new EventEmitter();

onButtonClick() {
  this.childEvent.emit(this.data);
}

Service Sharing

The best way to share data and services between components is through dependency injection.

// shared.service.ts
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class SharedService {
  data: any;
}

Injecting a Service in a Component:

// component.ts
import { Component } from '@angular/core';
import { SharedService } from './shared.service';

@Component({
  selector: 'app-component',
  template: `...`
})
export class Component {
  constructor(private sharedService: SharedService) {}
}

Lifecycle Hooks

Angular components have several lifecycle hooks that are invoked at different stages of their lifecycle.

import { Component, OnInit, OnChanges, SimpleChanges } from '@angular/core';

@Component({
  selector: 'app-lifecycle',
  template: `...`
})
export class LifecycleComponent implements OnInit, OnChanges {
  ngOnChanges(changes: SimpleChanges): void {
    console.log('Component input properties have changed.');
  }

  ngOnInit(): void {
    console.log('Component has been initialized.');
  }
}

Input and Output Properties

Input properties receive data from parent components, while output properties emit events to parent components.

// child.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-child',
  template: `...`
})
export class ChildComponent {
  @Input() message: string;
  @Output() messageSent = new EventEmitter<string>();

  sendMessage() {
    this.messageSent.emit('Hello from child!');
  }
}

View Containers

ViewContainerRef and TemplateRef enable dynamic insertion and removal of views.

import { Component, ViewContainerRef, TemplateRef } from '@angular/core';

@Component({
  selector: 'app-dynamic',
  template: `...`
})
export class DynamicComponent {
  constructor(private viewContainer: ViewContainerRef, private templateRef: TemplateRef<any>) {}

  addView() {
    this.viewContainer.createEmbeddedView(this.templateRef);
  }

  removeView() {
    this.viewContainer.clear();
  }
}

Custom Directives

Custom directives extend the behavior of HTML elements.

import { Directive, ElementRef, Renderer2 } from '@angular/core';

@Directive({
  selector: '[appHighlight]'
})
export class HighlightDirective {
  constructor(private el: ElementRef, private renderer: Renderer2) {
    this.renderer.setStyle(this.el.nativeElement, 'backgroundColor', 'yellow');
  }
}

Unit Testing

Use Karma and Jasmine for unit testing Angular components.

// component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Component } from './component';

describe('Component', () => {
  let component: Component;
  let fixture: ComponentFixture<Component>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [Component]
    }).compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(Component);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});

Two-Way Data Binding

Angular’s two-way data binding, facilitated by the [(ngModel)] directive, simplifies synchronization between form controls and component properties.

<!-- app.component.html -->
<input type="text" [(ngModel)]="username" placeholder="Enter your username">

Pure Functions and Change Detection

Angular’s change detection mechanism relies on pure functions and dirty checking. Template updates depend on input property changes, and pure functions ensure consistent outputs for the same inputs, reducing unnecessary rendering.

// pure-function.component.ts
import { Component, ChangeDetectionStrategy } from '@angular/core';

@Component({
  selector: 'app-pure-function',
  template: `...`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class PureFunctionComponent {
  // Ensure the method is a pure function
  calculateValue(input: number): number {
    return input * 2;
  }
}

Asynchronous Programming

Angular components often handle asynchronous data flows, such as HTTP requests. The RxJS library provides powerful tools for these scenarios.

// async.component.ts
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ApiService } from './api.service';

@Component({
  selector: 'app-async',
  template: `...`
})
export class AsyncComponent implements OnInit {
  public data$: Observable<any>;

  constructor(private apiService: ApiService) {}

  ngOnInit() {
    this.data$ = this.apiService.getData().pipe(
      map(response => response.data)
    );
  }
}

Dynamic Component Loading

Dynamic component loading allows runtime determination of which components to render, ideal for highly customizable UIs.

// dynamic-loading.component.ts
import { Component, ViewChild, ViewContainerRef } from '@angular/core';
import { ComponentFactoryResolver, ComponentRef } from '@angular/core';
import { DynamicComponent } from './dynamic.component';

@Component({
  selector: 'app-dynamic-loading',
  template: `...`
})
export class DynamicLoadingComponent {
  @ViewChild('container', { read: ViewContainerRef }) container: ViewContainerRef;

  constructor(private resolver: ComponentFactoryResolver) {}

  loadComponent() {
    const factory = this.resolver.resolveComponentFactory(DynamicComponent);
    this.container.createComponent(factory);
  }
}

Local State Management

While NgRx or Redux are suitable for global state management, simple local state management is sufficient at the component level.

// local-state.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-local-state',
  template: `...`
})
export class LocalStateComponent {
  public count = 0;

  increment() {
    this.count++;
  }
}

Error Handling

Error handling is critical for robust applications. Angular provides multiple mechanisms to capture and handle errors.

// error-handling.component.ts
import { Component, OnInit } from '@angular/core';
import { catchError, tap } from 'rxjs/operators';
import { throwError } from 'rxjs';

@Component({
  selector: 'app-error-handling',
  template: `...`
})
export class ErrorHandlingComponent implements OnInit {
  public data: any;

  constructor(private apiService: ApiService) {}

  ngOnInit() {
    this.apiService.getData().pipe(
      tap(data => this.data = data),
      catchError(error => {
        console.error('Error fetching data:', error);
        return throwError(error);
      })
    ).subscribe();
  }
}

Performance Optimization

  • Change Detection Strategy: Use ChangeDetectionStrategy.OnPush to reduce unnecessary change detection.
  • Lazy Loading: Improve startup speed with module lazy loading.
  • Preloading Strategy: Preload modules or components that may be needed soon.

Internationalization and Localization

Angular provides robust support for internationalization (i18n), including date, currency formatting, and translation.

// i18n.component.ts
import { Component } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';

@Component({
  selector: 'app-i18n',
  template: `...`
})
export class I18nComponent {
  constructor(private translateService: TranslateService) {
    this.translateService.use('en'); // Set default language to English
  }
}

Web Workers

While not directly related to components, using Web Workers in Angular applications can offload CPU-intensive tasks, enhancing user experience.

// web-worker.component.ts
import { Component, OnInit } from '@angular/core';
import { WorkerService } from './worker.service';

@Component({
  selector: 'app-web-worker',
  template: `...`
})
export class WebWorkerComponent implements OnInit {
  constructor(private workerService: WorkerService) {}

  ngOnInit() {
    this.workerService.runTask();
  }
}

Code Splitting and Modularization

Organizing and splitting code effectively improves maintainability and performance.

// app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { LazyModule } from './lazy/lazy.module';

const routes: Routes = [
  { path: 'lazy', loadChildren: () => LazyModule }
];

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

Upgrading and Migration

As Angular evolves, understanding how to upgrade and migrate applications is essential.

# Upgrade Angular version
ng update @angular/core @angular/cli
Share your love