Angular Flex-Layout using MediaObserver

In the last post, we covered setting up and using the Angular Flex-Layout library. In that post, we looked at the responsive aspect of the library using directives and suffixing them with breakpoints where required.

To recap, add the fxLayout directive to a DOM element in the HTML. Then, add the abbreviated breakpoint depending on where the view size should change. For example, fxLayout.md or fxFlexAlign.lt-lg etc…

This post covers how to use the MediaObserver service.

The mediaObserver is installed from the Angular Flex-Layout library as shown below. In the projects terminal window type:

npm install @angular/flexlayout
view raw addFlex-Layout.ts hosted with ❤ by GitHub

And then import it to our app.module.ts file.

import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { FormsModule } from "@angular/forms";
import { AppComponent } from "./app.component";
import { FlexLayoutModule } from "@angular/flex-layout";
@NgModule({
imports: [BrowserModule, FormsModule, FlexLayoutModule],
declarations: [AppComponent],
bootstrap: [AppComponent]
})
export class AppModule {}
view raw app.module.ts hosted with ❤ by GitHub

The MediaObserver service is an Observable that exposes features to subscribe to mediaQuery changes. It also serves as an isActive() validator method to check if a mediaQuery is currently active.

The MediaObserver service has two API’s.
asObservable(): Observable<MediaChange>
isActive(query: string): boolean

We use Angular Dependency Injection to inject a reference to the MediaObserver as a constructor parameter to use this service.

import { Component, OnDestroy, OnInit } from '@angular/core';
import { MediaChange, MediaObserver } from '@angular/flex-layout';
import { Subscription } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnInit, OnDestroy {
title = 'Angular Flex-Layout';
/**
*
* @param mediaObserver
*/
constructor(public mediaObserver: MediaObserver) {}
private mediaSubscription!: Subscription;
private activeMediaQuery: string = '';
ngOnInit(): void {
const getAlias = (MediaChange: MediaChange[]) => {
return MediaChange[0].mqAlias;
};
this.mediaSubscription = this.mediaObserver
.asObservable()
.pipe(
distinctUntilChanged(
(x: MediaChange[], y: MediaChange[]) => getAlias(x) === getAlias(y)
)
)
.subscribe((change) => {
change.forEach((item) => {
this.activeMediaQuery = item
? `'${item.mqAlias}' = (${item.mediaQuery})`
: '';
if (item.mqAlias === 'md') {
this.loadMobileContent();
}
console.log('activeMediaQuery', this.activeMediaQuery);
});
});
}
ngOnDestroy(): void {
this.mediaSubscription.unsubscribe();
}
loadMobileContent() {
console.log('load mobile content');
// Do something special since the viewport is currently
// using mobile display sizes.
}
}
view raw mediaobservable.ts hosted with ❤ by GitHub

The current example on the Angular Flex-Layout’s wiki page is a little out of date. The media$ observable is now deprecated and replaced with mediaObserver. There appears to be a small bug when reporting the change detection as it duplicates the results, as shown below.

mediaObserver — showing duplicates

To get around this I’ve added a call to the distinctUntilChanged() RXJS operator (lines 29–31). The operator now only emits a single change.

Resize the browser to log the current breakpoint.

console.log of active breakpoint

Here the screen size is small (sm), but also shows us that the screen is less than medium (lt-md), less than large (lt-lg), less than extra-large (lt-xl), and greater than extra-small (gt-xs). In most cases, we really only need to know the actual size, but it’s good to know the others just in case.

So how do we use the MediaObserver?

When designing our applications, we need to know when the screen view size changes so that we can change our UI to make optimal use of the space. This might mean hiding an element or switching from rows to columns, for example:

<div fxLayout="row" fxLayoutGap="10px">
<div fxFlex="1 0 10" *ngIf="mediaObserver.isActive('md')"
id="boxone" style="background: red;">Box One</div>
<div fxFlex="1 0 10" id="boxtwo" style="background: blue;">Box Two</div>
</div>
view raw mediaobservable.html hosted with ❤ by GitHub

