Passenger Feature Library
We begin by scaffolding the passenger feature library, which falls within the booking domain.
npm run generate-project -- library feature feature-passenger-info --scope=booking --npm-scope=nrwl-airlines
# or
yarn generate-project library feature feature-passenger-info --scope=booking --npm-scope=nrwl-airlines
Once the generation command finishes executing with the specified parameters, the resulting directory layout is displayed below.
libs/booking/feature-passenger-info
├── src
│ ├── lib
│ │ ├── passenger-info
│ │ │ ├── passenger-info.component.css
│ │ │ ├── passenger-info.component.html
│ │ │ ├── passenger-info.component.spec.ts
│ │ │ └── passenger-info.component.ts
│ │ ├── booking-feature-passenger-info.module.spec.ts
│ │ └── booking-feature-passenger-info.module.ts
│ ├── index.ts
│ └── test.ts
├── README.md
├── karma.conf.js
├── tsconfig.lib.json
├── tsconfig.spec.json
└── tslint.json
This structure differs from what we saw with the feature shell and data access libraries.
After the generation tool creates the workspace library complete with an entry module, it proceeds to execute the commands listed here.
Notice that the tool no longer includes the --no-common-module flag on the ng generate module invocation, since this module is responsible for declaring components.
ng generate component passenger-info --project=booking-feature-passenger-info --module=booking-feature-passenger-info.module.ts --display-block
Let's inspect the module that was produced by the tool.
// booking-feature-passenger-info.module.ts
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { PassengerInfoComponent } from './passenger-info/passenger-info.component';
@NgModule({
declarations: [PassengerInfoComponent],
imports: [CommonModule],
})
export class BookingFeaturePassengerInfoModule {}
The entry module from the listing serves as a solid foundation. However, we still need to establish routing for our component within this feature. The following listing demonstrates how to add that.
// booking-feature-passenger-info.module.ts
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { PassengerInfoComponent } from './passenger-info/passenger-info.component';
const routes: Routes = [
{
path: '',
pathMatch: 'full',
component: PassengerInfoComponent,
},
];
@NgModule({
declarations: [PassengerInfoComponent],
imports: [RouterModule.forChild(routes), CommonModule],
})
export class BookingFeaturePassengerInfoModule {}
Excellent — the feature library is now ready to integrate with the routing configuration of the feature shell library.
The generated component is standard fare. What it would render in a production booking system is irrelevant for this discussion.
Let's wire this feature into the application's navigation by adding a route entry to the booking feature shell module, as shown below.
// booking-feature-shell.module.ts
import { Routes } from '@angular/router';
import { ShellComponent } from './shell/shell.component';
const routes: Routes = [
{
path: '',
component: ShellComponent,
children: [
{
path: 'passenger-info',
loadChildren: () => import('@nrwl-airlines/booking/feature-passenger-info').then((esModule) => esModule.BookingFeaturePassengerInfoModule),
},
],
},
];
Flight Search Feature Library
The booking domain still needs one more feature library. Execute this command to generate the flight search library.
npm run generate-project -- library feature feature-flight-search --scope=booking --npm-scope=nrwl-airlines
# or
yarn generate-project library feature feature-flight-search --scope=booking --npm-scope=nrwl-airlines
We'll apply the same modifications as we did for the passenger feature library—adding routing for the entry component and configuring a lazy route in the feature shell module.
// booking-feature-flight-search.module.ts
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { FlightSearchComponent } from './flight-search/flight-search.component';
const routes: Routes = [
{
path: '',
pathMatch: 'full',
component: FlightSearchComponent,
},
];
@NgModule({
declarations: [FlightSearchComponent],
imports: [RouterModule.forChild(routes), CommonModule],
})
export class BookingFeatureFlightSearchModule {}
// booking-feature-shell.module.ts
import { Routes } from '@angular/router';
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),
},
],
},
];
As seen in the last listing, we've also set up the fallback route to redirect to the flight search path.
Here's what our project tree looks like now.
nrwl-airlines
├── apps
│ └── booking
│ ├── booking-desktop
│ └── booking-desktop-e2e
└── libs
├── booking
│ ├── data-access
│ ├── feature-flight-search
│ ├── feature-passenger-info
│ └── feature-shell
└── shared
├── data-access
└── environments
With a few routes in place, launch the desktop booking app via npm start or yarn start and visit http://localhost:4200/flight-search and http://localhost:4200/passenger-info.
The booking domain is complete. Next, we'll set up the mobile booking application.
Mobile Booking Application
I've expanded the generate project tool with an application command that automates the exact steps we used to scaffold the desktop booking app.
Run this command to create the application project.
npm run generate-project -- application booking-mobile --scope=booking --grouping-folder=booking --npm-scope=nrwl-airlines
# or
yarn generate-project application booking-mobile --scope=booking --grouping-folder=booking --npm-scope=nrwl-airlines
Now we have two virtually identical applications. Is that sensible? Nrwl's book covers an adaptive layout strategy where the choice of application is determined by browser detection, allowing both booking web apps to share the same URL.
How do we distinguish components across two application projects that share orchestration through a common feature shell library? For several approaches, check out "Shell Library patterns with Nx and Monorepo Architectures" by Nacho Vazquez, or simply leverage file replacements defined in angular.json.
For this case study, we'll go with the file replacement method.
First, generate a mobile-specific template for the flight search component using this command.
"<p>mobile flight-search works!</p>" > libs/booking/feature-flight-search/src/lib/flight-search/flight-search.mobile.component.html
Next, update the build and serve configurations for the mobile application to swap in the mobile template via file replacements.
# development configuration
ng config projects["booking-mobile"].architect.build.options.fileReplacements []
ng config projects["booking-mobile"].architect.build.options.fileReplacements[0].replace "libs/booking/feature-flight-search/src/lib/flight-search/flight-search.component.html"
ng config projects["booking-mobile"].architect.build.options.fileReplacements[0].with "libs/booking/feature-flight-search/src/lib/flight-search/flight-search.mobile.component.html"
# production configuration
ng config projects["booking-mobile"].architect.build.configurations.production.fileReplacements[1].replace "libs/booking/feature-flight-search/src/lib/flight-search/flight-search.component.html"
ng config projects["booking-mobile"].architect.build.configurations.production.fileReplacements[1].with "libs/booking/feature-flight-search/src/lib/flight-search/flight-search.mobile.component.html"
One caveat: the file replacement entries must be manually kept in sync between the development and production configurations.
That gives us these file replacement settings for the mobile booking app project.
{
"//": "angular.json",
"projects": {
"booking-mobile": {
"architect": {
"build": {
"options": {
"fileReplacements": [
{
"replace": "libs/booking/feature-flight-search/src/lib/flight-search/flight-search.component.html",
"with": "libs/booking/feature-flight-search/src/lib/flight-search/flight-search.mobile.component.html"
}
]
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "libs/shared/environments/src/lib/environment.ts",
"with": "libs/shared/environments/src/lib/environment.prod.ts"
},
{
"replace": "libs/booking/feature-flight-search/src/lib/flight-search/flight-search.component.html",
"with": "libs/booking/feature-flight-search/src/lib/flight-search/flight-search.mobile.component.html"
}
]
}
}
}
}
}
}
}
Verify everything works by running the commands below. You should see the output mobile flight-search works!.
ng run booking-mobile:serve
Don't forget to run the linter and tests on our newly created projects to keep the workspace clean.
ng run booking-mobile:lint
ng run booking-mobile:test --watch=false
ng run booking-mobile-e2e:lint
ng run booking-mobile-e2e:e2e
Well done—our application project structure now looks like this.
nrwl-airlines
└── apps
└── booking
├── booking-desktop
├── booking-desktop-e2e
├── booking-mobile
└── booking-mobile-e2e
Wrap-Up
Boot up the mobile booking app by running ng run booking-mobile:serve.
The complete workspace folder layout is captured in this figure.
nrwl-airlines
├── apps
│ └── booking
│ ├── booking-desktop
│ ├── booking-desktop-e2e
│ ├── booking-mobile
│ └── booking-mobile-e2e
├── libs
│ ├── booking
│ │ ├── data-access
│ │ ├── feature-flight-search
│ │ ├── feature-passenger-info
│ │ └── feature-shell
│ └── shared
│ ├── data-access
│ └── environments
└── tools
During this section of the tutorial, our generate project tool helped us create the passenger feature library.
To enable navigation within that feature, we registered a single route targeting its component in the passenger feature module. We then attached a lazy-loaded route in the booking feature shell module.
We then scaffolded the flight search feature library. As before, we added its routing and configured the default path to direct users to the flight search route within the booking feature shell module.
After confirming the routes were operational, we used the generation tool for the first time to create both an application and an end-to-end test project—specifically, the mobile booking app and its E2E suite.
For the adaptive layout idea from Nrwl's publication, we employed file replacements to provide a mobile-specific template for the flight search component.
Finally, we ran the linter and the test suites for our new projects, then launched the mobile booking application; its current state is shown in the screenshot above.
In Part 4, we'll add two more workspace libraries and two applications within the booking domain, wiring up data access and the feature shell module. We'll also assess how much of this process our generate project tool automates.
Further Reading
For those who prefer to skip ahead, the complete code is available in the LayZeeDK/ngx-nrwl-airlines-workspace GitHub repository

