Refactoring to Angular Pipes

Refactoring to Angular Pipes

Derived from photo by Bodey Marcoccia / flickr.com, CC BY-SA

Angular pipes are often overlooked in their simplicity. If you are familiar with filters in Angular 1.x, they are conceptually similar. At their core, they convert a set of inputs to display text in the UI. And while there are other ways to accomplish this, you may find that by using pipes you can reduce the amount of code in your components.

This is one of a series of posts detailing the process of building an application with Angular, Visual Studio, ASP.NET Core, and Azure. See the complete list of posts here.

The prior post demonstrates how to create a component using Angular Material. The component class contains a TODO comment to convert mapping logic into a pipe. This post shows how to create the pipe:

// TODO: move to own pipe or directive
private mapToDisplayDateText(date: Date): string {
    // TODO: Make today's date mockable, convert to UTC
    const todaysDate = new Date();
    const isToday = areSameDate(todaysDate, date);
    const yesterdaysDate = addDays(new Date(), -1);
    const isYesterday = areSameDate(yesterdaysDate, date);

    const dayText = isToday ? 'Today' : isYesterday ? 'Yesterday' : formatDate(date, 'dddd, MMMM, D, YYYY');
    const lastCompletedDate = `${dayText}, ${formatDate(date, 'h:mm A')}`;

    return lastCompletedDate;
}

Why Use Pipes

Admittedly, I'm a little torn on using pipes. In their pure form, the concept is straightforward: take a set of inputs and return the display output as a string. A pure JavaScript function can also accomplish the same result in a component. I sometimes view pipes as conceptual overhead that you don't necessarily need to learn when starting with Angular. The concept count is already so high for many developers. Why add pipes to the mix?

This post explores how encapsulating this mapping logic in its own UI-based wrapper, a pipe, reduces component complexity in two ways. First when mapping in the component, you are responsible for determining if a value has changed or not before mapping (or you decide not to check this at all). When using a pipe, Angular's change detection evaluates the value to determine if mapping is required.

Second, using a pipe can reduce model complexity. For instance, this example formats a date in a custom way. This date may be rendered in different ways depending on where it is shown. When mapping in the component, each component is responsible for mapping this date into the correct string format and the component is responsible for two pieces of state, the original value and the display value (or multiple display values). When this logic is encapsulated in a pipe, the view declaratively defines how the data should display and the component is only responsible for one piece of state, the date object.

Initially, I recommend sticking with pure pipes. They are more performant out of the box because they adhere to the default (and fast) Angular change detection behavior where only values and object references are compared. If you find the need to detect changes at a deeper level of an object, there are probably better ways to handle this outside the scope of a pipe – and this article.

This post isn't attempting to recreate the documentation, please read that here. Rather, this is more of a case study.

Creating the Pipe

In the initial component implementation, the pipe formats the date and replaces the day of the week text when the day is either today or yesterday. This is how the component class is written with the formatting logic in the component:

import { Component, OnInit } from '@angular/core';
import { formatDate, areSameDate, addDays } from '../utility';
import { Routine, RoutinesViewModel, RoutineViewModel } from './models';

@Component({
  selector: 'app-routines',
  templateUrl: './routines.component.html',
  styleUrls: ['./routines.component.css']
})
export class RoutinesComponent implements OnInit {
  private routines: Routine[] = [
    {
      name: 'Morning',
      lastCompletedDate: new Date()
    },
    {
      name: 'Social Media',
      lastCompletedDate: new Date(2017, 8, 23, 19, 55)
    },
    {
      name: 'Bedtime',
      lastCompletedDate: new Date(2017, 8, 5, 21, 16)
    }
  ];

  viewModel: RoutinesViewModel = {
    routines: []
  };

  ngOnInit() {
    this.render();
  }

  trackRoutine(routine: RoutineViewModel) {
    return routine.name;
  }

  // TODO: move to own pipe or directive
  private mapToDisplayDateText(date: Date): string {
    // TODO: Make today's date mockable, convert to UTC
    const todaysDate = new Date();
    const isToday = areSameDate(todaysDate, date);
    const yesterdaysDate = addDays(new Date(), -1);
    const isYesterday = areSameDate(yesterdaysDate, date);

    const dayText = isToday ? 'Today' : isYesterday ? 'Yesterday' : formatDate(date, 'dddd, MMMM, D, YYYY');
    const lastCompletedDate = `${dayText}, ${formatDate(date, 'h:mm A')}`;

    return lastCompletedDate;
  }

