How to set up an Nx-style monorepo workspace with the Angular CLI: Part 5
Lars Gyrup Brink Nielsen
Posted on March 31, 2021
Original cover photo by Edgar Chaparro on Unsplash.
Original publication date: 2020-05-22.
This tutorial is part of the Angular Architectural Patterns series.
In Part 4 of this tutorial, we used our generate project tool to create the check-in data access library, the check-in feature shell library, the check-in desktop application, and the mobile check-in application. We hooked everything up and reviewed how much was automated by our tool.
In this part of the tutorial, we're going to create the seatmap data access library with NgRx feature state. We then created the seat listing feature library and hooked it up to all applications with routing. Finally, we created the shared buttons UI library and the shared formatting utilities library which we used in the seat listing component.
Seatmap data access library
The shared seatmap feature has its own data access library. This is where we would add data services and application state management specific to the seatmap domain.
For now, we'll put the feature store and effects in place by using the --with-state
parameter of the generate project tool. Note that we use the nested grouping folder shared/seatmap
.
The seatmap data access Angular module gives us an overview of what's configured in the seatmap data access library. This is a good starting point.
Everything looks ready to go!
Seat listing feature library
It's time to add the first feature of the seatmap domain which is used in both the check-in and booking applications.
Our tool generates an Angular module and a component for us.
To add this feature to our applications, we add a route to each feature shell module.
As the check-in applications don't have any other features at this moment, we'll use the seatmap 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 {}
The booking application will continue to have the flight search as its default route.
Careful! Before we try to use a seatmap route, we have to configure routes in the seat listing feature.
As the final touch, we register the seatmap data access Angular module.
// 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 {}
Start the mobile check-in application and make sure there are no errors.
You should see the title check-in-mobile
and the message seat-listing works!
.
You might have noticed that the seat listing feature Angular module is similar to a feature shell Angular module. This is because the seat listing component is the entry point for the seatmap domain. However, this Angular module is lazy loaded in case the user won't need it.
Shared buttons UI library
Let's create our first reusable presentational components and expose them in a shared buttons UI library.
Let's delete the default component and create a new confirm button component with a SCAM.
In the following listings, we give the confirm button a simple implementation.
Edit the shared UI buttons module to only export the confirm button SCAM.
// shared-ui-buttons.module.ts
import { NgModule } from '@angular/core';
import { ConfirmButtonModule } from './confirm-button/confirm-button.module';
@NgModule({
exports: [
// š
ConfirmButtonModule, // š
],
})
export class SharedUiButtonsModule {}
Finally, make sure to export the confirm button component class in the library's public API. Consumers might want to hold a reference to an instance or dynamically render a confirm button.
Let's use the confirm button in the seat listing component, even though it can be used in the same way in every domain.
// 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 {}
First add it to the seat listing feature Angular module as seen in the previous listing since it's the declaring Angular module of the seat listing component.
Now that it's in the compilation scope of the seat listing component, we can use it in its template and bind it to the component model.
In the previous listings we pass a message for the confirmation dialog and listens for the user's response which we log to the browser console.
Shared formatting utilities library
Our final workspace library is the shared formatting utilities library.
For now, this library will only expose add a single pure function. Delete the generated Angular module and its test suite.
We'll use Luxon as our date-time library.
Create a file called format-date.ts
in the library's lib
folder.
Expose it in the library's public API. Remember to remove the export of the Angular module that we deleted.
Let's use the formatting function in the seat listing component.
Now expose the UI property in the seat listing template.
Note that it's a terrible practice to get the current date and time directly in a declarable as it makes it non-deterministic and in turn difficult to test. We should have created a separate service for accessing the current date-time. I'll leave that as an exercise for you.
The purpose of this section was to create and expose a formatting utilities library, so I'll leave it as an exercise to you to create an abstraction for accessing the current date-time.
The final file and folder structure of the shared formatting utilities library looks like this:
Conclusion
Start the desktop check-in application by running the ng run check-in-desktop:serve
command. It should look like the following screenshot.
Well done! We now have a full Nrwl Airlines monorepo workspace with multiple applications and workspace libraries as seen in the following figure.
In the final part of this tutorial, we first generated the seatmap data access library with feature state.
Next, we generated the seat listing feature library and added seatmap routing to the check-in and booking feature shell Angular modules. To make this work, we added a single route to the seat listing component in the seatmap listing feature shell Angular module.
As the final part of the seatmap domain, we registered seatmap data access in the seat listing feature Angular module since this is the entry point feature library for the seatmap domain.
We generated the shared buttons UI library, then implemented and exposed the confirm button component which we used to display a check-in confirmation dialog in the seat listing component.
Finally, we generated the shared formatting utilities workspace library and added the format date function which we used in the seat listing component to display the current date as seen in the screenshot of this conclusion.
Tutorial series conclusion
In this tutorial series, we learned how to use the Angular CLI to generate an Nx-style workspace. We used the default schematics, made changes and finally automated those changes using a custom Node.js command line tool. We would do ourselves a favour by converting the tool to Angular schematics, but that's beyond the scope of this article.
View the generate project tool at GitHub Gists.
In the generate project tool, we should also have used the programmatic APIs of the other command line tools we used, but I wanted you to be able to easier identify how our manually run commands relate to the tool.
We created application projects with as little logic as possible. We created small workspace libraries that encapsulate use case-specific business logic or reusable logic.
Using path mappings, both our application projects and library projects can refer to library projects using our chosen import path prefix, the --npm-scope
parameter we passed to the generate project tool.
The Nx CLI is not required to use a monorepo workspace structure and commands. The Nx CLI is built around the same building blocks as the Angular CLI: Schematics, builders, and the workspace configuration.
What's missing?
Nx CLI offers a lot more than schematics. Nx gives us enforcement of architectural boundaries so that dependencies are not created between layers that we won't allow. We could create something like this ourselves, for example using the TSLint import-blacklist
rule, but that would be error-prone and cumbersome.
Nx CLI enables us to generate a dependency graph which visualises the dependencies between our projects and allows us to reason about them. We could use Dependency cruiser to do this.
Nx contains schematics for other frameworks and tooling as well, for example ESLint, Jest, Cypress, Storybook, React, Express, and Nest. We don't have an alternative here, except Nest has their own schematics and Storybook have a command similar to a generator schematic.
Nx adds a ton of commands, tools, and configurations that are helpful to set up a production-grade deployment pipeline. Incremental builds, distributed cache, affected builders, and parallel execution, to mention a few. We could set some of these up ourselves using other tools and configuration, but if you have the opportunity to use it, the Nx CLI will not disappoint you.
Resources
Refer to the GitHub repository LayZeeDK/ngx-nrwl-airlines-workspace
for the full solution.
Peer reviewers
Thank you for helping me whip this tutorial in shape, fellow professionals:
Posted on March 31, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.