Photograph at the top by Edgar Chaparro, originally published on Unsplash.

This piece first appeared on 2020-05-22.

You're reading an entry from the Angular Architectural Patterns tutorial series.

During the previous segment, our generate project utility was used to scaffold the check-in data access library, the check-in feature shell library, the desktop check-in app, and a mobile variation of it. Once the wiring was complete, we took stock of the number of steps the tool had taken care of.

Now, we move on to building the seatmap data access library, which relies on NgRx for feature state. After that, we develop the seat listing feature library and wire it into all applications via routing. The shared buttons UI library and the shared formatting utilities library round out this portion, both of which find a home in the seat listing component.

Library for seatmap data access

Each feature in the shared seatmap area gets its own dedicated data access library. Data services and any application state management tied to the seatmap domain belong in this spot.

npm run generate-project -- library data-access --scope=seatmap --grouping-folder=shared/seatmap --npm-scope=nrwl-airlines --with-state
# or
yarn generate-project library data-access --scope=seatmap --grouping-folder=shared/seatmap --npm-scope=nrwl-airlines --with-state
Enter fullscreen mode Exit fullscreen mode
Generate the seatmap data access library.

At this stage, we’ll add the feature store and effects through the --with-state flag on the project generator. It’s worth mentioning that the grouping folder is set to shared/seatmap, keeping things nested.

// seatmap-data-access.module.ts
import { NgModule } from '@angular/core';
import { EffectsModule } from '@ngrx/effects';
import { StoreModule } from '@ngrx/store';

import { SeatmapEffects } from './+state/seatmap.effects';
import * as fromSeatmap from './+state/seatmap.reducer';

@NgModule({
  imports: [StoreModule.forFeature(fromSeatmap.seatmapFeatureKey, fromSeatmap.reducer), EffectsModule.forFeature([SeatmapEffects])],
})
export class SeatmapDataAccessModule {}
Enter fullscreen mode Exit fullscreen mode
The seatmap data access module.

Want an overview of everything the seatmap data access Angular module has configured? Start with the seatmap data access library itself. That's your best entry point.

ng run seatmap-data-access:lint

ng run seatmap-data-access:test --watch=false
Enter fullscreen mode Exit fullscreen mode
Lint and test the seatmap data access library.

All systems are now fully configured and operational.

Seat listing feature library

This marks the beginning of the seatmap domain's first feature—a capability that both the check-in and booking applications will rely on.

npm run generate-project -- library feature feature-seat-listing --scope=seatmap --grouping-folder=shared/seatmap --npm-scope=nrwl-airlines
# or
yarn generate-project library feature feature-seat-listing --scope=seatmap --grouping-folder=shared/seatmap --npm-scope=nrwl-airlines
Enter fullscreen mode Exit fullscreen mode
Generate the seatmap seat listing feature library.

With this command, Angular CLI takes care of creating both the module and the component automatically.

Now, for our feature to work in each app, a new route needs to be registered inside every feature shell module.

// check-in-feature-shell.module.ts
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CheckInDataAccessModule } from '@nrwl-airlines/check-in/data-access';
import { SharedDataAccessModule } from '@nrwl-airlines/shared/data-access';

import { ShellComponent } from './shell/shell.component';

const routes: Routes = [
  {
    path: '',
    component: ShellComponent,
    children: [
      {
        path: '',
        pathMatch: 'full',
        redirectTo: 'seatmap', // 👈
      },
      {
        path: 'seatmap', // 👈
        loadChildren: () => import('@nrwl-airlines/seatmap/feature-seat-listing').then((esModule) => esModule.SeatmapFeatureSeatListingModule),
      },
    ],
  },
];

@NgModule({
  declarations: [ShellComponent],
  exports: [RouterModule],
  imports: [RouterModule.forRoot(routes), SharedDataAccessModule, CheckInDataAccessModule, CommonModule],
})
export class CheckInFeatureShellModule {}
Enter fullscreen mode Exit fullscreen mode
Check-in feature shell module with a route to the seat listing.