When the browser hits the ‘md’ breakpoint, the DIV (starting on line 2) is removed from the DOM.

To use the mediaObserver in your HTML (as shown above) it needs to be accessed by a get() for it to be visible.

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnInit, OnDestroy {
title = 'Angular Flex-Layout';
get media() {
return this.mediaObserver;
}
}
view raw mediaGetter.ts hosted with ❤ by GitHub
Getter for accessing the mediaObserver

Let’s try out another example, you will need to install Angular Material for this to work:

import { Component, OnInit } from '@angular/core';
import { MediaChange, MediaObserver } from '@angular/flex-layout';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit {
constructor(private mediaObserver: MediaObserver) {}
cols: Observable<any> | undefined;
ngOnInit(): void {
const grid = new Map([
['xs', 1],
['sm', 2],
['md', 3],
['lg', 4],
['xl', 5],
]);
this.cols = this.mediaObserver.asObservable().pipe(
map((change: MediaChange[]) => {
console.log(change[0]);
console.log(grid.get(change[0].mqAlias));
return grid.get(change[0].mqAlias);
})
);
}
}
view raw mediaObserver.ts hosted with ❤ by GitHub

Let’s walk through the above code:

We inject the mediObserver into the constructor as before, then create a new Map of the breakpoints. We set up an observable to the mediaObserver using the asObservable(). When the browser is resized, we get the currently set breakpoint from mqAlias, get the matching value from the grid array, and assign all of this to the cols variable.

<mat-grid-list rowHeight="1:1" [cols]="cols | async" gutterSize="6px">
<mat-grid-tile> {{1}} </mat-grid-tile>
<mat-grid-tile> {{2}} </mat-grid-tile>
<mat-grid-tile> {{3}} </mat-grid-tile>
<mat-grid-tile> {{4}} </mat-grid-tile>
<mat-grid-tile> {{5}} </mat-grid-tile>
</mat-grid-list>
view raw mediaObserver.html hosted with ❤ by GitHub

In the HTML we set up an Angular Material grid list component and use the async pipe operator to subscribe to the cols observable. The grid shrinks as the browser width decreases. Tile rows wrap to the row below until only one tile can fit on a page. When the browser width increases, the tiles move up to form one long row.

Output when the browser is resized.

The number we get back from the grid Map denotes the number of columns shown in our grid list, here the browser size is ‘md’ or ‘3’ columns.

Using the mediaObserver we now have an observable that we can subscribe to in our application and track when our viewport changes. The mediaObserver can be used in both the HTML or the Typescript code behind and we walked through a couple of examples in this post.

Thanks for reading.

How To Best Use The Angular Material Form Field?

Photo by Markus Winkler on Unsplash

Blog Post #010

Duncan Faulkner November 2020

What is a mat-form-field component?

The mat-form-field is part of the Angular Material library and is found in the @angular/material/form-field namespace.

{MatFormFieldModule} from '@angular/material/form-field';

So what is a mat-form-field component? By itself this component doesn’t do much, this component affects other components by applying common styles to components that are wrapped within a mat-form-field. Typically components like input, textarea, mat-select, and mat-chip-list are wrapped in a mat-form-field this is considered a best practice, for example:

<mat-form-field>
    <input matInput>
</mat-form-field>

Below is the results rendered in the browser.

figure 1: legacy input

The input now has an underline underneath it. Selecting the input moves the text from the placeholder to a floating label.

figure 2: input selected placeholder now becomes floating label

This is the default appearance of the <mat-form-field> and is called Legacy, the others are Standard, Fill and Outline.

<mat-form-field appearance="legacy">
    <input placeholder="application label" matInput>
</mat-form-field>
<mat-form-field appearance="standard">
    <mat-label>application label</mat-label>
    <input matInput>
</mat-form-field>
<mat-form-field appearance="fill">
    <mat-label>application label</mat-label>
    <input matInput>
