Thursday, 13 June, 2019 UTC


Summary

Most of the time, we use CSS media queries to handle responsive, screen size changes to layout our content differently. However, there are times where CSS media queries alone isn't sufficient for that. We need to handle the responsiveness in our code.
In this article, I would like to share about how to detect responsive breakpoints in Angular, with a twist - we don't maintaining responsive breakpoint sizes in your Typescript code (because responsive breakpoints are already defined in CSS).
We will use Angular with Bootstrap in this example, but it works for any CSS frameworks and classes. Let's start.
What's the Plan
We will be using CSS Classes to determine the current responsive breakpoints. There are 5 breakpoints in Bootstrap CSS. The CSS classes to determine the visibility of each breakpoints is:
  • Visible only on xs: .d-block .d-sm-none
  • Visible only on sm: .d-none .d-sm-block .d-md-none
  • Visible only on md: .d-none .d-md-block .d-lg-none
  • Visible only on lg: .d-none .d-lg-block .d-xl-none
  • Visible only on xl: .d-none .d-xl-block
The CSS display property will be toggled between none or block. We will apply these classes to HTML elements.
Everytime when screen size changes, we will loop and find the HTML element with style display: block, this is how we will detect the current breakpoint.
Here is the code if you are too excited to see the solution: https://stackblitz.com/edit/angular-size.
The Implementation: Component
Let's create an Angular component size-detector.
The component HTML template:
<!-- size-detector.component.html -->
<div *ngFor="let s of sizes" class="{{s.css + ' ' + (prefix + s.id) }}">{{s.name}}</div>
The component Typescript code:
// size-detector.component.ts
...
export class SizeDetectorComponent implements AfterViewInit {
  prefix = 'is-';
  sizes = [
    {
      id: SCREEN_SIZE.XS, name: 'xs', css: `d-block d-sm-none`
    },
    {
      id: SCREEN_SIZE.SM, name: 'sm', css: `d-none d-sm-block d-md-none`
    },
    {
      id: SCREEN_SIZE.MD, name: 'md', css: `d-none d-md-block d-lg-none`
    },
    {
      id: SCREEN_SIZE.LG, name: 'lg', css: `d-none d-lg-block d-xl-none`
    },
    {
      id: SCREEN_SIZE.XL, name: 'xl', css: `d-none d-xl-block`
    },
  ];

  @HostListener("window:resize", [])
  private onResize() {
    this.detectScreenSize();
  }

  ngAfterViewInit() {
    this.detectScreenSize();
  }

  private detectScreenSize() {
    // we will write this logic later
  }
}
After looking at the component code, you might be wondering where is those SCREEN_SIZE.* value come from. It is an enum. Let's create the screen size enum (You may create a new file or just place the enum in same component file)
// screen-size.enum.ts

/_ An enum that define all screen sizes the application support _/
export enum SCREEN_SIZE {
  XS,
  SM,
  MD,
  LG,
  XL
}
Also, remember to add Bootstrap to your project! You may add it via npm or yarn, but in this example, we will use the easier way. Add the cdn link in index.html.
<!-- index.html -->
<link rel="stylesheet" 
    href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
The code is pretty expressive itself.
  1. First, we define a list of sizes that we support and the CSS classes that used to determine each breakpoints.
  2. In the HTML, we loop through the size list, create div element, assign css and display it. Also note that we give each div an additional unique css class is-<SIZE_ENUM>.
  3. We have a function detectScreenSize. This is where we will write our logic to detect the screen size changes. We will complete that later.
  4. We need to run the logic to everytime when screen size changes. We use the HostListener decorator to listen to the window resize event.
  5. We also need to run the logic when we first initialize the application. We need to run it during the AfterViewInit component lifecycle hook.
The Implementation: Service & Component
Now we have the component code "almost" ready, let's start implementing our resize service.
// resize.service.ts

@Injectable()
export class ResizeService {

  get onResize$(): Observable<SCREEN_SIZE> {
    return this.resizeSubject.asObservable().pipe(distinctUntilChanged());
  }

  private resizeSubject: Subject<SCREEN_SIZE>;

  constructor() {
    this.resizeSubject = new Subject();
  }

  onResize(size: SCREEN_SIZE) {
    this.resizeSubject.next(size);
  }

}
The resize service code is simple:
  1. We create a rxjs subject resizeSubject.
  2. We have a public method onResize that receive size as the parameter. It will then push the value to the resize stream. (We will call this method later in our size-detector component)
  3. Notice that we use distinctUntilChanged operator in the resize observable. We use that to reduce unnecessary notification. For example, when your screen size change from 200px to 300px, it is still consider as xs size in bootstrap. We don't need to notify in that case. (You can remove the operator if you need)
  4. We export the the resize stream as observable via onResize$. Any components, services, directives, etc can then subscribe to this stream to get notify whenever size is changed.
Next, let's go back to our size-detector component and update the detectScreenSize logic.
// size-detector.component.ts
...

private detectScreenSize() {
    constructor(private elementRef: ElementRef, private resizeSvc: ResizeService) { }

    const currentSize = this.sizes.find(x => {
      // get the HTML element
      const el = this.elementRef.nativeElement.querySelector(`.${this.prefix}${x.id}`);

      // check its display property value
      const isVisible = window.getComputedStyle(el).display != 'none';

      return isVisible;
    });

    this.resizeSvc.onResize(currentSize.id);
}

...
Let's breakdown and go through the logic together:
  1. First, we will need to inject the ElementRef and our newly created ResizeService to our component.
  2. Base on our CSS classes, at any point of time, there will be ONLY ONE HTML element visible. We loop through our sizes array and find it.
  3. For each size of our sizes array, we will use HTML5 element's query selector to find the element by the unique css class we defined earlier on is-<SIZE_ENUM>.
  4. Once we find the current visible element, we then notify our resize service by calling the onResize method.
Using the Service and Component
You may place the size-detector component under our root component app-component. For example:
<!-- app.component.html -->

<hello name="{{ name }}"></hello>
<!-- Your size-detector component place here -->
<app-size-detector></app-size-detector>
In this example, I have another hello-component in the app-component, but that doesn't matter.
Since I place the component in app-component, means I can use the ResizeService everywhere (directives, components, services, etc).
For instance, let's say I want to detect the screen size changes in hello-component, I can do so by inject the ResizeService in constructor, then subscribe to the onSizeChange$ observable and do what I need.
// hello.component.ts

@Component({
  selector: 'hello',
  template: `<h1>Hello {{size}}!</h1>`,
})
export class HelloComponent  {

  size: SCREEN_SIZE;

  constructor(private resizeSvc: ResizeService) { 
    // subscribe to the size change stream
    this.resizeSvc.onResize$.subscribe(x => {
      this.size = x;
    });
  }

}
In the above code, we detect the screen size changes and simply display the current screen size value.
See it in action!
One of the real life use case scenario might be you have accordion on screen. In mobile, you would like to collapse all accordion panels, show only the active one at a time. However, in desktop, you might want to expand all panel.
Summary
This is how we can detect the screen size changes without maintaining the actual breakpoint sizes in our JavaScript code. Here is the code: https://stackblitz.com/edit/angular-size.
If you think of it, it is not very often that the user changes the screen size when browsing the app. You may handle the screen sizes changes application wide (like our example above) or just handle it everytime you need it (per use case / component basis).
Besides that, if you don't mind to duplicate and maintain the breakpoint sizes in JavaScript code, you may remove the component, move the detectScreenSize into your service and change a bit on the logic. It is not difficult to implement that. (Try it probably?)
That's all. Happy coding!