Since the check-in apps currently lack additional functionality, the seatmap will serve as the default route.

// booking-feature-shell.module.ts
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { BookingDataAccessModule } from '@nrwl-airlines/booking/data-access';
import { SharedDataAccessModule } from '@nrwl-airlines/shared/data-access';

import { ShellComponent } from './shell/shell.component';

const routes: Routes = [
  {
    path: '',
    component: ShellComponent,
    children: [
      {
        path: '',
        pathMatch: 'full',
        redirectTo: 'flight-search',
      },
      {
        path: 'flight-search',
        loadChildren: () => import('@nrwl-airlines/booking/feature-flight-search').then((esModule) => esModule.BookingFeatureFlightSearchModule),
      },
      {
        path: 'passenger-info',
        loadChildren: () => import('@nrwl-airlines/booking/feature-passenger-info').then((esModule) => esModule.BookingFeaturePassengerInfoModule),
      },
      {
        path: 'seatmap', // 👈
        loadChildren: () => import('@nrwl-airlines/seatmap/feature-seat-listing').then((esModule) => esModule.SeatmapFeatureSeatListingModule),
      },
    ],
  },
];

@NgModule({
  declarations: [ShellComponent],
  exports: [RouterModule],
  imports: [RouterModule.forRoot(routes), SharedDataAccessModule, BookingDataAccessModule, CommonModule],
})
export class BookingFeatureShellModule {}
Enter fullscreen mode Exit fullscreen mode

Within the booking application, the flight search remains the default landing route.

Watch out! Prior to attempting any navigation to a seatmap route, route configuration within the seat listing feature must be established first.

// seatmap-feature-seat-listing.module.ts
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { SeatListingComponent } from './seat-listing/seat-listing.component';

const routes: Routes = [
  // 👈
  {
    path: '',
    pathMatch: 'full',
    component: SeatListingComponent,
  },
];

@NgModule({
  declarations: [SeatListingComponent],
  imports: [
    RouterModule.forChild(routes), // 👈
    CommonModule,
  ],
})
export class SeatmapFeatureSeatListingModule {}
Enter fullscreen mode Exit fullscreen mode
Seat listing feature module with default route.

To wrap things up, the seatmap data access Angular module gets registered.

// seatmap-feature-seat-listing.module.ts
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { SeatmapDataAccessModule } from '@nrwl-airlines/seatmap/data-access';

import { SeatListingComponent } from './seat-listing/seat-listing.component';

const routes: Routes = [
  {
    path: '',
    pathMatch: 'full',
    component: SeatListingComponent,
  },
];

@NgModule({
  declarations: [SeatListingComponent],
  imports: [
    RouterModule.forChild(routes),
    SeatmapDataAccessModule, // 👈
    CommonModule,
  ],
})
export class SeatmapFeatureSeatListingModule {}
Enter fullscreen mode Exit fullscreen mode

Launch the mobile check-in app and verify that it runs without any errors.

ng run check-in-mobile:serve
Enter fullscreen mode Exit fullscreen mode
Start the development server for the mobile check-in web app.

In the browser, the title should read check-in-mobile, and the displayed text should be seat-listing works!.

The seat listing feature Angular module bears a strong resemblance to a feature shell Angular module. That similarity exists because the seat listing component serves as the main entry point for the seatmap domain. Still, this Angular module is loaded lazily to avoid fetching it unless the user actually needs it.

Shared buttons UI library

Now we’ll build our first set of reusable presentational components and make them available through a dedicated shared buttons UI library.

npm run generate-project -- library ui ui-buttons --scope=shared --npm-scope=nrwl-airlines
# or
yarn generate-project library ui ui-buttons --scope=shared --npm-scope=nrwl-airlines
Enter fullscreen mode Exit fullscreen mode
Generate shared buttons UI library.

We’ll remove the boilerplate component and scaffold a fresh confirm button using a SCAM pattern instead.