  private render() {
    const viewModel = {
      routines: this.routines.map(routine => {
        const routineViewModel: RoutineViewModel = {
          lastCompletedDate: this.mapToDisplayDateText(routine.lastCompletedDate),
          name: routine.name
        };

        return routineViewModel;
      })
    };

    this.viewModel = viewModel;
  }
}
<md-toolbar color="primary">
  <span>Routines</span>
</md-toolbar>

<md-nav-list>
  <md-list-item *ngFor="let routine of viewModel.routines; index as i; trackBy: trackRoutine">
    <a>
      <div md-line>
        <span class="mat-title">{{ routine.name }}</span>
      </div>
      <div md-line>
        <span class="mat-subheading-2">{{ routine.lastCompletedDate }}</span>
      </div>
    </a>
  </md-list-item>
</md-nav-list>

<a md-fab class="add-routine-button"><md-icon>add</md-icon></a>

This is the pipe and the component after refactoring:

import { Pipe, PipeTransform } from '@angular/core';
import { formatDate, areSameDate, addDays } from '../utility';

@Pipe({ name: 'todayAwareDate' })
export class TodayAwareDatePipe implements PipeTransform {
  transform(date: Date, todayText = 'Today', yesterdayText = 'Yesterday') {
    // TODO: Make today's date mockable, convert to UTC
    const todaysDate = new Date();
    const isToday = areSameDate(todaysDate, date);
    const yesterdaysDate = addDays(new Date(), -1);
    const isYesterday = areSameDate(yesterdaysDate, date);

    const dayText = isToday ? todayText : isYesterday ? yesterdayText : formatDate(date, 'dddd, MMMM, D, YYYY');
    const lastCompletedDate = `${dayText}, ${formatDate(date, 'h:mm A')}`;

    return lastCompletedDate;
  }
}
import { Component, OnInit } from '@angular/core';
import { Routine, RoutinesViewModel } from './models';

@Component({
  selector: 'app-routines',
  templateUrl: './routines.component.html',
  styleUrls: ['./routines.component.css']
})
export class RoutinesComponent implements OnInit {
  private routines: Routine[] = [
    {
      name: 'Morning',
      lastCompletedDate: new Date()
    },
    {
      name: 'Social Media',
      lastCompletedDate: new Date(2017, 8, 23, 19, 55)
    },
    {
      name: 'Bedtime',
      lastCompletedDate: new Date(2017, 8, 5, 21, 16)
    }
  ];

  viewModel: RoutinesViewModel = {
    routines: []
  };

  ngOnInit() {
    this.render();
  }

  trackRoutine(routine: Routine) {
    return routine.name;
  }

  private render() {
    this.viewModel = {
      routines: this.routines
    };
  }
}
<md-toolbar color="primary">
  <span>Routines</span>
</md-toolbar>

<md-nav-list>
  <md-list-item *ngFor="let routine of viewModel.routines; index as i; trackBy: trackRoutine">
    <a>
      <div md-line>
        <span class="mat-title">{{ routine.name }}</span>
      </div>
      <div md-line>
        <span class="mat-subheading-2">{{ routine.lastCompletedDate | todayAwareDate }}</span>
      </div>
    </a>
  </md-list-item>
</md-nav-list>

<a md-fab class="add-routine-button"><md-icon>add</md-icon></a>

There are a couple points to note. First, notice how much less code is in the component. Component code bases can grow very large. If possible, moving mapping logic out of a component generally improves the component's maintainability over time. Second, there used to be two models, one represented the 'entity' or server-side data model and the other represented the model for the view or 'view model'. This distinction may come back later but for now notice that the component no longer must keep track of the date object and the displayed date string. There is just one model, Routine with one date property.

Like how Angular Material creates an NgModule per component, the same convention can be followed for pipes. This is the NgModule for the TodayAwareDatePipe:

import { NgModule } from '@angular/core';

import { TodayAwareDatePipe } from './today-aware-date.pipe';

@NgModule({
  exports: [TodayAwareDatePipe],
  declarations: [TodayAwareDatePipe]
})
export class TodayAwareDateModule { }

It is imported into the RoutinesModule:

// other imports ...
import { TodayAwareDateModule } from '../pipes/today-aware-date.module';

@NgModule({
  declarations: [
    RoutinesComponent
  ],
  imports: [
    CommonModule,
    MdToolbarModule,
    MdListModule,
    MdIconModule,
    MdButtonModule,
    TodayAwareDateModule
  ],
  exports: [
    RoutinesComponent
  ]
})
export class RoutinesModule { }

Tip: Use the Angular v4 TypeScript Snippets Visual Studio Code extension to quickly create both pipes and NgModules.

To view the full repository, check out the branch for this post in the Bebop Routines project on GitHub.

Conclusion

