Codecademy Logo

Angular Component Interaction

View and Content Queries in Angular

In Angular, view and content queries allow components to retrieve references to elements (or components) instances directly defined (or projected) in its template.

Components can query for elements by referring to them in various ways, such as their component (or directive) class name or a template reference variable.

<!-- retrieve reference of direct children using view query-->
<app-users/>
<!-- retrieve reference of projected content using content query-->
<ng-content></ng-content>

@ViewChild decorator in Angular

In Angular, the @ViewChild decorator, imported from @angular/core, defines a view query to retrieve a direct child element’s ElementRef or component/directive instance.

View queries are initialized during the ngAfterViewInit part of a component’s lifecycle.

// other imports
import { Component, ViewChild, ElementRef, AfterViewInit } from "@angular/core"
@Component({
selector: "app-users",
standalone: true,
...
})
export class AppUsersComponent implements AfterViewInit {
@ViewChild('actionTemplateRef') actionButton: ElementRef
@ViewChild(AppUserProfileComponent) userProfile: AppUserProfileComponent
@ViewChild(AppHighlightDirective) highlightElement: AppHighlightDirective
constructor() {
// actionButton, userProfile, highlightElement undefined
}
ngAfterViewInit() {
// actionButton, userProfile, highlightElement initialized
}
}

@ContentChild decorator in Angular

In Angular, the @ContentChild decorator, imported from @angular/core, defines a content query to retrieve a projected child element’s ElementRef or component/directive instance.

Content queries are initialized during the ngAfterContentInit part of a component’s lifecycle.

// other imports
import { Component, ContentChild, ElementRef, AfterContentInit } from "@angular/core"
@Component({
selector: "app-user-form",
standalone: true,
...
})
export class AppUserFormComponent implements AfterContentInit {
@ContentChild('submitButtonTemplateRef') submitButton: ElementRef
@ContentChild(AppUserNameInput) userName: AppUserNameInput
@ContentChild(AppHighlightDirective) highlightElement: AppHighlightDirective
constructor() {
// submitButton, userName, highlightElement undefined
}
ngAfterContentInit() {
// submitButton, username, highlightElement initialized
}
}

Angular Component Lifecycle

In Angular, a component goes through a series of stages during its initialization and while it’s updated.

The lifecycle stages and when they are:

  1. constructor: JavaScript class is created (initialization)
  2. ngOnChanges: checks a component’s inputs have been changed (initialization & update)
  3. ngOnInit: component’s inputs have been initialized (initialization)
  4. ngDoCheck: component’s template checked for changes (initialization & update)
  5. ngAfterViewInit: component’s template children have been initialized (initialization)
  6. ngAfterContentInit: component’s projected content has been initialized (initialization)
  7. ngAfterViewChecked: component’s template child has been checked for changes (initialization & update)
  8. ngAfterContentChecked: component’s projected content has been checked for changes (initialization & update)
  9. ngOnDestroy: component is being destroyed

Angular Component Lifecycle Hooks

In Angular, custom code can be run during a component lifecycle using a lifecycle interface.

Lifecycle interfaces are imported from @angular/core and need to be implemented by the component (or directive) and override its accompanying lifecycle hook method.

Angular has best practice recommendations for using lifecycle hooks like using:

  • ngOnInit: process initial inputs to set up component subscriptions
  • ngOnDestroy: execute clean-up code like unsubscribing from an observable
  • ngAfterViewInit: access view queries and modify direct children as needed
  • ngAfterContentInit: access content queries and modify projected content as needed
  • ngOnChanges: inspect and process how inputs have changed
import { Component, OnInit, AfterViewInit, OnDestroy, ElementRef, Input, ViewChild } from "@angular/core"
@Component({
selector: "app-user",
standalone: true,
...
})
export class AppUserComponent implements OnInit, AfterViewInit, OnDestroy {
@Input() userId: string
@ViewChild('userDetailsRef') userDetails: ElementRef
constructor() {}
ngOnInit() {
// fetch user data using `userId` input
}
ngAfterViewInit() {
// access userDetails
}
ngOnDestroy() {
// clean up code before destroying component
}
}

Change detection in Angular

Angular components have a default change detection strategy applied to them when defined. This strategy is known as ChangeDetectionStrategy.Default. It checks the component tree and updates relevant components whenever actions that may affect the DOM occur, such as events and network requests.

Observing Input Changes in Angular Components

In Angular, changes to inputs can be inspected and responded to before the component’s template is checked using the ngOnChanges lifecycle hook.

ngOnChanges, unlike the other lifecycle hooks, contains an object-like parameter of type SimpleChanges (from @angular/core) where each key matches the name of the component’s input(s).

