Friday, July 07, 2023

Migrating away from Angular flex-layout

In a blog post from October 22, 2022 the Angular team announced

Layout has made significant advancements since Angular’s early days. Based on advancements to native layout solutions and removal of support for IE11, the Angular team will stop publishing new releases of the experimental @angular/flex-layout library starting in v15.

They then proceeded to layout migration alternatives.

  • CSS Flexbox
  • CSS Grid
  • TailwindCSS

This has resulted in considerable consternation for developers.  I do not wish to restate what others have already said about the problem and will instead simply point you to a good article on the topic entitled, Why the Deprecation of Flex-Layouts Is Concerning for Angular Developers. What I intend for this article is provide a simple and cleaner migration alternative off of the @angular/flex-layout package. The migration is actually a step back in time to utilize a set of CSS Flexbox classes defined for AngularJS and I need to give a shout-out to my team member, Pam Tingiris, for remembering and formulating this solution.

I am a solutions architect over a team of developers. During the past few years we have developed a number of Angular applications that are farily large and involved. When we started we were obviously using an older version of Angular without the @angular/flex-layout package. We then followed the Angular team's recommendation to utilize the @angular/flex-layout package so we migrated to it. While the migration was time consuming and tedious just due to the volume of changes to make, it was actually fairly straight-forward and was simply the inverse of what I'm recommended for you today. Fortunately, in our code repository we were able to go back and find the old AngularJS flex layout classes that we had previously migrated away from and deleted. I'm included it at the end of this post.

The migration is mostly a straight conversion from the @angular/flex-layout injected DOM stylings to CSS classes. For example, let's look at the initial example provided on the @angular/flex-layout GitHub page with an addition of a fxFlex div child:

<div fxLayout="row" fxLayoutAlign="space-between">
  <div fxFlex></div>
</div>

With the CSS classes, that simply becomes:

<div class="layout-row layout-align-space-between">
  <div class="flex"></div>
</div>

As you can see, it's a pretty straight-forward process. When you review the CSS at the bottom of the page, you will see there are classes for most every layout variation you might use. For example, fxLayoutAlign="space-between center" becomes class="layout-align-space-between-center" and fxFlex="50" becomes class="flex-50".

For the most part, we standardized our layout gaps to 1rem (example fxLayoutGap="1rem") so we added a single layout-gap class at the end of the original CSS file (.layout-gap { gap: 1rem }), but for the few occasions when we needed different spacing in a component we'd just add a custom class in the component's CSS such as .layout-gap-3rem { gap: 3rem }.

Now, you might be thinking to yourself, that's all well and good for basic flex layout directives, but what about this part of flex-layout?

The real power of Flex Layout, however, is its responsive engine. The Responsive API enables developers to easily specify different layouts, sizing, visibilities for different viewport sizes and display devices.

Here's where it's a little harder, but still not terrible. We'll utilize the Material CDK layout package to create a BreakpointObserver service.

You inject this service into any component that needs media queries for responsive displays.

  constructor(
    public breakpoints: BreakpointsService,
  ) { }

Now, if you have a page that is laid out in rows, but you want it to switch to columns for a phone display, you might have something like:

<div fxLayout="row" fxLayout.xs="column" fxLayoutAlign="space-between">
</div>

Utilizing the breakpoints, this will become:

<div class="layout layout-align-space-between"> [ngClass]="{'layout-column' : breakpoints.screen('xs')}">
</div>

Admittedly not as nice as the concise fxLayout.xs, but it does the job. The built in breakpoint names of the BreakpointObserver like XSmall don't correspond to the flex-layout names like xs, so we named them to match. Our BreakpointsService looks like:

/********************************************************************************
 * BreakpointsService (a BreakpointObserver)
 *
 * Implementation of the Material CDK BreakpointObserver
 * see https://material.angular.io/cdk/layout/overview#breakpointobserver
 * This defines a set of viewport breakpoints (media queries) that allow us
 * to react to changes in viewport sizes for a responsive UI.
 ********************************************************************************/