In this example, you saw how to refactor a component's mapping logic to a re-usable pipe. This helps the component's maintainability by reducing both logic and state. Also, the pipe leverages Angular change detection to ensure the mapping logic is only executed when necessary.

Are you using pipes today? How have you found them to be the most useful? Let me know your thoughts and questions in the comments.

Creating the First Screen with Angular Material

Creating the First Screen with Angular Material

Derived from photo by Andy Melton / flickr.com, CC BY-SA

The last post showed how to prototype the Routines App UI using Adobe XD. This post focuses on building the first screen using Angular Material. Before working on the UI, generate a new project using the Angular CLI. If not familiar with the CLI, this is a good resource with which to get started.

This is one of a series of posts detailing the process of building an application with Angular, Visual Studio, ASP.NET Core, and Azure. See the complete list of posts here.

This is a summary of decisions made and lessons learned while creating this screen:

  • Angular Material documentation has everything needed but it took me some time to get the whole mental model, hopefully this post will help anyone else getting started
  • Unlike Bootstrap, Angular Material's CSS doesn't attempt to canvas the whole screen with default CSS and it's more likely you will have to explicitly opt-in than opt-out of the Angular Material theming
  • While the Angular Material setup takes some time, using the components is straightforward by following the examples in the documentation

Adding Angular Material Dependencies

Getting started with Angular Material requires new npm packages, referencing CSS themes, and referencing fonts for Roboto typography and Material icons. First, install the required npm packages for Angular Material:

npm install @angular/cdk && @angular/material

The @angular/material package is somewhat self-explanatory and contains the modules and CSS required to use Angular Material components. The @angular/cdk is interesting. The CDK stands for component developer kit. This is a set of primitive components with defined functionality and API. Using these, anyone can apply custom visual design and extend with custom functionality. Some of the Angular Material themed components are built on top of CDK counterparts. For more information, see the documentation for the CDK data-table.

Next, add references for the Roboto font and Material icons font. These assets are common to the Material design language and Google makes them available on their CDN. Add the following lines in the <head> element of the src/index.html file:

  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
  <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" rel="stylesheet">

Angular Material currently includes four pre-built theme options for its components:

  • deeppurple-amber.css
  • indigo-pink.css
  • pink-bluegrey.css
  • purple-green.css

To create a custom theme, Angular Material provides a set of SCSS variables and mix-ins. For now, I'm choosing a pre-built theme. To apply, simply include the link to the pre-built CSS file in the src/styles.css file:

@import '~@angular/material/prebuilt-themes/deeppurple-amber.css';

By referencing the pre-built theme in the styles.css file, the Angular CLI bundles this and other referenced CSS together in one file.

If familiar with CSS frameworks such as Bootstrap, you may be used to the framework applying an extensive set of global styles to the page. Angular Material very deliberately tries to isolate its styles to the Angular Material components. For instance, if you add a regular HTML button to the screen without adding any Angular Material specific markup to it, it renders as the browser would.

Similarly, this also means that CSS used to reset browser-default margins are not applied. The first thing I noticed when starting to add the components to the UI is that I had a default padding around my body element. I found the most straight-forward way to handle this is to apply CSS resets as needed in the styles.css file. At this point, I have only needed to remove the default <body> margin and padding:

/* resets */
body {
  margin: 0;
  padding: 0;
}
/* end resets */

Adding Components

Essentially, this screen has three components: a header, list, and a button. Each of the components in Angular Material is contained within its own NgModule so each of the respective NgModules must be included as an import in the NgModule utilizing the components.

In addition, some of the Angular Material components depend on Angular animations. These are imported using the BrowserAnimationsModule. While I haven't seen any issues using the BrowserAnimationsModule in the component's NgModule versus the application's NgModule (or even removing it altogether), I decided to keep it in the application's NgModule to align with the documentation for now. This is the code for the app.module.ts file:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

import { AppComponent } from './app.component';

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

For the RoutinesComponent, create a new folder called routines. In this folder are five files:

  • model.ts – type definitions for model objects
  • routines.component.css – the RoutinesComponent CSS
  • routines.component.html – the RoutinesComponent template HTML
  • routines.component.ts – the RoutinesComponent class
  • routines.module.ts – the NgModule to export the RoutinesComponent

Starting with the RoutinesComponent class, there are several things that happen here to create a static UI. First, take a look at the code:

import { Component, OnInit } from '@angular/core';
import { formatDate, areSameDate, addDays } from '../utility';
import { Routine, RoutinesViewModel, RoutineViewModel } from './models';