</mat-form-field>
<mat-form-field appearance="outline">
    <mat-label>application label</mat-label>
    <input matInput>
</mat-form-field>
figure 3: image of all four form-field appearances.

Standard: is an updated version of Legacy to bring it inline with Fill and Outline, the changes are minor and mainly around the spacing.

Fill: adds a background to the input and the placeholder is middle aligned (more space between the placeholder and the bottom line), the floating label remains with in the background.

Outline: adds an outline all round the outer edge of the input and the placeholder is middle aligned (more space between the placeholder and the bottom line), and the floating label is now on top of the outline.

Placeholders: In the Legacy appearance, the placeholder is promoted to a floating label. In Standard, Fill and Outline however, it is just a regular label. If you want a floating label in Standard, Fill and Outline you need to include a <mat-label>.

matInput: this is an Angular Material directive that allows native HTML input components (input, textarea, select for example) to interact with Angular Material.

Hint messages: The mat-form-field has two ways to assign a hint message. A hint message is a message that appears underneath the underline. If the hintLabel attribute of the mat-form-field is used then the message is left aligned (for the hintLabel attribute left aligned is the only option).

If the mat-hint tag is used then these can be left or right aligned, by setting the align attribute to either start or end.

figure 4: hintLabel – left aligned
<mat-form-field hintLabel="hint message" appearance="standard">
    <mat-label>Standard Input</mat-label>
    <input matInput/>
</mat-form-field>
figure 5: mat-hint – right aligned
<mat-form-field appearance="standard">
    <mat-label>Standard Input</mat-label>
    <input matInput/>
    <mat-hint align="end">hint message</mat-hint>
</mat-form-field>

Hint message appear underneath the underline. The mat-form-field can have hint messages set either from the mat-form-field or the mat-hint. To set the hint message using the mat-form-field set the hintLabel attribute to a message. With this option the message is left aligned.

<mat-form-field hintLabel="hint message" appearance="standard">
    <mat-label>Input</mat-label>
    <input matInput>
</mat-form-field>

Using the mat-hint element set a message and alignment, for example:

<mat-form-field appearance="standard">
    <mat-label>Input</mat-label>
    <input matInput>
    <mat-hint align="start">Message</mat-hint>
</mat-form-field>
<mat-form-field appearance="standard">
    <mat-label>Input</mat-label>
    <input matInput>
    <mat-hint align="end">Message</mat-hint>
</mat-form-field>

Adding error messages to a mat-form-field appear underneath the underline. To add an error message add the mat-error element and check the validity of the form control.

<mat-form-field appearance="standard">
    <mat-label>Input</mat-label>
    <input matInput [formControl]="control" required>
    <mat-error *ngIf="control.invalid">This is a required field.</mat-error>
  </mat-form-field>

The formControl is part of Angular’s forms module and is used to track the value and validity state of a control. Then in the mat-error we add an *ngIf to check to see if the formControl is invalid, if it we display the error message.

Hope you enjoyed this little tutorial, I will be writing up more articles on Angular Material, feel free to reach out to me on Twitter.

How To Best Update Angular To Include Angular Material?

Blog Post #009

Duncan Faulkner – August 2020

Adding Angular Material to a new or existing Angular project is a simple process. In the terminal, change the directory to your project and type:

ng add @angular/material

The Angular CLI will now install Angular Material, Angular CDK and Angular Animations and add these as a dependency to the project.

In previous versions of Angular (before v9) hammerjs was required for touch and gesture support.

npm install hammerjs

In the main.ts file in the imports at the top, include the following:

import 'hammerjs' // add this import
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
  enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
  .catch(err => console.log(err));

Starting with version 9 of Angular hammerjs is now optional, you no longer need to install this as a dependency, it is now part of @angular/platform-browser. To use it, add the HammerModule to the platform-browser import in the app.module.ts file and include it in the NgModule imports array.

import { HammerModule, BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserAnimationsModule
    BrowserModule,
    HammerModule,
  ],
    bootstrap: [AppComponent]
})
export class AppModule {
}

