How to build a responsive Angular Frontend

Smartphones and tablet computers have become more and more important over the last couple of years. Today, it’s almost impossible to image the internet without websites that function and look great on almost any screen. However, with business apps, that is often not the case. And, to be fair, it’s often also not required. In my case, however, I wanted to add basic responsive support for an Angular frontent that I’m working on, and in this article, I’d like to present the method that I used.

Of course, it’d also be possible to just create a completely separate website or app that smartphone users will have to work with when they want to use your application. However, in my case, I only wanted to add basic support for those devices, and I wanted to do this with the least effort possible while still ensuring, that the frontend remains maintainable and clean.

My requirements

In my case, the basic support, mentioned earlier, had to include a fully functional version for tablets. So for this, I only changed the layout of the page. On smartphones, however, the app should only allow a user to perform CRUD operations. All the advanced features are inaccessible.

On a desktop computer, the app has to look like this:

Figure 1: An early mock-up of the desktop version of the control page

On tablets, the app has to include all the features. However, there are some additional buttons to allow a user to utilize the touch-screen for inputs:

Figure 2: The concept for the same page when it’s displayed on a tablet computer. This version must include the same features.

If the user attempts to access this page on a smartphone, however, a banner notifies the user that he’s only viewing a version with limited features:

Figure 3: The smartphone version displays a banner that notifies the user that not all features are available.

This might not be perfect. But these are the requirements for this app, and I think it’s important that you think about the devices that you want to support and draw a line somewhere, especially if you’re working on an enterprise application where it’s somewhat safe to assume that the company will only use a limited number of different devices (like it’s the case here). For all other devices, like smartphones in my example, there should be basic support.

Setting device breakpoints and media-queries in Angular

Naturally, a first approach to building a responsive Angular frontend would be to use media queries in a component’s stylesheet to define a few screen sizes and make the content react to the different sizes:

/* Example source: https://www.w3schools.com/howto/howto_css_media_query_breakpoints.asp */
/* Extra small devices (phones, 600px and down) */
@media only screen and (max-width: 600px) {...}

/* Small devices (portrait tablets and large phones, 600px and up) */
@media only screen and (min-width: 600px) {...}

/* Medium devices (landscape tablets, 768px and up) */
@media only screen and (min-width: 768px) {...}

/* Large devices (laptops/desktops, 992px and up) */
@media only screen and (min-width: 992px) {...}

/* Extra large devices (large laptops and desktops, 1200px and up) */
@media only screen and (min-width: 1200px) {...} 

However, that approach didn’t seem to work. Instead, you’ll have to use the BreakpointObserver to test, whether a certain condition is met:

private bpoSubscription;
private mobile = '(max-width: 915px)';
private tablet = '(min-width: 916px) and (max-width: 1384px)';
private desktop = '(min-width: 1385px)';

constructor(private bpo: BreakpointObserver)
{
    this.bpoSubscription = 
    this.bpo.observe([this.mobile, this.tablet, this.desktop]).subscribe((state : BreakpointState) => { 
        if(state.breakpoints[BreakpointService.mobile])
            // Website viewed on a small screen
        if(state.breakpoints[BreakpointService.tablet])
            // Tablet computer
        if(state.breakpoints[BreakpointService.desktop])
            // Full-size desktop
    });
}

The code snippet above demonstrates how your app can react to changes in the screen size (e.g. when the browser window gets resized or when a user rotates a smartphone). The observable stream is emitted whenever any of the media queries changes. Therefore, the event is also emitted when a component first loads. This ensures, that the right style will be displayed right away (without the need to resize the browser window).

Anyway, you’d need to add this construct to all the components that you want to make responsive. This would make the app hard to maintain and develop.

A centralized BreakpointService

Instead of adding the breakpoint detection to all the components, that need it, I created a service that any component, that needs to be responsive, can subscribe to. The service handles the breakpoint changes and notifies all of the subscribed components. This is a very simple implementation of the observer pattern:

import { Injectable } from '@angular/core';
import { BreakpointState, BreakpointObserver } from '@angular/cdk/layout';

@Injectable({
    providedIn: 'root'
})

export class BreakpointService implements IObservable
{
    private static clients : IObserver[] = [];

    private static mobile = '(max-width: 915px)';
    private static tablet = '(min-width: 916px) and (max-width: 1384px)';
    private static desktop = '(min-width: 1385px)';