@Component({
  selector: 'app-routines',
  templateUrl: './routines.component.html',
  styleUrls: ['./routines.component.css']
})
export class RoutinesComponent implements OnInit {
  private routines: Routine[] = [
    {
      name: 'Morning',
      lastCompletedDate: new Date()
    },
    {
      name: 'Social Media',
      lastCompletedDate: new Date(2017, 8, 23, 19, 55)
    },
    {
      name: 'Bedtime',
      lastCompletedDate: new Date(2017, 8, 5, 21, 16)
    }
  ];

  viewModel: RoutinesViewModel = {
    routines: []
  };

  ngOnInit() {
    this.render();
  }

  trackRoutine(routine: RoutineViewModel) {
    return routine.name;
  }

  // TODO: move to own Pipe or Directive
  private mapToDisplayDateText(date: Date): string {
    // TODO: Make today's date mockable, convert to UTC
    const todaysDate = new Date();
    const isToday = areSameDate(todaysDate, date);
    const yesterdaysDate = addDays(new Date(), -1);
    const isYesterday = areSameDate(yesterdaysDate, date);

    const dayText = isToday ? 'Today' : isYesterday ? 'Yesterday' : formatDate(date, 'dddd, MMMM, D, YYYY');
    const lastCompletedDate = `${dayText}, ${formatDate(date, 'h:mm A')}`;

    return lastCompletedDate;
  }

  private render() {
    const viewModel = {
      routines: this.routines.map(routine => {
        const routineViewModel: RoutineViewModel = {
          lastCompletedDate: this.mapToDisplayDateText(routine.lastCompletedDate),
          name: routine.name
        };

        return routineViewModel;
      })
    };

    this.viewModel = viewModel;
  }
}

The component references several models from models.ts. There is a data structure for Routine which I consider more of a domain entity that represents the 'real-world' routine object. Also, there are a several models that represent the data structure of the UI or 'view models'.

The RoutinesComponent has a render method that maps the domain entity to the component's viewModel property. The viewModel property is the main data structure with which to bind the template. To track the items in the list, the class has a trackRoutine function.

The mapToDisplayDateText method should be moved but it's temporary purpose is to create a custom display format for the date. You will also notice some helper functions for Date objects imported from a utility folder.

This component and the required Angular Material NgModules are configured with the RoutinesModule in the routines.module.ts file:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MdToolbarModule, MdListModule, MdButtonModule, MdIconModule } from '@angular/material';

import { RoutinesComponent } from './routines.component';

@NgModule({
  declarations: [
    RoutinesComponent
  ],
  imports: [
    CommonModule,
    MdToolbarModule,
    MdListModule,
    MdIconModule,
    MdButtonModule
  ],
  exports: [
    RoutinesComponent
  ]
})
export class RoutinesModule { }

In addition to the MdToolbarModule (representing the header), MdListModule, and MdButtonModule, there is a re-usable icon module for the button's icon, MdIconModule. Beyond these imports, most of the Angular Material code is in the RoutinesComponent HTML template in routines.component.html file:

<md-toolbar color="primary">
  <span>Routines</span>
</md-toolbar>

<md-nav-list>
  <md-list-item *ngFor="let routine of viewModel.routines; index as i; trackBy: trackRoutine">
    <a>
      <div md-line>
        <span class="mat-title">{{ routine.name }}</span>
      </div>
      <div md-line>
        <span class="mat-subheading-2">{{ routine.lastCompletedDate }}</span>
      </div>
    </a>
  </md-list-item>
</md-nav-list>

<a md-fab class="add-routine-button"><md-icon>add</md-icon></a>

The header is the MdToolbar using the md-toolbar selector. The template uses the color attribute to apply the theme's primary color to the background. For more information on this component, check out the toolbar documentation.

The routine list is made up of the MdList and MdListItem components. A simple ngFor creates each list item. Within each list item, Angular Material's typography CSS classes mat-title and mat-subheading-2 apply the theme's font sizes. Read more about the MdList and MdListItem in the corresponding documentation.

Finally, the button in this case is an MdAnchor component with the md-fab attribute. There are several styles of buttons with their own selectors. To apply the icon, there is an MdIcon component. The MdIcon component's image is configured in the element's content, in this case add. These icons correspond to the list of Material Icons referenced from Google's CDN in the index.html file.

The button is the only component in this screen that has custom CSS to position it fixed in the bottom right of the screen. The CSS for this is in the routines.component.css file.

Read more about the button components here and the icon component here.

One final note is to include the RoutinesModule in the AppModule and the AppComponent template. To see how this fits together, look at the first-angular-material-screen branch in the GitHub repository.

End of the First Round

This being my first time using Angular Material, I found some snags. Hopefully this post helps you avoid the same. Overall, I like my experience with the library so far and I look forward to implementing more advanced scenarios as this application progresses. Have you given Angular Material a try? What do you think? Leave your thoughts in the comments.