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 AngularIn 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 importsimport { 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: AppHighlightDirectiveconstructor() {// actionButton, userProfile, highlightElement undefined}ngAfterViewInit() {// actionButton, userProfile, highlightElement initialized}}
@ContentChild
decorator in AngularIn 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 importsimport { 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: AppHighlightDirectiveconstructor() {// submitButton, userName, highlightElement undefined}ngAfterContentInit() {// submitButton, username, highlightElement initialized}}
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:
constructor
: JavaScript class is created (initialization)ngOnChanges
: checks a component’s inputs have been changed (initialization & update)ngOnInit
: component’s inputs have been initialized (initialization)ngDoCheck
: component’s template checked for changes (initialization & update)ngAfterViewInit
: component’s template children have been initialized (initialization)ngAfterContentInit
: component’s projected content has been initialized (initialization)ngAfterViewChecked
: component’s template child has been checked for changes (initialization & update)ngAfterContentChecked
: component’s projected content has been checked for changes (initialization & update)ngOnDestroy
: component is being destroyedIn 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 subscriptionsngOnDestroy
: execute clean-up code like unsubscribing from an observablengAfterViewInit
: access view queries and modify direct children as neededngAfterContentInit
: access content queries and modify projected content as neededngOnChanges
: inspect and process how inputs have changedimport { 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: ElementRefconstructor() {}ngOnInit() {// fetch user data using `userId` input}ngAfterViewInit() {// access userDetails}ngOnDestroy() {// clean up code before destroying component}}
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.
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 inputfirstChange
: boolean indicating whether this change is the first or notimport { Component, OnChanges, SimpleChanges, Input } from "@angular/core"@Component({selector: "app-user",standalone: true,...})export class AppUserComponent implements OnChanges {@Input() userId: string@Input() isNewUser: booleanconstructor() {}ngOnChanges(changes: SimpleChanges) {const userIdChange = changes["userId"] // userId input changeconst isNewUserChange = changes["isNewUser"] // isNewUser input changeif(userIdChange.firstChange) {// log data}// other processing}}
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 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 callreturn of({ name: "Codey", id: "12" });}}
next
callback in Angular ObserverIn 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({ // observernext: (data: { name: string; id: string }) => {// process new userthis.userData = data}})}}
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 subscriptionconst subscription = fetchUserData(this.userData.id).subscribe({ // the observernext: (data) => { ... },error: (err) => { ... },complete: () => { ... }})}}
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: stringname: string}[] = []constructor() {}ngOnInit() {// create subscriptionconst onlyActiveUsersObservable = filter((user) => user.isActive)(fetchUsersData()) // filtered observableconst mappedActiveUsersObservable = map((user) => ({id: user.id,name: `${user.firstName} ${user.lastName}`}))(onlyActiveUsersObservable) // mapped observablemappedActiveUsersObservable.subscribe(/* observer */)}}
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: stringname: string}[] = []constructor() {}ngOnInit() {// create subscriptionfetchUsersData().pipe( // pipefilter((user) => user.isActive), // operator 1map((user) => ({id: user.id,name: `${user.firstName} ${user.lastName}`})) // operator 2).subscribe(/* observer */)}}
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: stringname: string}[] = []usersSubscription?: Subscriptionconstructor() {}ngOnInit() {// create subscriptionconst usersSub = fetchUserData().subscribe(/* observer */)this.usersSubscription = usersSub}ngOnDestroy() {this.usersSubscription?.unsubscribe() // unsubscribe prior to destroying component}}
from()
in rxjs
and AngularIn 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 AngularIn 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 ObserverIn 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 trueconstructor() {}ngOnInit() {fetchUserData(this.userData.id).subscribe({ // observernext: (data) => this.userData = data,error: (error) => this.userError = true})}}
complete
callback in Angular ObserverIn 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 notificationconstructor() {}ngOnInit() {fetchUserData(this.userData.id).subscribe({ // observernext: (data) => this.userData = data,complete: () => this.showComplete = true})}}