import { Injectable, OnDestroy } from '@angular/core';
import { BreakpointObserver, Breakpoints} from '@angular/cdk/layout';
import { takeUntil } from 'rxjs/operators';
import { BehaviorSubject, Subject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class BreakpointsService implements OnDestroy {
  destroyed = new Subject<void>();

  mediaQueryMap = new Map([
    ['xs', Breakpoints.XSmall ],
    ['sm', Breakpoints.Small],
    ['md', Breakpoints.Medium],
    ['lg', Breakpoints.Large],
    ['xl', Breakpoints.XLarge],
    ['gt-xs', '(min-width: 600px)'],
    ['gt-sm', '(min-width: 960px)'],
    ['gt-md', '(min-width: 1280px)'],
  ]);

  protected matchesSubject = new BehaviorSubject([]);
  matches = this.matchesSubject.asObservable();

  constructor(public breakpointObserver: BreakpointObserver) {
    breakpointObserver
      .observe([
        Breakpoints.XSmall,
        Breakpoints.Small,
        Breakpoints.Medium,
        Breakpoints.Large,
        Breakpoints.XLarge,
        '(min-width: 600px)',
        '(min-width: 960px)',
        '(min-width: 1280px)',
      ])
      .pipe(takeUntil(this.destroyed))
      .subscribe(result => {
        const matchedBreakpoints = Object.entries( result.breakpoints ).filter( e => e[1] );
        this.matchesSubject.next(matchedBreakpoints);
      });
  }

  ngOnDestroy() {
    this.destroyed.next();
    this.destroyed.complete();
  }

  /***
   * Screen sizes for responsive styling
   * @param size 'xs', 'gt-xs'
   * */
  screen(size) {
    let screenSize = false;
    const matches = this.matches.subscribe(m => {

      const mq = this.mediaQueryMap.get(size);
      screenSize = m.filter( match => match[0] === mq ).length === 1;

    } );
    return screenSize;
  }

}

And that's it! Now it's just a process of finding all the "fx" directives and any screen size rules, and swapping them out for classes. Once you remove the FlexLayoutModule from your app.module.ts, you'll quickly discover anything you might have missed.

import { FlexLayoutModule } from '@angular/flex-layout';

@NgModule({
  imports: [
    FlexLayoutModule,

In conclusion, we found this migration path to be largely a 1-to-1 replacement. We've seen so many other packages come into favor and then fall out of favor over time (like bootstrap) that we didn't want to go down the route of switching to TailwindCSS just to have it ripped out from under us again in a few years. Hopefully you find this to be a helpful alternative.

Flex Layout CSS classes

/* flex layout classes - instructions https://material.angularjs.org/latest/layout/introduction */

.layout-align, .layout-align-start-stretch {
  justify-content: flex-start;
  align-content: stretch;
  align-items: stretch
}

.layout-align-start, .layout-align-start-center, .layout-align-start-end, .layout-align-start-start, .layout-align-start-stretch {
  justify-content: flex-start
}

.layout-align-center, .layout-align-center-center, .layout-align-center-end, .layout-align-center-start, .layout-align-center-stretch {
  justify-content: center
}

.layout-align-end, .layout-align-end-center, .layout-align-end-end, .layout-align-end-start, .layout-align-end-stretch {
  justify-content: flex-end
}

.layout-align-space-around, .layout-align-space-around-center, .layout-align-space-around-end, .layout-align-space-around-start, .layout-align-space-around-stretch {
  justify-content: space-around
}

.layout-align-space-between, .layout-align-space-between-center, .layout-align-space-between-end, .layout-align-space-between-start, .layout-align-space-between-stretch {
  justify-content: space-between
}

.layout-align-space-evenly, .layout-align-space-evenly-center, .layout-align-space-evenly-end, .layout-align-space-evenly-start, .layout-align-space-evenly-stretch {
  justify-content: space-evenly
}

.layout-align-center-start, .layout-align-end-start, .layout-align-space-around-start, .layout-align-space-between-start, .layout-align-start-start, .layout-align-space-evenly-start {
  align-items: flex-start;
  align-content: flex-start
}

.layout-align-center-center, .layout-align-end-center, .layout-align-space-around-center, .layout-align-space-between-center, .layout-align-start-center, .layout-align-space-evenly-center {
  align-items: center;
  align-content: center;
  max-width: 100%
}

.layout-align-center-center > *, .layout-align-end-center > *, .layout-align-space-around-center > *, .layout-align-space-between-center > *, .layout-align-start-center > *, .layout-align-space-evenly-center > * {
  max-width: 100%;
  box-sizing: border-box
}

.layout-align-center-end, .layout-align-end-end, .layout-align-space-around-end, .layout-align-space-between-end, .layout-align-start-end, .layout-align-space-evenly-end {
  align-items: flex-end;
  align-content: flex-end
}

.layout-align-center-stretch, .layout-align-end-stretch, .layout-align-space-around-stretch, .layout-align-space-between-stretch, .layout-align-start-stretch, .layout-align-space-evenly-stretch {
  align-items: stretch;
  align-content: stretch
}

.flex {
  flex: 1
}

.flex, .flex-grow {
  box-sizing: border-box
}

.flex-grow {
  flex: 1 1 100%
}

.flex-initial {
  flex: 0 1 auto;
  box-sizing: border-box
}

.flex-auto {
  flex: 1 1 auto;
  box-sizing: border-box
}

.flex-none {
  flex: 0 0 auto;
  box-sizing: border-box
}

.flex-noshrink {
  flex: 1 0 auto;
  box-sizing: border-box
}

.flex-nogrow {
  flex: 0 1 auto;
  box-sizing: border-box
}

.flex-0, .layout-row > .flex-0 {
  flex: 1 1 100%;
  max-width: 0;
  max-height: 100%;
  box-sizing: border-box
}

.layout-row > .flex-0 {
  min-width: 0
}

.layout-column > .flex-0 {
  flex: 1 1 100%;
  max-width: 100%;
  max-height: 0%;
  box-sizing: border-box
}

.flex-5, .layout-row > .flex-5 {
  flex: 1 1 100%;
  max-width: 5%;
  max-height: 100%;
  box-sizing: border-box
}

.layout-column > .flex-5 {
  flex: 1 1 100%;
  max-width: 100%;
  max-height: 5%;
  box-sizing: border-box
}

.flex-10, .layout-row > .flex-10 {
  flex: 1 1 100%;
  max-width: 10%;
  max-height: 100%;
  box-sizing: border-box
}

.layout-column > .flex-10 {
  flex: 1 1 100%;
  max-width: 100%;
  max-height: 10%;
  box-sizing: border-box
}

.flex-15, .layout-row > .flex-15 {
  flex: 1 1 100%;
  max-width: 15%;
  max-height: 100%;
  box-sizing: border-box
}

.layout-column > .flex-15 {
  flex: 1 1 100%;
  max-width: 100%;
  max-height: 15%;
  box-sizing: border-box
}

.flex-20, .layout-row > .flex-20 {
  flex: 1 1 100%;
  max-width: 20%;
  max-height: 100%;
  box-sizing: border-box
}

.layout-column > .flex-20 {
  flex: 1 1 100%;
  max-width: 100%;
  max-height: 20%;
  box-sizing: border-box
}

.flex-25, .layout-row > .flex-25 {
  flex: 1 1 100%;
  max-width: 25%;
  max-height: 100%;
  box-sizing: border-box
}

.layout-column > .flex-25 {
  flex: 1 1 100%;
  max-width: 100%;
  max-height: 25%;
  box-sizing: border-box
}

.flex-30, .layout-row > .flex-30 {
  flex: 1 1 100%;
  max-width: 30%;
  max-height: 100%;
  box-sizing: border-box
}

.layout-column > .flex-30 {
  flex: 1 1 100%;
  max-width: 100%;
  max-height: 30%;
  box-sizing: border-box
}

.flex-35, .layout-row > .flex-35 {
  flex: 1 1 100%;
  max-width: 35%;
  max-height: 100%;
  box-sizing: border-box
}

.layout-column > .flex-35 {
  flex: 1 1 100%;
  max-width: 100%;
  max-height: 35%;
  box-sizing: border-box
}

.flex-40, .layout-row > .flex-40 {
  flex: 1 1 100%;
  max-width: 40%;
  max-height: 100%;
  box-sizing: border-box
}

.layout-column > .flex-40 {
  flex: 1 1 100%;
  max-width: 100%;
  max-height: 40%;
  box-sizing: border-box
}

.flex-45, .layout-row > .flex-45 {
  flex: 1 1 100%;
  max-width: 45%;
  max-height: 100%;
  box-sizing: border-box
}

.layout-column > .flex-45 {
  flex: 1 1 100%;
  max-width: 100%;
  max-height: 45%;
  box-sizing: border-box
}

.flex-50, .layout-row > .flex-50 {
  flex: 1 1 100%;
  max-width: 50%;
  max-height: 100%;
  box-sizing: border-box
}

.layout-column > .flex-50 {
  flex: 1 1 100%;
  max-width: 100%;
  max-height: 50%;
  box-sizing: border-box
}

.flex-55, .layout-row > .flex-55 {
  flex: 1 1 100%;
  max-width: 55%;
  max-height: 100%;
  box-sizing: border-box
}

.layout-column > .flex-55 {
  flex: 1 1 100%;
  max-width: 100%;
  max-height: 55%;
  box-sizing: border-box
}

.flex-60, .layout-row > .flex-60 {
  flex: 1 1 100%;
  max-width: 60%;
  max-height: 100%;
  box-sizing: border-box
}

.layout-column > .flex-60 {
  flex: 1 1 100%;
  max-width: 100%;
  max-height: 60%;
  box-sizing: border-box
}

.flex-65, .layout-row > .flex-65 {
  flex: 1 1 100%;
  max-width: 65%;
  max-height: 100%;
  box-sizing: border-box
}

.layout-column > .flex-65 {
  flex: 1 1 100%;
  max-width: 100%;
  max-height: 65%;
  box-sizing: border-box
}

.flex-70, .layout-row > .flex-70 {
  flex: 1 1 100%;
  max-width: 70%;
  max-height: 100%;
  box-sizing: border-box
}

.layout-column > .flex-70 {
  flex: 1 1 100%;
  max-width: 100%;
  max-height: 70%;
  box-sizing: border-box
}

.flex-75, .layout-row > .flex-75 {
  flex: 1 1 100%;
  max-width: 75%;
  max-height: 100%;
  box-sizing: border-box
}

.layout-column > .flex-75 {
  flex: 1 1 100%;
  max-width: 100%;
  max-height: 75%;
  box-sizing: border-box
}

.flex-80, .layout-row > .flex-80 {
  flex: 1 1 100%;
  max-width: 80%;
  max-height: 100%;
  box-sizing: border-box
}

.layout-column > .flex-80 {
  flex: 1 1 100%;
  max-width: 100%;
  max-height: 80%;
  box-sizing: border-box
}

.flex-85, .layout-row > .flex-85 {
  flex: 1 1 100%;
  max-width: 85%;
  max-height: 100%;
  box-sizing: border-box
}

.layout-column > .flex-85 {
  flex: 1 1 100%;
  max-width: 100%;
  max-height: 85%;
  box-sizing: border-box
}

.flex-90, .layout-row > .flex-90 {
  flex: 1 1 100%;
  max-width: 90%;
  max-height: 100%;
  box-sizing: border-box
}

.layout-column > .flex-90 {
  flex: 1 1 100%;
  max-width: 100%;
  max-height: 90%;
  box-sizing: border-box
}

.flex-95, .layout-row > .flex-95 {
  flex: 1 1 100%;
  max-width: 95%;
  max-height: 100%;
  box-sizing: border-box
}

.layout-column > .flex-95 {
  max-height: 95%
}

.flex-100, .layout-column > .flex-95 {
  flex: 1 1 100%;
  max-width: 100%;
  box-sizing: border-box
}

.flex-100 {
  max-height: 100%
}

.layout-column > .flex-100, .layout-row > .flex-100 {
  flex: 1 1 100%;
  max-width: 100%;
  max-height: 100%;
  box-sizing: border-box
}

.flex-33 {
  max-width: 33.33%
}

.flex-33, .flex-66 {
  flex: 1 1 100%;
  max-height: 100%;
  box-sizing: border-box
}

.flex-66 {
  max-width: 66.66%
}

.layout-row > .flex-33 {
  flex: 1 1 33.33%
}

.layout-row > .flex-66 {
  flex: 1 1 66.66%
}

.layout-column > .flex-33 {
  flex: 1 1 33.33%
}

.layout-column > .flex-66 {
  flex: 1 1 66.66%
}

.layout-row > .flex-33 {
  max-width: 33.33%
}

.layout-row > .flex-33, .layout-row > .flex-66 {
  flex: 1 1 100%;
  max-height: 100%;
  box-sizing: border-box
}

.layout-row > .flex-66 {
  max-width: 66.66%
}

.layout-row > .flex {
  min-width: 0
}

.layout-column > .flex-33 {
  max-height: 33.33%
}

.layout-column > .flex-33, .layout-column > .flex-66 {
  flex: 1 1 100%;
  max-width: 100%;
  box-sizing: border-box
}

.layout-column > .flex-66 {
  max-height: 66.66%
}

.layout-column > .flex {
  min-height: 0
}

.layout, .layout-column, .layout-row {
  box-sizing: border-box;
  display: flex
}

.layout-column {
  flex-direction: column
}

.layout-row {
  flex-direction: row
}

.layout-padding-sm > *, .layout-padding > .flex-sm {
  padding: 4px
}

.layout-padding, .layout-padding-gt-sm, .layout-padding-gt-sm > *, .layout-padding-md, .layout-padding-md > *, .layout-padding > *, .layout-padding > .flex, .layout-padding > .flex-gt-sm, .layout-padding > .flex-md {
  padding: 8px
}

.layout-padding-gt-lg > *, .layout-padding-gt-md > *, .layout-padding-lg > *, .layout-padding > .flex-gt-lg, .layout-padding > .flex-gt-md, .layout-padding > .flex-lg {
  padding: 16px
}

.layout-margin-sm > *, .layout-margin > .flex-sm {
  margin: 4px
}

.layout-margin, .layout-margin-gt-sm, .layout-margin-gt-sm > *, .layout-margin-md, .layout-margin-md > *, .layout-margin > *, .layout-margin > .flex, .layout-margin > .flex-gt-sm, .layout-margin > .flex-md {
  margin: 8px
}

.layout-margin-gt-lg > *, .layout-margin-gt-md > *, .layout-margin-lg > *, .layout-margin > .flex-gt-lg, .layout-margin > .flex-gt-md, .layout-margin > .flex-lg {
  margin: 16px
}

.layout-wrap {
  flex-wrap: wrap
}

.layout-nowrap {
  flex-wrap: nowrap
}

.layout-fill {
  margin: 0;
  width: 100%;
  min-height: 100%;
  height: 100%
}

.layout-gap { gap: 1rem }