Skip to content

How to Manage Breakpoints using BreakpointObserver in Angular

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

Defining Breakpoints is important when you start working with Responsive Design and most of the time they're created using CSS code. For example:

.title {
  font-size: 12px;
}

@media (max-width: 600px) {
  .title {
    font-size: 14px;
  }
}

By default, the text size value will be 12px, and this value will be changed to 14px when the viewport gets changed to a smaller screen (a maximum width of 600px).

That solution works. However, what about if you need to listen for certain breakpoints to perform changes in your application? This may be needed to configure third-party components, processing events, or any other.

Luckily, Angular comes with a handy solution for these scenarios: the BreakpointObserver. Which is a utility for checking the matching state of @media queries.

In this post, we will build a sample application to add the ability to configure certain breakpoints, and being able to listen to them.

Project Setup

Prerequisites

You'll need to have installed the following tools in your local environment:

  • Node.js. Preferably the latest LTS version.
  • A package manager. You can use either NPM or Yarn. This tutorial will use NPM.

Creating the Angular Project

Let's start creating a project from scratch using the Angular CLI tool.

ng new breakpointobserver-example-angular --routing --prefix corp --style css --skip-tests

This command will initialize a base project using some configuration options:

  • --routing. It will create a routing module.
  • --prefix corp. It defines a prefix to be applied to the selectors for created components(corp in this case). The default value is app.
  • --style scss. The file extension for the styling files.
  • --skip-tests. it avoids the generations of the .spec.ts files, which are used for testing

Adding Angular Material and Angular CDK

Before creating the breakpoints, let's add the Angular Material components, which will install the Angular CDK library under the hood.

ng add @angular/material

Creating the Home Component

We can create a brand new component to handle a couple of views to be updated while the breakpoints are changing. We can do that using the ng generate command.

ng generate component home

Pay attention to the output of the previous command since it will show you the auto-generated files.

Update the Routing Configuration

Remember we used the flag --routing while creating the project? That parameter has created the main routing configuration file for the application: app-routing.module.ts. Let's update it to be able to render the home component by default.

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

const routes: Routes = [
  {
    path: '',
    component: HomeComponent
  }
];

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

Update the App Component template

Remove all code except the router-outlet placeholder:

<!-- app.component.html -->
<router-outlet></router-outlet>

This will allow rendering the home component by default once the routing configuration is running.

Using the BreakpointObserver

The application has the Angular CDK installed already, which has a layout package with some utilities to build responsive UIs that react to screen-size changes.

Let's update the HomeComponent, and inject the BreakpointObserver as follows.

//home.component.ts
import { Component, OnInit } from '@angular/core';
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';

