Drag ‘n’ Drop with Angular Materials Table Component

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).

Follow me on Twitter to get notified about my newest blog posts!ūüź•

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.

Using Mat-Icon Component – Part Two

Blog Post #002

Duncan Faulkner – January 2020

I hadn’t intended to write about this just yet, but I got a chance to investigate this sooner than I had thought. I was refactoring part of a project that was using the Mat-Icon component, with an icon.module.ts file as I’d discussed in the previous post.

I wanted to see if I could improve on the code and reduce the amount of repeated code by iterating over a file and registering the icons.

I started with a JSON file and populated it with some image names and added the HttpClientModule to use an http.get() method to read the JSON file. All good so far, the file is reading and looping with image names being returned and passed into the matIconRegistery.addSvgIcon() method.

I thought I might get some issues, but it all seemed to work fine, except the application was erroring, the issue was that the application was loading before the call to the JSON file had returned with the results, so even though I was getting images and registering icons, the app had already tried to use the icons and they weren’t there at that point, and the images were being returned after the app had started.

OK, so may be this is a timing issue and it doesn’t work in the icon.module.ts file, so I moved code to the app.component.ts file. Nope, the results were the same. This was not the result I was expecting and it was a little odd because the icon.module.ts file was loading images before.

I’m going to revisit this at a later date, as I would like to understand what the issue is and why it was behaving the way it did, my guess is it has something to do with the http.get() request and the timing around that.

But as I needed to get this refactoring completed I needed to look for another solution.

After a bit of research I found an answer and that was to create a service to handle the iterating and registering of icons. Instead of a JSON file to store a list of images the solution was an enum, with a key value structure.

export enum Icons {
    add = 'add',
    power = 'power',
    save = 'save'
}

To keep things simple I’ve made the key and the image name the same.

The service is quite simple:

// imports
import { Icons } from '../image.enum';

@Injectable({ providedIn: 'root'})
export class IconService {
  constructor(
     private matIconRegistry: MatIconRegistry,
     private sanitizer: Domsanitizer
){}

registerIcons(): void {
   this.load(Object.values(Icons), 'assets/images/icons');
}

private load(key: string[], url: string): void {
   key.forEach(icon => {
     this.matIconRegistry.addSvgIcon(
       icon, 
       this.sanitizer.bypassSecurityTrustResourceUrl(`${url}/${icon}.svg`)
   ); 
 });
 }
}

And on the app.component.ts file call the registerIcons() function.

// imports
import { IconService } from './icon.service';

export class AppComponent implements OnInit {
   constructor(private iconService: IconService) {
      this.iconService.registerIcons();
   }

}

And that’s it, not what I had in mind when I started this, but it does deliver what I wanted to do, and that was to reduce the amount of code to register icons. Though I guess the enum could get pretty lengthy if there are a lot of images.

I have to thank Stefanos Kouroupis -dev.to – for his article and code on this as I wouldn’t have thought about using a service for this and this has changed my thinking. I wanted to include here it as a follow up to my previous post for completeness, you can see his full article here https://dev.to/elasticrash/using-custom-made-svg-icons-through-the-angular-material-library-2pif.

I will add a github repo of the code in a day or two.


Previous posts…

Angular Materials Mat-Icon component

Using local SVG images.

Blog post #001

Duncan Faulkner – October 2019

When developing an Angular application with Angular Material, there comes a point when we need to add icons on our components, or buttons etc… Angular Material has the Mat-Icon component for doing this. This component works with web fonts like Font-Awesome for instance, simply by adding the name of the image required and an image is displayed.

For example: <mat-icon>home</mat-icon>

Note: Requires Angular application and Angular Material is installed/configured and a reference to a web font like Font-Awesome library is all set up

But what about if you have custom icons that are not part of a web font? And would like to makes changes to the icon, for example: change the colour on hover or on a specific condition at run time?

In a recent project I had a bespoke set of SVG icons, the Angular web application was to be installed on a server that didn’t have access to the internet, so the images had to be local. I wanted to use the Mat-Icon component out of the box (in an early version of the project I had a custom icon component). I still wanted to be able to change the colours of the icons at various stages throughout the application based on certain conditions as well as on hover. This post covers how I achieved that.

