Reactive Responsive Design in Practice: Part 2

ngconf

ng-conf

Posted on March 1, 2021

Reactive Responsive Design in Practice: Part 2

Michael Madsen | ng-conf | Sep 2020

In my last article, I introduced the concept of Reactive Responsive Design (a term created by me), what it is, how it works, and why it’s important. Now let’s take a look at how we can use it to build a responsive UI. To start we need a service.

The purpose of the service is to provide unified breakpoints for screen sizes. This will unify the breakpoints an app cares about with code rather than convention. There are 3 things that will need to happen in the service.

  1. Define the sizes we want tracked
  2. Provide an observable with a standardized interface
  3. Define specific states we care about (like mobile or tablet)

rxrs provides a service (called the ScreenSizeService) with reasonable defaults for this. In Angular you will want to use the rxrs-ng library so everything plays nicely with Angular.


To get set up, first run:

npm install rxrs rxrs-ng

Then import the ScreenSizeService into your component.

import { Component, OnInit } from '@angular/core';
import { ScreenSizeService, ScreenWidthEnum } from 'rxrs-ng';

@Component({
  selector: 'app-main',
  templateUrl: './main.component.html',
  styleUrls: ['./main.component.scss'],
})
export class MainComponent implements OnInit {
  screenSize$ = this.screenSizeService.size$;

  constructor(private screenSizeService: ScreenSizeService) {}

  ngOnInit() {}
}
Enter fullscreen mode Exit fullscreen mode

To see the service in action let’s examine it from the perspective of solving the issues we have with responsive design discussed in the last article. They are:

  1. Where is my code!?
  2. Teams can end up with different queries.
  3. Can only query screen width.
  4. Code will always load.
  5. Hard to test.

Let’s jump right in!

Where is my code!?

This issue revolves around what I call media query bloat. Query bloat is when you have a lot of classes in your css repeated under different media queries. On a small scale, it looks alright but gets really unruly as your page/app grows. Here is a small example of query bloat.

.title {
  font-size: 2em;
}

.another-one {
  background-color: red;
}

@media screen and (max-width: 599px) {
  .title {
    font-size: 5em;
  }

  .another-one {
    background-color: aliceblue;
  }
}

@media screen and (min-width: 600px) and (max-width: 1023px) {
  .title {
    font-size: 4em;
  }

  .another-one {
    background-color: antiquewhite;
  }
}
Enter fullscreen mode Exit fullscreen mode

To address query bloat with rxrs use the ScreenSizeService. In your html you can template your classes using the size$ observable to dynamically generate your classes.

class=”title title-{{(size$| async)}}”

This creates an element the base class of title and the dynamic class of title-{current size} Using this strategy for our classes in the query bloat example produces something like the following.

.title {
  font-size: 2em;
}

.title-xSmall {
  font-size: 5em;
}

.title-small {
  font-size: 4em;
}

.another-one {
  background-color: red;
}

.another-one-xSmall {
  background-color: aliceblue;
}

.another-one-small {
  background-color: antiquewhite;
}
Enter fullscreen mode Exit fullscreen mode

Teams can end up with different queries

It’s better to enforce code standards automatically than to depend on code reviews. You get all of the consistency, quality, and velocity without any of the delay or dissent. It’s very easy for one team member to (either mistakenly or intentionally) set different breakpoints in their media queries. This can lead to undesirable layouts in some screen sizes. Just imagine the weird layouts possible if your top bar switched to mobile layouts at different points than the rest of your app. Just imagine trying to use Facebook on a tablet where it had the desktop version of the top bar and the mobile version of the body. A large portion of the functionality would be inaccessible.

In most cases, breakpoints are enforced by convention. The convention is often defined in some document lost on confluence, or wherever you store documentation, saying what the breakpoints should be. But with rxrs they’re defined in code in the ScreenSizeService so there isn’t an option to use unsanctioned breakpoints.

Can only query screen width

One failing of media queries is that they can only query the viewport’s width. This limits what can be accomplished with media queries alone. Imagine you need to build a page where you want the main page of your app to switch to the styling for a tablet when a large slide out is open. With traditional media queries you would be out of luck and need to figure out another strategy for that case.

Screen sizes using rxrs are Observables in your Typescript code. That means you have a lot of options that easily fit into your responsive page paradigm. For instance, you could map the size$ observable to take into account the state of the slide out.

screenSize$ = this.screenSizeService.size$.pipe(
  switchMap((screenSize) => {
    return toolBarOpen$.pipe(
      map((toolBarOpen) => {
        return screenSize === ScreenWidthEnum.large && toolBarOpen ?
          ScreenWidthEnum.medium :
          screenSize;
      })
    )
  })
);
Enter fullscreen mode Exit fullscreen mode

You could build a view state Observable for a more complex example. This topic will be treated in a later article. Using a view state observable is a large topic that will come in a later article. In the meantime, you should watch my talk from rxjs live!

Code will always load

To show the impact of this think about these questions.

  1. What is the screen size for the lowest performance device using your site?
  2. Does that screen size have fewer elements on the page?

For question 1, if you support mobile, the screen size of the lowest end devices are likely phones with small screens. Small screens often have fewer elements on the page because of the constrained proportions. This all seems OK until you realize that an element from the desktop version of your app is hidden by a media query for your mobile version, and that media queries just set visibility: none; on the element. That means that the component still is created by the framework, added to the DOM, then hidden.

While that might not be a huge issue for you, it can be more impactful than you think. Consider the case where there are heavy data requirements on that element not needed in the mobile version. You can use rxrs to address this situation! Use the size$ Observable to wrap the element that your media query would hide in an *ngIf — or its equivalent in your framework of choice.

<!-- Don't generate this component if in mobile -->
<big-component *ngIf="!(size$ | async) === 'small'"></big-component>
Enter fullscreen mode Exit fullscreen mode

With this, the component will only be created and added to the dom if the screen size isn’t small. This can make your app more performant on mobile!

Hard to test

I am passionate about testing, so it drives me crazy that you can’t really test css without taking pictures of your app and comparing them with older pictures (visual regression testing). rxrs allows layout testing directly in a component’s unit tests. This isn’t a testing article, but testing with rxrs is simply observable testing! This is a very powerful tool for complex layouts when used with a viewState Observable, and there is more to come in a subsequent article!

Conclusion

By using rxrs instead of media queries in your css, you and your team can see faster and more consistent development with more flexible and testable layouts.

Try rxrs today!


ng-conf: The Musical is coming

ng-conf: The Musical is a two-day conference from the ng-conf folks coming on April 22nd & 23rd, 2021. Check it out at ng-conf.org

Thanks to Erik Slack.
💖 💪 🙅 🚩
ngconf
ng-conf

Posted on March 1, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related