@Component({
  selector: 'corp-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {

  readonly breakpoint$ = this.breakpointObserver
    .observe([Breakpoints.Large, Breakpoints.Medium, Breakpoints.Small, '(min-width: 500px)'])
    .pipe(
      tap(value => console.log(value)),
      distinctUntilChanged()
    );

  constructor(private breakpointObserver: BreakpointObserver) { }

  ngOnInit(): void {
  }
}

Once the BreakpointObserver is injected, we'll be able to evaluate media queries to determine the current screen size, and perform changes accordingly.

Then, a breakpoint$ variable references an observable object after a call to the observe method.

The observe method gets an observable of results for the given queries, and can be used along with predetermined values defined on Breakpoints as a constant.

Also, it's possible to use custom breakpoints such as (min-width: 500px). Please refer to the documentation to find more details about this.

Next, you may need to subscribe to the breakpoint$ observable to see the emitted values after matching the given queries.

Again, let's update the home.component.ts file to do that.

// home.component.ts

// .... other imports
import { distinctUntilChanged, tap } from 'rxjs/operators';

@Component({
  //....
})
export class HomeComponent implements OnInit {

  Breakpoints = Breakpoints;
  currentBreakpoint:string = '';

  // ... readonly breakpoint$ = this.breakpointObserver

  constructor(private breakpointObserver: BreakpointObserver) { }

  ngOnInit(): void {
    this.breakpoint$.subscribe(() =>
      this.breakpointChanged()
    );
  }
  private breakpointChanged() {
    if(this.breakpointObserver.isMatched(Breakpoints.Large)) {
      this.currentBreakpoint = Breakpoints.Large;
    } else if(this.breakpointObserver.isMatched(Breakpoints.Medium)) {
      this.currentBreakpoint = Breakpoints.Medium;
    } else if(this.breakpointObserver.isMatched(Breakpoints.Small)) {
      this.currentBreakpoint = Breakpoints.Small;
    } else if(this.breakpointObserver.isMatched('(min-width: 500px)')) {
      this.currentBreakpoint = '(min-width: 500px)';
    }
  }
}

In the above code, the ngOnInit method is used to perform a subscription to the breakpoint$ observable and the method breakpointChanged will be invoked every time a breakpoint match occurs.

As you may note, the breakpointChanged method verifies what Breakpoint value has a match through isMatched method. In that way, the current component can perform changes after a match happened (in this case, it just updates the value for the currentBreakpoint attribute).

Using Breakpoint values on the Template

Now, we can set a custom template in the home.component.html file and be able to render a square according to the currentBreakpoint value.

<!-- home.component.html -->

<mat-card style="text-align: center; margin: 20px">
  {{ currentBreakpoint }}
</mat-card>

<div class="container" [ngSwitch]="currentBreakpoint">
  <span class="large" *ngSwitchCase="Breakpoints.Large"> Large </span>
  <span class="medium" *ngSwitchCase="Breakpoints.Medium"> Medium </span>
  <span class="small" *ngSwitchCase="Breakpoints.Small"> Small </span>
  <span class="custom" *ngSwitchCase="'(min-width: 500px)'"> Custom </span>
</div>

The previous template will render the current media query value at the top along with a rectangle according to the size: Large, Medium, Small or Custom.

Live Demo and Source Code

Want to play around with this code? Just open the Stackblitz editor or the preview mode in fullscreen.

Find the complete angular project in this GitHub repository: breakpointobserver-example-angular. Do not forget to give it a star ⭐️ and play around with the code.

Feel free to reach out on Twitter if you have any questions. Follow me on GitHub to see more about my work.

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

You might also like

End-to-end type-safety with JSON Schema cover image

End-to-end type-safety with JSON Schema

End-to-end type-safety with JSON Schema I recently wrote an introduction to JSON Schema post. If you’re unfamiliar with it, check out the post, but TLDR: It’s a schema specification that can be used to define the input and output data for your JSON API. In my post, I highlight many fantastic benefits you can reap from defining schemas for your JSON API. One of the more interesting things you can achieve with your schemas is end-to-end type safety from your backend API to your client application(s). In this post, we will explore how this can be accomplished slightly deeper. Overview The basic idea of what we want to achieve is: * a JSON API server that validates input and output data using JSON Schema * The JSON Schema definitions that our API uses transformed into TypeScript types With those pieces in place, we can achieve type safety on our API server and the consuming client application. The server side is pretty straightforward if you’re using a server like Fastify with already enabled JSON Schema support. This post will focus on the concepts more than the actual implementation details though. Here’s a simple diagram illustrating the high-level concept: We can share the schema and type declaration between the client and server. In that case, we can make a request to an endpoint where we know its type and schema, and assuming the server validates the data against the schema before sending it back to the client, our client can be confident about the type of the response data. Marrying JSON Schema and TypeScript There are a couple of different ways to accomplish this: * Generating types from schema definitions using code generation tools * Creating TypeBox definitions that can infer TypeScript types and be compiled to JSON Schema I recommend considering both and figuring out which would better fit your application and workflows. Like anything else, each has its own set of trade-offs. In my experience, I’ve found TypeBox to be the most compelling if you want to go deep with this pattern. Code generation A couple of different packages are available for generating TS types from JSON Schema definitions. * https://github.com/bcherny/json-schema-to-typescript * https://github.com/vega/ts-json-schema-generator They are CLI tools that you can provide a glob path to where your schema files are located and will generate TS declaration files to a specified output path. You can set up an npm hook or a similar type of script that will generate types for your development environment. TypeBox TypeBox is a JSON Schema type builder. With this approach, instead of json files, we define schemas in code using the TypeBox API. The TypeBox definitions infer to TypeScript types directly, which eliminates the code generation step described above. Here’s a simple example from the documentation of a JSON Schema definition declared with TypeBox: ` This can then be inferred as a TypeScript type: ` Aside from schemas and types, TypeBox can do a lot more to help us on our type-safety journey. We will explore it a bit more in upcoming sections. Sharing schemas between client and server applications Sharing our JSON Schema between our server and client app is the main requirement for end-to-end type-safety. There are a couple of different ways to accomplish this, but the simplest would be to set up our codebase as a monorepo that contains both the server and client app. Some popular options for TypeScript monorepos are: PNPM, Turborepo, and NX. If a monorepo is not an option, you can publish your schema and types as a package that can be installed in both projects. However, this setup would require a lot more maintenance work. Ultimately, as long as you can import your schemas and types from the client and server app, you are in good shape. Server-to-client validation and type-safety For the sake of simplicity, let's focus on data flowing from the server to the client for now. Generally speaking, the concepts also apply in reverse, as long as your JSON API server validates your inputs and outputs. We’ll look at the most basic version of having strongly typed data on the client from a request to our server. Type-safe client requests In our server application, if we validate the /users endpoint with a shared schema - on the client side, when we make the request to the endpoint, we know that the response data is validated using the user schema. As long as we are confident of this fact, we can use the generated type from that schema as the return type on our client fetch call. Here’s some pseudocode: ` Our server endpoint would look something like this: ` You could get creative and build out a map that defines all of your endpoints, their metadata, and schemas, and use the map to define your server endpoints and create an API client. Transforming data over the wire Everything looks stellar, but we can still take our efforts a bit further. To this point, we are still limited to serialized JSON data. If we have a created_at field (number or ISO string) tied to our user, and we want it to be a Date object when we get a hold of it on the client side - additional work and consideration are required. There are some different strategies out there for deserializing JSON data. The great thing about having shared schemas between our client and server is that we can encode our type information in the schema without sending additional metadata from our server to the client. Using format to declare type data In my initial JSON Schema blog post, I touched on the format field of the specification. In our schema, if the actual type of our date is a string in ISO8601 format, we can declare our format to be "date-time". We can use this information on the client to transform the field into a proper Date object. ` Transforming serialized JSON Data This can be a little bit tricky; again, there are many ways to accomplish it. To demonstrate the concept, we’ll use TypeBox to define our schemas as discussed above. TypeBox provides a Transform type that you can use to declare, encode, and decode methods for your schema definition. ` It even provides helpers to statically generate the decoded and encoded types for your schema ` If you declare your decode and encode functions for your schemas, you can then use the TypeBox API to handle decoding the serialized values returned from your JSON API. Here’s what the concept looks like in practice fetching a user from our API: ` Nice. You could use a validation library like Zod to achieve a similar result but here we aren’t actually doing any validation on our client side. That happened on the server. We just know the types based on the schema since both ends share them. On the client, we are just transforming our serialized JSON into what we want it to be in our client application. Summary There are a lot of pieces in play to accomplish end-to-end type safety. With the help of JSON Schema and TypeBox though, it feels like light work for a semi-roll-your-own type of solution. Another great thing about it is that it’s flexible and based on pretty core concepts like a JSON API paired with a TypeScript-based client application. The number of benefits that you can reap from defining JSON Schemas for your APIs is really great. If you’re like me and wanna keep it simple by avoiding GraphQL or other similar tools, this is a great approach....

Angular 18 Announced: Zoneless Change Detection and More cover image

Angular 18 Announced: Zoneless Change Detection and More

Angular 18 Announced: Zoneless Change Detection and More Angular 18 has officially landed, and yet again, the Angular team has proven that they are listening to the community and are committed to continuing the framework's renaissance. The release polishes existing features, addresses common developer requests, and introduces experimental zoneless change detection. Let's examine the major updates and enhancements that Angular 18 offers. 1. Zoneless Angular One of the most exciting features of Angular 18 is the introduction of experimental support for zoneless change detection. Historically, Zone.js has been responsible for triggering Angular's change detection. However, this approach has downsides, especially regarding performance and debugging complexity. The Angular team has been working towards making Zone.js optional, and Angular 18 marks a significant milestone in this journey. Key Features of Zoneless Angular 1. Hybrid Change Detection: In Angular 18, a new hybrid change detection mode is enabled by default. This mode allows Angular to listen to changes from signals and other notifications regarding changes that occur either inside an Angular zone or not. That effectively means you can write (library) code that works regardless of whether Zone.js is being used, paving the way to fully zoneless apps without compromising backward compatibility. For new applications, Angular enables zone coalescing by default, which removes the number of change detection cycles and improves performance. For existing applications, you can enable zone coalescing by configuring your NgZone provider in bootstrapApplication: ` 2. Experimental API for Zoneless Mode: Angular 18 introduces an experimental API to disable Zone.js entirely. This API allows developers to run applications in a fully zoneless mode, paving the way for improved performance and simpler debugging. The zoneless change detection requires an entirely new scheduler which relies on notifications from the Angular core APIs, such as ChangeDetectorRef.markForCheck (called automatically by the AsyncPipe), ComponentRef.setInput, signal updates, host listener updates, or attaching a view that was marked dirty. 3. Improved Composability and Interoperability: Zoneless change detection enhances composability for micro-frontends and interoperability with other frameworks. It also offers faster initial render and runtime, smaller bundle sizes, more readable stack traces, and simpler debugging. How to Try Zoneless Angular To experiment with zoneless change detection, you can add the provideExperimentalZonelessChangeDetection provider to your application bootstrap: ` After adding the provider, remove Zone.js from your polyfills in angular.json. You can read more about the experimental zoneless change detection in the official documentation. By the way, angular.dev is now considered the official home for Angular developers! 2. Server-side Rendering and Hydration Enhancements A feature I'm particularly excited about is the improvements to Angular's server-side rendering (SSR) and hydration in terms of developer experience and debugging: 1. Enhanced DevTools Support: Angular DevTools now includes overlays and detailed error breakdowns to help visualize and debug hydration issues directly in the browser. This refreshing focus on developer experience shows that the Angular team wants to make the framework more approachable and user-friendly. 2. Hydration Support for Angular Material: All Angular Material components now support client hydration and are no longer skipped, which enhances performance and user experience. 3. Event Replay: Available in developer preview, event replay captures user interactions during SSR and replays them once the application is hydrated, ensuring a seamless user experience before complete hydration. It is powered by the same library as Google Search. 4. i18n Hydration Support: Up to v17, Angular skipped hydration for components with i18n blocks. From v18, hydration support for i18n blocks is in developer preview, allowing developers to use client hydration in internationalized applications. 3. Stable Material Design 3 After introducing experimental support for Material Design 3 in Angular 17, Angular 18 now includes stable support. The key features of Material Design 3 in Angular 18 include: 1. Simplified Theme Styles: Based on CSS variables, the new theming styles offer more granular customization and a flexible API for applying color variants to components. 2. Theming Generation Schematics: Using the Ng CLI, you can generate Material 3 themes. 3. Sass APIs: New Sass APIs allow developers to read colors, typography, and other properties from the Material 3 theme, making it easier to create custom components. How to use Material Design 3 in Angular 18 To use Material Design 3 in Angular 18, you can define a theme in your application's styles.scss file using the mat.defineTheme function: ` Or generate a Material 3 theme using the Ng CLI: ` You can then apply the theme to your application using the mat.theme mixin: ` Head to the official guide for a more detailed guide. You'll also notice they have refreshed the docs with the new themes and further documentation. 4. Signal-Based APIs The path to fully signal-based components includes new signal inputs, model inputs, and signal query APIs. We already wrote about them as they were in developer-preview in v17, but they have been further refined in v18. These APIs offer a type-safe, reactive way to manage state changes and interactions within components: 1. Signal Input API: Signal inputs allow values to be bound from parent to child components. Those values are exposed using a signal and can change during the component's life cycle. 2. Model Input API: Model inputs are a special input type that enables a component to propagate new values back to the parent component. That allows developers to keep the parent component in sync with the child component with two-way binding. 3. Signal Query API: This was a particularly requested feature from the community. The signal query APIs work the same way as ViewChild and ContentChild under the hood, but they return signals, providing more predictable timing and type safety. 5. Fallback Content For ng-content A very requested feature from the community, Angular 18 introduces a new ng-content directive that allows developers to define fallback content when no content is projected into a component. This feature is particularly useful for creating reusable components with default content. Here's an example of using the new ng-content directive. Using the following component ` like this ` will render Howdy World. 6. Other Improvements In addition to the major updates mentioned above, Angular 18 also includes several other improvements and updates: 1. TypeScript 5.4: Angular 18 now supports TypeScript 5.4, which lets you take advantage of new features such as preserved narrowing in closures following last assignments. 2. Global Observable in Angular Forms: Angular 18 introduces a global events observable in Angular forms, which allows you to track all changes around any abstract control and its children, including the touched or dirty in a single observable. Here's an example of how you can use the global observable: ` 3. Stable Deferrable views: Deferrable views are now stable in Angular 18. 4. Stable Control Flow: The built-in control flow is now stable in Angular 18! It is more performant than its predecessor. It also received improved type checking, including guardrails for certain performance-related anti-patterns. 5. Route Redirects as Functions: For added flexibility in managing redirects, Angular v18 now lets you use a function for redirectTo that returns a string, which allows you to create more sophisticated redirection logic based on runtime conditions. For example: ` Conclusion Angular 18 is a significant release that brings many new features, enhancements, and experimental APIs to the Angular ecosystem. The introduction of zoneless change detection, improvements to server-side rendering and hydration, stable Material Design 3 support, signal-based APIs, and fallback content for ng-content are just a few of the highlights of this release. The Angular team has again demonstrated its commitment to improving the framework's developer experience, performance, and flexibility. It also demonstrated a clear vision for Angular's future. If you're curious about what's next, you can check out the Angular roadmap....

Why Migrate from Protractor to Cypress? cover image

Why Migrate from Protractor to Cypress?

Great memories come to my mind when I read about Protractor. I remember those days when I was working actively using the AngularJS framework to build Single-Page Applications, and Protractor was the most viable option for writing E2E(End-to-End) tests. In fact, Protractor was created in 2013 when AngularJS was the most popular web framework, and it allowed developers to run tests through a web application in a real browser, along with interaction commands to mimic the user behavior. As expected, the ecosystem around JavaScript technologies has been changing continuously, with new tools emerging that afford developers more modern options for writing tests. Is this the right time to think of other alternatives? In this post, I'll give you some reasons to migrate to Cypress since it's one of the best alternatives for E2E testing. Protractor Will Be Deprecated The Angular team has announced the deprecation of Protractor in May 2021. Some of the reasons behind these deprecation plans are: - State of Protractor. Protractor is dependent on selenium-webdriver, and is not able to upgrade to the new version without introducing a huge breaking change, and forcing users to do a migration for all their tests. - Web Testing Landscape. There are different testing solutions today, and they offer better stability and improved support for features like cross-browser testing. The current deprecation timeline looks like this: - Angular version 12 (May 2021) - Announcement of deprecation - Angular CLI will not include Protractor for new projects - Show warning when @angular-devkit/build-angular:protractor is used - Only PRs that address security issues and fixes for browser releases that break Protractor tests will be merged - Angular version 15 (end of 2022) - End of development on Protractor There is a closed RFC with the future of Angular E2E & Plans for Protractor on GitHub with more details about this. Cypress supports Interactive Testing First, we'll need to understand that as part of the _Cypress tooling, there's a Test Runner and a Dashboard Service available for your test suite. - The Test Runner is an open source project available to run your test suite in an interactive environment in real-time. While it's in progress, a command log will display the results of your tests. - The Dashboard Service can record your test run, and enables access to your results. The above screenshot shows an example of the test runner in real-time, where you will find an intuitive way to navigate through the different steps in your tests. Cypress Has a Huge Ecosystem That’s true if you consider the number of resources you can find about Cypress nowadays. It's evolving every day, and it's adopted by developers and QA engineers using modern JavaScript frameworks. As part of the Angular ecosystem, we can mention one of the biggest players: Nx, which is widely used with Angular projects. For this tool, Cypress is used by default while generating new applications. > Cypress will be used by default when generating new applications. If you want to continue using Protractor, set the e2eTestRunner to protractor in the schematics section of the angular.json file. In case you're using Nx, please refer to the Updating your E2E testing configuration guide for more information. How to Perform the Migration? The Cypress team is actively working on the Cypress Angular Schematic, which allows adding Cypress for E2E testing in your Angular projects. You'll need to run the following command and pay attention to the output. ` Here's what happens in the background: * Cypress tooling gets installed in your project. * Some npm scripts are added in your package.json file to run your tests. * "cypress:open": "cypress open". This script will open your browser and render the content of your app. * "cypress:run": "cypress run". This script will run your tests headlessly (the browser will not be opened). * Cypress configuration files and directories are created. * Initial tests and scripts are generated so that you can run Cypress for the first time. * It has the ability to update the default ng e2e command to use Cypress instead of Protractor. If those steps don't work for you, you may need to do the upgrade manually and the good news is that Cypress documentation has you covered already: Manual Installation. Are you ready to learn more about Cypress? Do not miss this topic on our blog site. Feel free to reach out on Twitter if you have any questions. Follow me on GitHub to see more about my work....

What does it actually look like to build software with AI today? Not in theory, but in practice. cover image

What does it actually look like to build software with AI today? Not in theory, but in practice.

What does it actually look like to build software with AI today? Not in theory, but in practice. At the Leadership Exchange, this was the question at the center of the Developer Panel, where leaders from across the industry unpacked what’s really changing inside engineering teams and what organizations need to do right now to keep up. The Developer Panel at the Leadership Exchange explored the cutting edge of AI in software engineering and examined what organizations should focus on today to prepare for the future. Moderated by Jeff Cross, Co-Founder & CEO at Nx, the panel featured Victor Savkin, Cofounder & CTO at Nx, Alex Sover, Vice President of Engineering at OpenAP, Brent Zucker, Senior Director of Engineering at Visa, and Jonathan Fontanez, AI Engineering Lead at This Dot Labs. Panelists shared insights into how AI is transforming the software development lifecycle and how teams can adopt tools effectively while preparing for organizational change. Panelists discussed emerging workflows, including CI-in-the-loop, agentic healing, and context engineering. They examined how validation, code reviews, and PRDs are evolving alongside AI capabilities and how teams are integrating external sources such as production traces to improve quality and reliability. The discussion also covered what the next generation of agentic tools might look like and how these capabilities will shape engineering practices in the near future. Adoption of AI comes with challenges. Teams often rely on plugins or extensions without foundational understanding, and individual contributors may fear displacement. Panelists emphasized that education, governance, and skill-building are essential for teams to manage AI agents effectively while maintaining quality. They also highlighted the need to standardize workflows and ensure organizational alignment to fully leverage AI capabilities. The conversation extended beyond technical challenges to organizational implications. Panelists discussed how teams can avoid issues like Conway’s Law, manage distributed teams effectively, and evolve engineering practices alongside AI adoption. Leadership and management strategies play a crucial role in ensuring that AI integration delivers meaningful outcomes while maintaining efficiency and alignment with business objectives. Key Takeaways - AI workflows require both technical and organizational preparation. - Education, governance, and skill development are essential for successful implementation. - Forward-looking teams are rethinking validation, CI pipelines, and context management to fully leverage agentic AI. The discussion highlighted that adopting AI at the cutting edge is not just about new tools - it is about rethinking processes, workflows, and organizational culture. Companies that embrace this holistic approach are most likely to succeed in leveraging AI to its full potential. Are you interested in more conversations like this? Message us for an invite to the next, or for a private discussion around these topics. Tracy can be reached at tlee@thisdot.co....

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co