npx rimraf libs/shared/ui-buttons/src/lib/buttons

ng generate module confirm-button --project=shared-ui-buttons

ng generate component confirm-button --project=shared-ui-buttons --export --display-block
Enter fullscreen mode Exit fullscreen mode
Delete the default component and create a confirm button component.

The confirm button is given a straightforward implementation in the listings below.

<!-- confirm-button.component.html -->
<button (click)="onClick()">
  <ng-content></ng-content>
</button>
Enter fullscreen mode Exit fullscreen mode
The confirm button's template projects content into the button and binds the `click` event to its event handler.
// confirm-button.component.ts
import { Component, EventEmitter, Input, Output } from '@angular/core';

@Component({
  selector: 'nrwl-airlines-confirm-button',
  styleUrls: ['./confirm-button.component.css'],
  templateUrl: './confirm-button.component.html',
})
export class ConfirmButtonComponent {
  @Input()
  message = 'Do you confirm this action?';

  @Output()
  confirmed = new EventEmitter<boolean>();

  onClick() {
    this.confirmed.emit(confirm(this.message));
  }
}
Enter fullscreen mode Exit fullscreen mode
The confirm button prompts the user with the `confirm()` dialog that has the message defined by its input property, then emits the user's answer through its output property.

Open the shared UI buttons module and adjust its exports so that only the SCAM for the confirm button is exposed.

// shared-ui-buttons.module.ts
import { NgModule } from '@angular/core';

import { ConfirmButtonModule } from './confirm-button/confirm-button.module';

@NgModule({
  exports: [
    // 👈
    ConfirmButtonModule, // 👈
  ],
})
export class SharedUiButtonsModule {}
Enter fullscreen mode Exit fullscreen mode

As a final step, ensure the confirm button component class is exported through the library's public API. This enables consumers to retain a reference to an instance or to render a confirm button dynamically.

// libs/shared/ui-buttons/src/index.ts
/*
 * Public API Surface of shared-ui-buttons
 */

export * from './lib/shared-ui-buttons.module';
export * from './lib/confirm-button/confirm-button.component'; // 👈
Enter fullscreen mode Exit fullscreen mode
Public API exposing the confirm button component class.

The confirm button appears in the seat listing component for now, but the exact same approach applies to any other domain you might use it in.

// seatmap-feature-seat-listing.module.ts
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { SeatmapDataAccessModule } from '@nrwl-airlines/seatmap/data-access';
import { SharedUiButtonsModule } from '@nrwl-airlines/shared/ui-buttons'; // 👈

import { SeatListingComponent } from './seat-listing/seat-listing.component';

const routes: Routes = [
  {
    path: '',
    pathMatch: 'full',
    component: SeatListingComponent,
  },
];

@NgModule({
  declarations: [SeatListingComponent],
  imports: [
    RouterModule.forChild(routes),
    SeatmapDataAccessModule,
    CommonModule,
    SharedUiButtonsModule, // 👈
  ],
})
export class SeatmapFeatureSeatListingModule {}
Enter fullscreen mode Exit fullscreen mode

Because the seat listing component's declaring module is the seat listing feature module (as covered in the preceding listing), that is where you need to register it initially.

With that import in place, the seat listing component's compilation scope now includes the store, so its template can reference it and link it to the component's state.

<!-- seat-listing.component.html -->
<p>seat-listing works!</p>

<nrwl-airlines-confirm-button message="Do you confirm checking in at this seat?" (confirmed)="onSeatConfirmed($event)"> Check in </nrwl-airlines-confirm-button>
Enter fullscreen mode Exit fullscreen mode
The seat listing component template which uses the confirm button.
// seat-listing.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'seatmap-seat-listing',
  styleUrls: ['./seat-listing.component.css'],
  templateUrl: './seat-listing.component.html',
})
export class SeatListingComponent {
  onSeatConfirmed(isConfirmed: boolean): void {
    // 👈
    console.log('Is seat confirmed?', isConfirmed);
  }
}
Enter fullscreen mode Exit fullscreen mode
The seat listing component model which is bound to the confirm button.