    public static useDesktopStyle = false;
    public static useTabletStyle = false;
    public static useMobileStyle = false;

    private static bpoSubscription;

    constructor(private bpo: BreakpointObserver)
    {
        BreakpointService.bpoSubscription = 
        this.bpo.observe([BreakpointService.mobile, BreakpointService.tablet, BreakpointService.desktop]).subscribe((state : BreakpointState) => { 
            BreakpointService.useMobileStyle = state.breakpoints[BreakpointService.mobile];
            BreakpointService.useTabletStyle = state.breakpoints[BreakpointService.tablet];
            BreakpointService.useDesktopStyle = state.breakpoints[BreakpointService.desktop];
            BreakpointService.clients.forEach(client => {
                client.notify();
            });
        });
    }

    public register(client : IObserver) : void
    {
        BreakpointService.clients.push(client);
        client.notify();
    }

    public unregister(client : IObserver) : void
    {
        const index = BreakpointService.clients.indexOf(client, 0);

        if (index > -1)
            BreakpointService.clients.splice(index, 1);
    }
}

Any component, that wants to utilize this service, will have to implement the IObserver interface:

@Component({
  selector: 'app-control',
  templateUrl: './control.component.html',
  styleUrls: ['./control.component.scss']
})
export class ControlComponent implements OnInit, IObserver
{
  useDesktopStyle = false;
  useTabletStyle = false;
  useMobileStyle = false;

  constructor(private bps : BreakpointService)
  { }

  // The breakpoint service calls this method if the screen size changes
  notify(): void
  {
    this.useDesktopStyle = BreakpointService.useDesktopStyle;
    this.useTabletStyle = BreakpointService.useTabletStyle;
    this.useMobileStyle = BreakpointService.useMobileStyle;
  }

  ngOnDestroy()
  {
    this.bps.unregister(this);
  }

  ngOnInit()
  {
    this.bps.register(this);
  }
}

As you can see, a component registers itself and unsubscribes from the service when it doesn’t need it any more.

Note, that the BreakpointService also provides three static variables that any component can use to determine the screen size at any point in time without subscribing to the service. However, a component won’t receive status updates if it doesn’t subscribe to the service.

Ways to update the Angular App when the screen size changes

At this point, the app is able detect when the screen size changes. However, that doesn’t make it responsive. I’m sure that there are several other methods that you can use to solve this problem. However, I’ve found that the following three methods were the most useful in my case:

Use ngIf and [hidden] directives
This approach is useful when you want to remove certain elements from the DOM or just hide them in case the screen is not large enough:

<div id="left-panel" [hidden]="useTabletStyle">
<!-- content -->
</div>

The div-element in the example above will not be displayed when the useTabletStyle variable is set to true. However, the div will still remain in the DOM. Use the ngIf directive to remove it from the DOM.

Apply classes dynamically
In my opinion, this is the best approach for all the cases where you want to change the style of an element if it’s displayed on a different screen:

<div id="center-panel" [ngClass]="useMobileStyle ? 'mobile' : ''">
<!-- Content -->
</div>

In the component’s CSS file, you can do something like this:

#center-panel {
    min-width: 250px;
}

#center-panel.mobile {
    width: 100% !important;
}

This method ensures that you can maintain good separation in your code. The style-relevant code will reside in the style-sheet, the markup remains in the HTML document, and the logic lives in the component’s typescript file.

You can, however, also manipulate the classlist of an element manually, if you prefer to do it that way, but I strongly recommend that you let Angular handle that task.

Manipulate the DOM directly
You can, of course, also directly manipulate the DOM and add/remove elements or change their style. However, I suggest that you let Angular handle the things by using the two methods that I discussed so far. I just wanted to mention this option, as it was quite useful for testing things. However, you should make sure to keep the responsibilities in your code clearly separated.

The finished responsive frontend

I used this technique to ‘responsify’ a previously unresponsive Angular frontend (the one that I discussed in the beginning). Here’s a short video that shows how the layout of different components changes according to the screen size:

Summary

Responsive business apps might not be a common thing. However, I think it’s important to make sure that the layout still works on all common screen sizes (at least to an extent).

In my opinion, the observer-pattern is perfect for accomplishing this task. A centralized service detects changes in the screen size and notifies all the components that subscribed to the service.

The components can then handle the notification in whichever way they prefer to. I think that it’s best to use Angular directives to hide/remove components, and dynamic class names to change the style of components.

Leave your two cents, comment here!

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.