SVG Icons

There are a number of different ways to register an icon with the Mat-Icon component, this post discusses addSvgIcon, the others are addSvgIconInNamespace, addSvgIconLiteral or  addSvgIconLiteralInNamespace and are all methods of  MatIconRegistry and I might cover these in more detail in a future post.

Setting up an Angular project

Note In this post, I’m not going to step through the creation of an Angular application as there are so many already online. Plus I tend to create Nx workspaces for all my Angular projects as I prefer this project layout. I plan to do a blog post on this very soon, for the moment though see Getting started with Narwhal’s Nx Workspaces.

In the newly created Angular project, create a shared directory and add a new file named material.module.ts. I like to separate Angular Material imports into their own module, I also create a separate module for other 3rd party components, just makes it easier to import later, especially using the NX workspace layout and feature folders.

// Material Module example.
 // All other Angular Material component imports here
 // but the important ones are…
 import { MatIconModule, MatIconRegistry } from "@angular/material/icon";
 @NgModule({
   declarations: [],
   imports: [
     // Other material imports removed for brevity,
     MatIconModule
   ],
   exports: [
     // Other material exports removed for brevity,
     MatIconModule,
     MatIconRegistry
   ],
   entryComponents: [],
   providers: [MatIconRegistry]
 })
 export class MaterialModule {}

MatIconModule is the module for the component, and the MatIconRegistry is a service to register and display icons. Add a reference to the¬†material.module.ts¬†in the¬†app.module.ts. Don’t forget to export it as well, otherwise it won’t be available and Angular will not know anything about the Angular Material components.

// Include the material module in app.module.ts
import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";
import { AppComponent } from "./app.component";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { MaterialModule } from "./shared/material.module";

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, BrowserAnimationsModule, MaterialModule],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

Now we have the Angular Material components set up and configured we need to register the icons, before we can use them. For the moment we are just going to add these to the¬†app.component.ts¬†to get up and running, we’ll look at a better method later on.

// First Example
@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.scss"]
})
export class AppComponent {
  constructor(private matIconRegistry: MatIconRegistery) {
    this.matIconRegistry.addSvgIcon("home", "assets/img/icon/home.svg");
    this.matIconRegistry.addSvgIcon("add", "assets/img/icon/add.svg");
  }
  // or we could do this, and chain the addsvgIcon methods.
  // we'll use this method going forward in this post
  // {
  //  this.matIconRegistry.addSvgIcon('home','assets/img/icon/home.svg')
  //  .addSvgIcon('add','assets/img/icon/add.svg');
  // }
}

Add this to the¬†app.component.html¬†page, we’ll discuss this in more detail in a bit.

<!-- First HTML example -->
<div>
     <mat-icon svgIcon="home"></mat-icon>
     <mat-icon svgIcon="add"><mat-icon>
</div>

At this point we are not going to see much in the browser as we have an issue with the image URL. If you open the browsers console section, you will see the following error:

Error: unsafe value used in a resource URL context.

So what does this error mean? A brief explanation from the Angular Material website says:

To prevent an Cross Site Scripting (XSS), SVG URL’s and HTML strings passed to MatIconRegistry must be marked as trusted by the Angular’s DomSanitizer service. Icons are fetched via XmlHttpRequest and must have their URL’s on the same domain as the containing page or configured to allow cross-domain access.

So lets add the DomSanitizer and fix this issue.