In the code snippets above, we send a message to the confirmation dialog and then subscribe to the user's choice, outputting that result to the browser's console.

Shared formatting utilities library

The last library we build in this workspace is dedicated to formatting utilities shared across the project.

npm run generate-project -- library util util-formatting --scope=shared --npm-scope=nrwl-airlines
# or
yarn generate-project library util util-formatting --scope=shared --npm-scope=nrwl-airlines
Enter fullscreen mode Exit fullscreen mode
Generate the shared formatting utilities library.

At this stage, the library’s only responsibility is to surface one pure function. Remove the generated Angular module along with its corresponding tests.

npx rimraf libs/shared/util-formatting/src/lib/*.module*.ts
Enter fullscreen mode Exit fullscreen mode
Delete the shared formatting utilities Angular module and its test suite.

For handling dates and times in this project, Luxon is the library we're going with.

npm install luxon
npm install --save-dev @types/luxon
# or
yarn add luxon
yarn add --dev @types/luxon
Enter fullscreen mode Exit fullscreen mode
Install Luxon.

Inside the library's lib directory, add a new file with the name format-date.ts.

// format-date.ts
import { DateTime } from 'luxon';

export function formatDate(luxonDate: DateTime): string {
  return luxonDate.toLocaleString({ ...DateTime.DATE_MED, weekday: 'long' });
}
Enter fullscreen mode Exit fullscreen mode
A function to format a date in our preferred display format.

Add it to the library's public API, and don't forget to drop the exported Angular module we removed.

// libs/shared/util-formatting/src/index.ts
/*
 * Public API Surface of shared-util-formatting
 */

export * from './lib/format-date'; // 👈
Enter fullscreen mode Exit fullscreen mode
The date formatting function is exposed in the shared formatting utilities library's public API.

Now we can apply the same formatting helper inside the seat listing component.

// seat-listing.component.ts
import { Component } from '@angular/core';
import { formatDate } from '@nrwl-airlines/shared/util-formatting'; // 👈
import { DateTime } from 'luxon'; // 👈

@Component({
  selector: 'seatmap-seat-listing',
  styleUrls: ['./seat-listing.component.css'],
  templateUrl: './seat-listing.component.html',
})
export class SeatListingComponent {
  get today(): string {
    // 👈
    const now = DateTime.local();

    return formatDate(now);
  }

  onSeatConfirmed(isConfirmed: boolean): void {
    console.log('Is seat confirmed?', isConfirmed);
  }
}
Enter fullscreen mode Exit fullscreen mode
The seat listing component model uses the `formatDate` function.

With the state in place, it’s time to surface the UI property inside the seat listing template.

<!-- seat-listing.component.html -->
<p>Today is {{ today }}<!-- ? --></p>

<nrwl-airlines-confirm-button message="Do you confirm checking in at this seat?" (confirmed)="onSeatConfirmed($event)"> Check in </nrwl-airlines-confirm-button>
Enter fullscreen mode Exit fullscreen mode
Seat listing component template presenting the `today` UI property.

Calling date and time methods straight inside a declarable is considered bad practice — it breaks determinism and complicates testing. A dedicated service should have been the approach for date-time retrieval. Feel free to implement that on your own.

Since the focus here was merely on establishing a formatting utilities library, making date-time access an abstracted service remains a task for you to tackle independently.

Here’s how the shared formatting utilities library’s file and folder arrangement ends up:

libs/shared/util-formatting
├── src
│   ├── lib
│   │   └── format-date.ts
│   ├── index.ts
│   └── test.ts
├── README.md
├── karma.conf.js
├── tsconfig.lib.json
├── tsconfig.spec.json
└── tslint.json
Enter fullscreen mode Exit fullscreen mode
The final file and folder structure of the shared formatting utilities library.

Conclusion