Each key‘s value is a SimpleChange object containing the properties:

  • previousValue: the previous value of the input
  • currentValue: the current value of the input
  • firstChange: boolean indicating whether this change is the first or not
import { Component, OnChanges, SimpleChanges, Input } from "@angular/core"
@Component({
selector: "app-user",
standalone: true,
...
})
export class AppUserComponent implements OnChanges {
@Input() userId: string
@Input() isNewUser: boolean
constructor() {}
ngOnChanges(changes: SimpleChanges) {
const userIdChange = changes["userId"] // userId input change
const isNewUserChange = changes["isNewUser"] // isNewUser input change
if(userIdChange.firstChange) {
// log data
}
// other processing
}
}

Using Angular with Reactive Programming

Angular uses the reactive programming paradigm to work with streams of data between components or working with asynchronous data.

In reactive programming, an observable represents the source that emits a stream of data.

An observer can “react” to the emitted data by subscribing to an observable to process the data.

import processData from './utils';
import { from } from 'rxjs';
const subscription = from([1,2,3]).subscribe((data) => processData(data));

Angular and RxJS

Angular uses the reactive programming library called rxjs to handle observables, observers, and subscriptions.

rxjs is used in many areas of Angular, such as component communication, by creating streams of data from asynchronous operations that components can emit, transform, and react to as the data changes over time.

import { Component, OnInit } from "@angular/core";
import { Observable, of } from "rxjs";
import { map } from "rxjs/operators";
@Component({
selector: 'app-user',
template: '<p>User: {{ userName }}</p>',
standalone: true
})
export class AppUserComponent implements OnInit {
userName: string = '';
ngOnInit() {
this.getUserData().pipe(
map(user => user.name)
).subscribe(name => this.userName = name);
}
private getUserData(): Observable<{ name: string, id: string }> {
// Simulating an API call
return of({ name: "Codey", id: "12" });
}
}

The next callback in Angular Observer

In Angular and rxjs, an observer can contain a next() callback function which will be called every time the observable emits data, passing in the data as an argument.

next() is a suitable place to update a component’s instance data with the latest emitted data from an observable.

import { Component, OnInit } from "@angular/core"
import { fetchUserData } from "./api"
@Component({
selector: "app-user",
standalone: true,
...
})
export class AppUserComponent implements OnInit {
userData: { name: string; id: string } = {
name: "Mike",
id: "1"
}
constructor() {}
ngOnInit() {
fetchUserData(this.userData.id).subscribe(
{ // observer
next: (data: { name: string; id: string }) => {
// process new user
this.userData = data
}
}
)
}
}

Subscribing to Observables in Angular and rxjs

In Angular and rxjs, data emitted from an observable can be “reacted” to by creating a subscription to the observable and providing an object known as an observer.

rxjs observables have a .subscribe() method used to register observers. This method returns an object of type Subscription (from rxjs).

import { Component, OnInit } from "@angular/core"
import { Subscription } from "rxjs"
import { fetchUserData } from "./api"
@Component({
selector: "app-user",
standalone: true,
...
})
export class AppUserComponent implements OnInit {
userData: { name: string; id: string } = {
name: "Mike",
id: "1"
}
constructor() {}
ngOnInit() {
// create subscription
const subscription = fetchUserData(this.userData.id).subscribe(
{ // the observer
next: (data) => { ... },
error: (err) => { ... },
complete: () => { ... }
}
)
}
}

Observable operators in Angular and rxjs

rxjs contains many built-in operators used to preprocess observables before subscribing to them.

At their core, operators receive a handler function to process the emitted data. These operators return a new function, which then receives an observable as an input, applies the provided handler function, and returns a new observable with the updated data.

Since raw observable data may not be ideal for an Angular component’s need, built-in operators like map(), filter(), and reduce() can be used to process the emitted data before subscribing to the observable.

import { Component, OnInit } from "@angular/core"
import { Subscription, map, filter } from "rxjs"
import { fetchUserData } from "./api" // observable
@Component({
selector: "app-users",
standalone: true,
...
})
export class AppUsersComponent implements OnInit {
users: {
id: string
name: string
}[] = []
constructor() {}
ngOnInit() {
// create subscription
const onlyActiveUsersObservable = filter((user) => user.isActive)(fetchUsersData()) // filtered observable
const mappedActiveUsersObservable = map((user) => ({
id: user.id,
name: `${user.firstName} ${user.lastName}`
}))(onlyActiveUsersObservable) // mapped observable
mappedActiveUsersObservable.subscribe(/* observer */)
}
}

Pipes in Angular and rxjs