You can also create a custom class to override specific features of HammerModule.

import { Injectable } from '@angular/core';
import { BrowserModule,
         HammerModule,
         HammerGestureConfig,
         HAMMER_GESTURE_CONFIG}
from '@angular/platform-browser';
@Injectable()
export class HammerConfig extends HammerGestureConfig {
    overrides = <any> {
       'pinch': {enable: true}
  }
}
@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserAnimationsModule
    BrowserModule,
    HammerModule,
  ],
    providers:
    [{
      provide: HAMMER_GESTURE_CONFIG,
      useClass: HammerConfiguration
   }],
   bootstrap: [AppComponent]
})
export class AppModule {
}

The other values that can be overridden are:
pan, pinch, press, rotate, swipe and tap.
Note: that pinch and rotate are disabled by default.

Thanks for reading enjoy…


How To Best Use Drag ‘n’ Drop Angular Material Table?

Image: https://material.angular.io/

Blog Post #007

Duncan Faulkner – February 2020

The Angular Material mat-table component probably has the most examples on the Angular Materials website. But I think we can squeeze one more in.

So here is a Drag ‘n’ Drop example.

I needed the ability to drag ‘n’ drop rows in a mat-table for a project I was working on and I thought this would make a good example of how easy it was to implement.

This example requires all the usual setup of an Angular application, rather than fill this post with all the steps to setup an Angular project. I have written a post here that goes through the steps. I will also create a another separate post on adding Angular Material, but for now here is how to add Angular Material to your Angular Project.

Project set up? Let’s add Angular Materials, from the terminal type:

ng add @angular/material

This will add the Angular Material dependencies to the project, next create a directory called shared and then add a new file called material.module.ts in this directory.

Then add the material imports, for this we just need MatTableModule.

// Other Material imports here
import { MatTableModule } from '@angular/material/table';
const MATERIALMODULES = [MatTableModule];
@NgModule({
     imports: [...MATERIALMODULES],
     declarations: [],
     exports: [...MATERIALMODULES]
});
export class MaterialModule {}

Then import material.module.ts file into the app.module.ts file.

// default modules add when app.module.ts was created
import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";
import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { HttpClientModule } from "@angular/common/http";
// Add these modules to import
import { MaterialModule } from "./shared/materials.module";
import { DragDropModule } from "@angular/cdk/drag-drop";
const MATERIALMODULE = [MaterialModule];
const IMPORTMODULES = [
  BrowserModule,
  AppRoutingModule,
  MaterialModule,
  BrowserAnimationsModule,
  HttpClientModule,
  DragDropModule
];
const APPCOMPONENT = [AppComponent];
@NgModule({
  declarations: [...APPCOMPONENT],
  imports: [...IMPORTMODULES],
  exports: [...MATERIALMODULE],
  providers: [],
  bootstrap: [...APPCOMPONENT]
})
export class AppModule {}

The MaterialModule is the Angular Material file we just created a few moments ago and the DragDropModule is required to implement the drag and drop from the Angular Material CDK (more on this later).

In the app.component.html file remove any html and replace it with the following.

<table mat-table [dataSource]="dataSource" cdkDropList [cdkDropListData]="dataSource"
    (cdkDropListDropped)="drop($event)" class="mat-elevation-z8">
    <ng-container matColumnDef="drag">
        <th mat-header-cell *matHeaderCellDef> Drag </th>
        <td mat-cell *matCellDef="let element">
            <mat-icon cdkDragHandle svgIcon="dragVertical"></mat-icon>
        </td>
    </ng-container>
    <!-- Position Column -->
    <ng-container matColumnDef="position">
        <th mat-header-cell *matHeaderCellDef> No. </th>
        <td mat-cell *matCellDef="let element"> {{element.position}} </td>
    </ng-container>
    <!-- Name Column -->
    <ng-container matColumnDef="name">
        <th mat-header-cell *matHeaderCellDef> Name </th>
        <td mat-cell *matCellDef="let element"> {{element.name}} </td>
    </ng-container>
    <!-- Weight Column -->
    <ng-container matColumnDef="weight">
        <th mat-header-cell *matHeaderCellDef> Weight </th>
        <td mat-cell *matCellDef="let element"> {{element.weight}} </td>
    </ng-container>
    <!-- Symbol Column -->
    <ng-container matColumnDef="symbol">
        <th mat-header-cell *matHeaderCellDef> Symbol </th>
        <td mat-cell *matCellDef="let element"> {{element.symbol}} </td>
    </ng-container>
    <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
    <tr mat-row *matRowDef="let row; columns: displayedColumns;" cdkDragLockAxis="y" cdkDrag [cdkDragData]="row">
    </tr>
