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
Enter fullscreen mode Exit fullscreen mode
Generate passenger info feature library.

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
Enter fullscreen mode Exit fullscreen mode
The file and folder structure of the booking passenger info feature library.

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
Enter fullscreen mode Exit fullscreen mode
Generate component command run when generating a feature library.

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 {}
Enter fullscreen mode Exit fullscreen mode
Initial entry point Angular module in the passenger info feature library.

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 {}
Enter fullscreen mode Exit fullscreen mode
Passenger info feature Angular module with route configuration for its entry point component.

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),
      },
    ],
  },
];
Enter fullscreen mode Exit fullscreen mode
Route configuration for the passenger info feature.

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
Enter fullscreen mode Exit fullscreen mode
Generate flight search feature library.

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 {}
Enter fullscreen mode Exit fullscreen mode
Booking flight search feature module with entry point component route.
// 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),
      },
    ],
  },
];
Enter fullscreen mode Exit fullscreen mode
Route configuration for the flight search feature.

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
Enter fullscreen mode Exit fullscreen mode
Project folder structure after setting up the flight search feature library.

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
Enter fullscreen mode Exit fullscreen mode
Generate mobile booking application.

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
Enter fullscreen mode Exit fullscreen mode
Create mobile flight search component template.

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"
Enter fullscreen mode Exit fullscreen mode
Replace flight search component template with mobile version.

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"
                }
              ]
            }
          }
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
File replacement configuration for the mobile desktop application, enabling the mobile flight search component template.

Verify everything works by running the commands below. You should see the output mobile flight-search works!.

ng run booking-mobile:serve
Enter fullscreen mode Exit fullscreen mode
Start development server for the mobile booking application.

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
Enter fullscreen mode Exit fullscreen mode
Lint and test the mobile booking application and end-to-end testing projects.

Well done—our application project structure now looks like this.

nrwl-airlines
└── apps
     └── booking
         ├── booking-desktop
         ├── booking-desktop-e2e
         ├── booking-mobile
         └── booking-mobile-e2e
Enter fullscreen mode Exit fullscreen mode
All booking application and end-to-end testing projects are ready.

Wrap-Up

Boot up the mobile booking app by running ng run booking-mobile:serve.

The mobile booking application with the mobile version of the flight search feature.

The mobile booking application with the mobile version of the flight search feature.

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
Enter fullscreen mode Exit fullscreen mode
The workspace folder structure after Part 3.

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