In rxjs, operators are used to preprocess observable data before subscribing to it. Applying more than one operator, which is common, becomes cumbersome and confusing, so using pipes is recommended.

Pipes can contain any number of operators, and the pipe applies them to the input observable and supplies its output to the next operator.

A pipe can be created by calling the .pipe() method of an observable and passing in a comma-separated list of operators.

Finally, a pipe will output a final observable that can be subscribed to.

import { Component, OnInit } from "@angular/core"
import { Subscription, map, filter } from "rxjs"
import { fetchUserData } from "./api" // observable
@Component({
selector: "app-users",
standalone: true,
...
})
export class AppUsersComponent implements OnInit {
users: {
id: string
name: string
}[] = []
constructor() {}
ngOnInit() {
// create subscription
fetchUsersData().pipe( // pipe
filter((user) => user.isActive), // operator 1
map((user) => ({
id: user.id,
name: `${user.firstName} ${user.lastName}`
})) // operator 2
).subscribe(/* observer */)
}
}

Unsubscribing from Observables in Angular and rxjs

In rxjs, calling the .subscribe() method on an observable returns a Subscription object.

In Angular, this object can be stored and later used to remove the registered observer from the observable by calling its .unsubscribe() method (typically done in OnDestroy).

import { Component, OnInit, OnDestroy } from "@angular/core"
import { Subscription } from "rxjs"
import { fetchUserData } from "./api" // observable
@Component({
selector: "app-users",
standalone: true,
...
})
export class AppUsersComponent implements OnInit, OnDestroy {
users: {
id: string
name: string
}[] = []
usersSubscription?: Subscription
constructor() {}
ngOnInit() {
// create subscription
const usersSub = fetchUserData().subscribe(/* observer */)
this.usersSubscription = usersSub
}
ngOnDestroy() {
this.usersSubscription?.unsubscribe() // unsubscribe prior to destroying component
}
}

from() in rxjs and Angular

In rxjs, observables can be created using the from() function (from rxjs).

from(), takes an array-like, observable, or promise-like object as an argument and produces an Observable that emits the data in the array, emitted from the observable, or resolved by the promise. It streams the data out individually.

from() can be used in Angular to create observables from an array-like non-observable data.

import { Component } from "@angular/core"
import { Observable, from } from "rxjs"
import { users } from "./util/mocks" // array of users
@Component({
selector: "app-home",
standalone: true,
...
})
export class AppHomeComponent {
getUsersData: Observable<{ name: string; id: string }> = from(users) // create observable emitting each element individually
}

of() in rxjs and Angular

In rxjs, observables can be created using the of() function (from rxjs).

of(), takes a sequence of arguments and emits each element as-is. Unlike, from(), if given an array-like argument, of() will emit the entire array as opposed to each element.

of() can be used in Angular to create observables from any set of arguments, including single values, multiple values, or entire arrays.

import { Component } from "@angular/core"
import { Observable, of } from "rxjs"
import { user } from "./util/mocks" // user object
@Component({
selector: "app-home",
standalone: true,
...
})
export class AppHomeComponent {
getUserData: Observable<{ name: string; id: string}> = of(user) // create observable emitting the user object
}

error callback in Angular Observer

In Angular and rxjs, an observer can contain an optional error() callback function, which will be called every time the observable runs into an error. This callback will receive the Error encountered by the observable.

error() is a great place to handle errors that occur in observables and updating the UI.

import { Component, OnInit } from "@angular/core"
import { fetchUserData } from "./api"
@Component({
selector: "app-user",
standalone: true,
...
})
export class AppUserComponent implements OnInit {
userData: { name: string; id: string } = {
name: "Mike",
id: "1"
}
userError = false // update UI when true
constructor() {}
ngOnInit() {
fetchUserData(this.userData.id).subscribe(
{ // observer
next: (data) => this.userData = data,
error: (error) => this.userError = true
}
)
}
}

complete callback in Angular Observer

In Angular and rxjs, an observer can contain an optional complete() callback function, which will be called when the observable is finished emitting data.

complete() is a suitable place to execute code when the observable has finished emitting data and no more data will be produced.

import { Component, OnInit } from "@angular/core"
import { fetchUserData } from "./api"
@Component({
selector: "app-user",
standalone: true,
...
})
export class AppUserComponent implements OnInit {
userData: { name: string; id: string } = {
name: "Mike",
id: "1"
}
showComplete = false // show user fetched notification
constructor() {}
ngOnInit() {
fetchUserData(this.userData.id).subscribe(
{ // observer
next: (data) => this.userData = data,
complete: () => this.showComplete = true
}
)
}
}

Learn More on Codecademy