</table>

A mat-table component takes a datasource, but we also need to add cdkDropList, a cdkDropListData and cdkDropListDropped to the table. The cdkDropList is a container that wraps a set of draggable items. The cdkDropListData is the datasource to attach to this container. And the cdkDropListDropped emits an event when a user drops an item into the container.

The last <tr> section in the mat-table, needs the collection of columns this is the array containing the column names, and row refers to current row.

The cdkDragLockAxis restricts the dragged element to either vertical or horizontal, this stops the user dragging the item all over the screen if it doesn’t make sense to, for example if you are only dragging a list of items to sort them then locking the axis to the “y axis” would keep this within the boundary of the list. The cdkDrag refers to the item being dragged and cdkDragData is the data attached to this drag instance.

That’s the HTML section, now for the code section.

import { Component } from "@angular/core";
import { IconService } from "./services/icon.service";
import { CdkDragDrop, moveItemInArray } from "@angular/cdk/drag-drop";
export interface PeriodicElement {
  name: string;
  position: number;
  weight: number;
  symbol: string;
}
const ELEMENT_DATA: PeriodicElement[] = [
  { position: 1, name: "Hydrogen", weight: 1.0079, symbol: "H" },
  { position: 2, name: "Helium", weight: 4.0026, symbol: "He" },
  { position: 3, name: "Lithium", weight: 6.941, symbol: "Li" },
  { position: 4, name: "Beryllium", weight: 9.0122, symbol: "Be" },
  { position: 5, name: "Boron", weight: 10.811, symbol: "B" },
  { position: 6, name: "Carbon", weight: 12.0107, symbol: "C" },
  { position: 7, name: "Nitrogen", weight: 14.0067, symbol: "N" },
  { position: 8, name: "Oxygen", weight: 15.9994, symbol: "O" },
  { position: 9, name: "Fluorine", weight: 18.9984, symbol: "F" },
  { position: 10, name: "Neon", weight: 20.1797, symbol: "Ne" }
];
/**
 * @title Basic use of `<table mat-table>`
 */
@Component({
  selector: "app-root",
  styleUrls: ["app.component.scss"],
  templateUrl: "app.component.html"
})
export class AppComponent {
  constructor(private iconService: IconService) {
    this.iconService.registerIcons();
  }
  displayedColumns: string[] = ["drag", "position", "name", "weight", "symbol"];
  dataSource = ELEMENT_DATA;
  drop(event: CdkDragDrop<PeriodicElement[]>) {
    moveItemInArray(this.dataSource, event.previousIndex, event.currentIndex);
    console.log(event.container.data);
  }
}

The drop event takes a generic CdkDragDrop<> in this case it’s a array of type PeriodicElement, this refers to the row that is being dropped. The moveItemInArray takes a datasource, the previousIndex and the currentIndex. The dataSource is the array (in this example the ELEMENT_DATA), the previousIndex is where the item came (position 10) from and the currentIndex (position 1) is where the item is being dropped. This method does all the hard work of the sorting etc… we just need to write the code to persist this and update the view (I’ve not done that in this example). If you check the console.log at this point then we can see that the data has been reordered.

And that’s all there is to add Drag n Drop to a mat-table.