// Second Example - with the DomSanitizer
@Component({
 selector: 'app-root',
 templateUrl: './app.component.html',
 styleUrls: ['./app.component.scss']
})
export class AppComponent {
 constructor (
 private domSanitizer: DomSanitizer,
 private matIconRegistry: MatIconRegistery
 ){
 this.matIconRegistry
 .addSvgIcon('home',this.domSanitizer.bypassSecurityTrustResourceUrl('assets/img/icon/home.svg')
 .addSvgIcon('add',this.domSanitizer.bypassSecurityTrustResourceUrl('assets/img/icon/add.svg')
 // add other icons here....;
 }
}

The call to bypassSecurityTrustResourceUrl takes a URL as a parameter and sanitises it so that an attacker cannot inject a ‘JavaScript:` URL for example, see official documentation on DomSanitizer. Now that we have this in place we should now see two icons in the browser.

If we have a lot of icons to add this means lots of typing and lots of repetitive code, so lets refactor this some more. Start by removing all of this code (including the constructor) from the app.component.ts. This shouldn’t really be in the app.component.ts file. Lets create another new module in the shared directory and call it¬†icon.module.ts¬†and then add the following:

// Third Example - icon module
import { NgModule } from "@angular/core";
import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser";
import { MaterialModule } from "../shared/material.module";
import { MatIconRegistry } from "@angular/material/icon";
@NgModule({
  declarations: [],
  imports: [MaterialModule],
  exports: [],
  providers: []
})
export class IconModule {
  private path: string = "../../assets/images"; // change this
  constructor(
    private domSanitizer: DomSanitizer,
    public matIconRegistry: MatIconRegistry
  ) {
    this.matIconRegistry
      .addSvgIcon("home", this.setPath(`${this.path}/home.svg`))
      .addSvgIcon("add", this.setPath(`${this.path}/file-plus.svg`));
  }
  private setPath(url: string): SafeResourceUrl {
    return this.domSanitizer.bypassSecurityTrustResourceUrl(url);
  }
}

Overall, that’s not too bad. We are only writing out the domSanitizer code once in the private method but more importantly, all the code is out of the app.component.ts file and is now a self contained module. If there are a lot of icons to add then this file will get a bit long, but the typing has gotten shorter (well, a little shorter at least). You could change the constructor to iterate through a .json file of image names, the path wouldn’t change and could be a const, which would only mean maintaining a .json file with any new images. I might look at doing that in a follow up post.

Note: Don’t forget to add this new icon.module.ts to the app.module.ts otherwise it won’t work.

Using the Mat-Icon component

So how do we use the mat-icon component? As seen earlier in this post we add the following code to our app.components.html page.

<!--First HTML example-->
<div>
    <mat-icon svgIcon="home"></mat-icon>
    <mat-icon svgIcon="add"></mat-icon>
</div>

This is a very simple example is showing how to put a home and an add icon on a page/component. This is not too dissimilar to how we would use this component with web fonts, but we are now using the svgIcon input property. The value we give to this input is the first parameter used in our call to register the .addSvgIcon(‘home’, …) in this case home.

Now we have an icon in place, how do we change the colour of the icon when someone hovers over it for example?

Change the icon colour

A Home icon. Example icon copy this into a file with the SVG extension.

<svg version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z" fill="#00FFFF"></svg>

In the above XML, I’ve removed all the namespaces etc… The main part here is the fill=”#00FFFF”. This sets the colour for the image, in this case to AquaMarine.

If the fill=”#00…” property is not there and you want a different colour to the default black, then you can add it to the path as above. It’s optional.

I usually add the fill property and set it to white and then change it, in SCSS as and when required (because this example had a white background I used another colour).

!<-- Second HTML example -->
<div>
  !<-- other code omitted for brevity -->
  <a mat-button>
      <mat-icon svgIcon="home" class="btn-icon"></mat-icon>
  </a>
</div>

Add a class to the mat-icon html tag, as above. Next, add the SCSS for the class so that, when a user hovers over the button the colour of the icon changes.

.btn-icon {
  &:hover {
    path {
      fill: rgba(#00ffff, 1);
    }
  }
}

Note:¬†One thing I did forget to add to the app.component.ts above, was the following line:¬†encapsulation: ViewEncapsulation.None, without this then the hover affect doesn’t work.

@Component({
  selector: "mat-icon-demo-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.scss"],
  encapsulation: ViewEncapsulation.None
})
export class AppComponent {
  title = "mat-icon-demo";
}

Enjoy!

There is a github repo for this blog post at: https://github.com/Anglebrackets-io/mat-icon-demo


Previous posts…