Contact

React to screen-size changes in Angular

Jaroslaw Zolnowski
Oct 4, 2022

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.

Responsiveness Image

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:

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:

Breakpoints Info Image

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.

Jaroslaw Zolnowski
Oct 4, 2022