React to screen-size changes in Angular
Overview
Imagine that you have an application where you display different content depending on the size of the screen. There are different ways to solve this issue, for example with @media css. Media queries with CSS is really helpful, but if we have business logic to implement depending on the screen size, creating custom classes with separate CSS just to handle these kinds of problems will create unnecessary chaos in our project.
This is where Angular CDK jumps in. In this article, we will focus on the solution provided by the Angular team, the two main classes from @angular/cdk/layout package: BreakpointObserver and MediaMatcher. We will also build an application that will track changes in the screen size and display different content depending on the resolution.
Let's get started!
Angular CDK Layout
The layout package helps to build responsive UIs that react to screen-size changes. With it, we can catch any resize and know when the screen exits or enters a breakpoint.
Prerequisites
If you would like to follow along with this article, you will need:
- Node.js installed locally
- @angular/cli installed globally
In this article, the examples are based on Angular CLI 14.2.2
and Node 16.13.1
.
Project Setup
Let's open a terminal, navigate to the directory where we want to create the project, and type:
ng new cdk-breakpointobserver-example
It will create and initialize a new Angular application.
Navigate to the newly created project and install @angular/cdk
package:
npm i --save @angular/cdk
To use services from Layout Module, we will first need to import it. Lets do that in app.module:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { LayoutModule } from '@angular/cdk/layout';
import { MatCardModule } from '@angular/material/card';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
BrowserAnimationsModule,
LayoutModule,
MatCardModule,
],
bootstrap: [AppComponent]
})
export class AppModule {}
Now we can start using the available utilities from the LayoutModule
.
BreakpointObserver
BreakpointObserver
lets you evaluate media queries to determine the current screen size and react to changes
when the viewport size crosses a breakpoint.
export declare class BreakpointObserver implements OnDestroy {
isMatched(value: string | readonly string[]): boolean;
observe(value: string | readonly string[]): Observable<BreakpointState>;
...
}
There are two main methods defined in the class: observe
and isMatched
. The observe
method is used to
observe when the screen-size changes between
different [breakpoints](when the viewport changes between a matching media query). The isMatched
tells us
whether one or more media queries match the current viewport size.
MediaMatcher
MediaMatcher
is a low-level utility that wraps around
JavaScript’s matchMedia, which can be
used to get a native MediaQueryList.
export declare class MediaMatcher {
matchMedia(query: string): MediaQueryList;
...
}
As we can see, it gives us access to the native MediaQueryList
object through the public method matchMedia
.
BreakpointObserver and MediaMatcher in action
Let's return to our application. We want to track the changes of the viewport size, and also list the values of each predefined breakpoint.
To do this, we need to inject BreakpointObserver
and MediaMatcher
into the class constructor and call
the observe
and matchMedia
methods for class instances.
import {ChangeDetectionStrategy, Component, OnInit} from '@angular/core';
import {BreakpointObserver, Breakpoints, BreakpointState, MediaMatcher} from '@angular/cdk/layout';
import {Observable} from 'rxjs';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent implements OnInit {
breakpointsInfo: {name: string, value: MediaQueryList}[] = [];
breakpointState: Observable<BreakpointState> | undefined;
xSmall: string = Breakpoints.XSmall;
small: string = Breakpoints.Small;
medium: string = Breakpoints.Medium;
large: string = Breakpoints.Large;
xLarge: string = Breakpoints.XLarge;
webLandscape: string = Breakpoints.WebLandscape;
constructor(public breakpointObserver: BreakpointObserver, private mediaMatcher: MediaMatcher) {}
ngOnInit(): void {
this.breakpointsInfo = this.getBreakpointsInfo();
this.breakpointState = this.breakpointObserver.observe([
Breakpoints.XSmall,
Breakpoints.Small,
Breakpoints.Medium,
Breakpoints.Large,
Breakpoints.XLarge,
Breakpoints.WebLandscape,
Breakpoints.WebPortrait,
])
}
getBreakpointsInfo(): {name: string, value: MediaQueryList}[] {
return [
Breakpoints.XSmall,
Breakpoints.Small,
Breakpoints.Medium,
Breakpoints.Large,
Breakpoints.XLarge,
Breakpoints.Web,
Breakpoints.WebLandscape,
Breakpoints.WebPortrait,
Breakpoints.Handset,
Breakpoints.HandsetLandscape,
Breakpoints.HandsetPortrait,
Breakpoints.Tablet,
Breakpoints.TabletLandscape,
Breakpoints.TabletPortrait
].map((breakpoint, index) => ({name: Object.keys(Breakpoints)[index], value: this.mediaMatcher.matchMedia(breakpoint)}));
}
}
The observe method returns Observable of BreakpointState type, that we need to subscribe to for observing when the viewport of your application changes. We can do this manually in the component class with subscribe method, or in the component template with async pipe.
<div *ngIf="breakpointState | async as state" style="display: flex; gap: 10px">
<h2 *ngIf="state.breakpoints[xSmall]">Breakpoint: Extra Small</h2>
<h2 *ngIf="state.breakpoints[small]">Breakpoint: Small</h2>
<h2 *ngIf="state.breakpoints[medium]">Breakpoint: Medium</h2>
<h2 *ngIf="state.breakpoints[large]">Breakpoint: Large</h2>
<h2 *ngIf="state.breakpoints[xLarge]">Breakpoint: Extra Large</h2>
<h2>Orientation: {{ breakpointObserver.isMatched(webLandscape) ? 'Landscape' : 'Portrait' }}</h2>
</div>
It is worth adding that we can listen for non-standard breakpoints that we define ourselves:
this.breakpointObserver.observe(['(min-width: 500px)'])
To additionally display information about the value of
individual Breakpoints, we need to
loop through breakpointsInfo
collection and display the values of the name
and value
property:
<mat-card>
<mat-card-title>Breakpoints ranges</mat-card-title>
<mat-card-content>
<ul>
<li *ngFor="let breakpoint of breakpointsInfo">
<p><strong>{{breakpoint.name}}</strong>: {{breakpoint.value.media}}</p>
</li>
</ul>
</mat-card-content>
</mat-card>
As a result, we should see the following effect:
Conclusions
We saw how to react to viewport size changes using BreakpointObserver
and MediaMatcher
classes. We can
easily handle the elements of business logic, not being limited only to files with the styles, but also have
control over them in the component and its template.
Source code with a working application described in the article can be found on the GitHub repository.
Thank you for your attention!
Looking for more support for your Angular projects?
Our team at DevIntent is highly experienced in web development, including framework migration and updates. Check out our offerings or reach out.