Launch the desktop check-in app using ng run check-in-desktop:serve. This should produce a UI matching the screenshot shown below.

The check-in desktop application with the NgRx Store DevTools open.

The check-in desktop application with the NgRx Store DevTools open.

Great work! As shown below, the Nrwl Airlines monorepo workspace is now fully equipped with multiple apps alongside workspace libraries.

nrwl-airlines
├── apps
│   ├── booking
│   │   ├── booking-desktop
│   │   ├── booking-desktop-e2e
│   │   ├── booking-mobile
│   │   └── booking-mobile-e2e
│   └── check-in
│       ├── check-in-desktop
│       ├── check-in-desktop-e2e
│       ├── check-in-mobile
│       └── check-in-mobile-e2e
├── libs
│   ├── booking
│   │   ├── data-access
│   │   ├── feature-flight-search
│   │   ├── feature-passenger-info
│   │   └── feature-shell
│   ├── check-in
│   │   ├── data-access
│   │   └── feature-shell
│   └── shared
│       ├── data-access
│       ├── environments
│       ├── seatmap
│       │   ├── data-access
│       │   └── feature-seat-listing
│       ├── ui-buttons
│       └── util-formatting
└── tools
Enter fullscreen mode Exit fullscreen mode
The final folder structure of our Nrwl Airlines monorepo.

At the end of this series, we first created the seatmap data access library bundled with feature state.

After that, we scaffolded the seat listing feature library, then wired seatmap routing into the check-in and booking feature shell Angular modules. For that to function, we inserted one route pointing to the seat listing component inside the seatmap listing feature shell Angular module.

To complete the seatmap domain, we wired seatmap data access into the seat listing feature Angular module, which serves as the domain's primary feature library.

We scaffolded the shared buttons UI library, then built and exported the confirm button component. That component displayed a check-in confirmation dialog within the seat listing component.

Last, we created the shared formatting utilities workspace library, adding the format date function. The seat listing component used it to render the current date, matching the screenshot found in this closing section.

Tutorial series conclusion

This tutorial series showed how to generate an Nx-style workspace with the Angular CLI. We relied on default schematics, adapted them, and finally scripted those changes with a custom Node.js command-line tool. Converting that tool into Angular schematics would be beneficial, though it falls outside what this article covers.

View the generate project tool at GitHub Gists.

The generate project tool could have used the programmatic APIs of the other command-line tools we employed, but keeping the calls explicit makes it simpler to see how the manually entered commands map to the tool's behavior.

We built application projects containing minimal logic. We produced small workspace libraries that either encapsulate logic tied to a specific use case or hold reusable functionality.

Thanks to path mappings, both application and library projects can reference library projects using the import path prefix we chose—the --npm-scope value provided to the generate project tool.

A monorepo workspace structure and its commands don't require the Nx CLI. Nx CLI shares the same base components as the Angular CLI, including Schematics, builders, and workspace configuration.

What's missing?

Nx CLI encompasses more than just schematics. It enforces architectural boundaries, preventing dependencies between layers we choose to isolate. Building that ourselves, for instance with the TSLint import-blacklist rule, would be both tedious and error-prone.

Nx CLI lets us generate a dependency graph that shows how our projects are interconnected, aiding our reasoning about those links. A similar result could come from Dependency cruiser.

Nx also provides schematics for additional frameworks and tools, such as ESLint, Jest, Cypress, Storybook, React, Express, and Nest. There's no direct replacement for most, though Nest ships its own schematics and Storybook has a generator-like command.

Nx introduces many commands, utilities, and settings that simplify creating a production-ready deployment pipeline—incremental builds, distributed cache, affected builders, and parallel execution are just some. Some of those could be assembled using other tools and setup, but when available, the Nx CLI won't let you down.

Resources

Check the GitHub repository LayZeeDK/ngx-nrwl-airlines-workspace for the complete solution.

Peer reviewers

I appreciate the professionals who helped refine this tutorial: