How to set up an Nx-style monorepo workspace with the Angular CLI: Part 4

layzee

Lars Gyrup Brink Nielsen

Posted on March 31, 2021

How to set up an Nx-style monorepo workspace with the Angular CLI: Part 4

Original cover photo by Edgar Chaparro on Unsplash.

Original publication date: 2020-05-19.

This tutorial is part of the Angular Architectural Patterns series.

In Part 3 of this tutorial, we created two feature libraries with routed components and hooked them up to the check-in feature shell library. We used the generate project tool to create the mobile check-in application and created a mobile-specific template for the flight search component.

In this part, we're going to create two workspace libraries in the check-in domain, the check-in data access library and the check-in feature shell library. We register data access in the feature shell library, create the check-in desktop application and hook up the feature shell Angular module. After reviewing and verifying how much of this is automated by our generate project tool, we quickly generate the mobile check-in application.

Check-in data access library

Let's move on to the check-in domain. This time, we'll start with the data access library. I added an extra option called --with-state to generate a feature store and effects in the +state folder.

npm run generate-project -- library data-access --scope=check-in --npm-scope=nrwl-airlines --with-state
# or
yarn generate-project library data-access --scope=check-in --npm-scope=nrwl-airlines --with-state
Enter fullscreen mode Exit fullscreen mode
Generate check-in data access library with NgRx-based state.

This generates the following file and folder structure.

libs/check-in/data-access
├── src
│   ├── lib
│   │   ├── +state
│   │   │   ├── check-in.actions.spec.ts
│   │   │   ├── check-in.actions.ts
│   │   │   ├── check-in.effects.spec.ts
│   │   │   ├── check-in.effects.ts
│   │   │   ├── check-in.reducer.spec.ts
│   │   │   ├── check-in.reducer.ts
│   │   │   ├── check-in.selectors.spec.ts
│   │   │   └── check-in.selectors.ts
│   │   ├── check-in-data-access.module.spec.ts
│   │   └── check-in-data-access.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
Generated file and folder structure for the check-in data access library.

How sweet is this? One command and we've got a project-specific data access library set up with project configuration, path mappings, feature store, and feature effects.

Lint and test the project. Correct any errors like you did earlier.

ng run check-in-data-access:lint

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

Check-in feature shell library

We use the generate project tool to generate the check-in feature shell.

npm run generate-project -- library feature feature-shell --scope=check-in --npm-scope=nrwl-airlines
# or
yarn generate-project library feature feature-shell --scope=check-in --npm-scope=nrwl-airlines
Enter fullscreen mode Exit fullscreen mode
Generate check-in feature shell library.

Everything's now set up for us.

libs/check-in/feature-shell
├── src
│   ├── lib
│   │   ├── shell
│   │   │   ├── shell.component.css
│   │   │   ├── shell.component.html
│   │   │   ├── shell.component.spec.ts
│   │   │   └── shell.component.ts
│   │   ├── check-in-feature-shell.module.spec.ts
│   │   └── check-in-feature-shell.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
Generated file and folder structure for the check-in feature shell library.

Let's take a quick look at the check-in feature shell Angular module.

// check-in-feature-shell.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

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

const routes: Routes = [
  {
    path: '',
    component: ShellComponent,
    children: [],
  },
];

@NgModule({
  declarations: [ShellComponent],
  exports: [RouterModule],
  imports: [RouterModule.forRoot(routes)],
})
export class CheckInFeatureShellModule {}
Enter fullscreen mode Exit fullscreen mode
Check-in feature shell module.

We need to register the shared data access and check-in data access Angular modules.

// check-in-feature-shell.module.ts
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: [],
  },
];

@NgModule({
  declarations: [ShellComponent],
  exports: [RouterModule],
  imports: [
    RouterModule.forRoot(routes),
    SharedDataAccessModule, // ?
    CheckInDataAccessModule, // ?
  ],
})
export class CheckInFeatureShellModule {}
Enter fullscreen mode Exit fullscreen mode

Great! Since we don't have any features to add to the check-in application yet, we can move on to the next project for now.

Check-in desktop application

It's time to create the first check-in application, the desktop web app. I've added support for feature shells in the generate project tool. If a feature shell library exists in the same scope, the application will be generated with the same changes as we made earlier in this article. We just need to make sure to use the --npm-scope parameter.

npm run generate-project -- application check-in-desktop --scope=check-in --grouping-folder=check-in --npm-scope=nrwl-airlines
# or
yarn generate-project application check-in-desktop --scope=check-in --grouping-folder=check-in --npm-scope=nrwl-airlines
Enter fullscreen mode Exit fullscreen mode
Generate the check-in desktop application using its feature shell library.

Let's make sure it worked. Open up the app module of the new application project.

// apps/check-in/check-in-desktop/src/app/app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { CheckInFeatureShellModule } from '@nrwl-airlines/check-in/feature-shell';

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

@NgModule({
  bootstrap: [AppComponent],
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    CheckInFeatureShellModule, // ?
  ],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode
App module of the check-in desktop application.

The app component is generated with the same template as our other apps.

<!-- apps/check-in/check-in-desktop/src/app/app.component.html -->
<h1>{{title}}</h1>

<router-outlet></router-outlet>
Enter fullscreen mode Exit fullscreen mode
Check-in desktop app component template.

As seen in the app module, we eagerly load the check-in feature shell module which routes to its shell component as we saw in the previous section.

The changes to the app component are reflected in its test suite.

// apps/check-in/check-in-desktop/src/app/app.component.spec.ts
import { TestBed, async } from '@angular/core/testing';
import { RouterModule } from '@angular/router';

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

describe('AppComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [AppComponent],
      imports: [
        RouterModule.forRoot([]), // ?
      ],
    }).compileComponents();
  }));

  it('should create the app', () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.componentInstance;
    expect(app).toBeTruthy();
  });

  it(`should have as title 'check-in-desktop'`, () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.componentInstance;
    expect(app.title).toEqual('check-in-desktop');
  });

  it('should render title', () => {
    const fixture = TestBed.createComponent(AppComponent);
    fixture.detectChanges();
    const compiled = fixture.nativeElement;
    expect(compiled.querySelector('h1').textContent) // ?
      .toContain('check-in-desktop'); // ?
  });
});
Enter fullscreen mode Exit fullscreen mode
Test suite for the check-in desktop app component.

Our Angular testing module adds the router module to make us able to render the app component's template. The heading selector and content now reflects the simple template.

The final change that we want to check is the usage of the shared environments workspace library in the main file.

// apps/check-in/check-in-desktop/src/main.ts
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from '@nrwl-airlines/shared/environments'; // ?

if (environment.production) {
  enableProdMode();
}

platformBrowserDynamic()
  .bootstrapModule(AppModule)
  .catch((err) => console.error(err));
Enter fullscreen mode Exit fullscreen mode
The check-in desktop application main file.

The generate project tool detects a shared environments library, changes the import statement in the main file and additionally delete the src/environments folder and files which are usually generated as part of an application project.

Now, let's make sure that we're using the shared environments library in the fileReplacements option of our application project's configuration.

{
  "//": "angular.json",
  "projects": {
    "check-in-desktop": {
      "architect": {
        "build": {
          "configurations": {
            "production": {
              "fileReplacements": [
                {
                  "replace": "libs/shared/environments/src/lib/environment.ts",
                  "with": "libs/shared/environments/src/lib/environment.prod.ts"
                }
              ]
            }
          }
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
File replacements configured to use the shared environments library for the check-in desktop application.

We're looking good!

ng run check-in-desktop:lint

ng run check-in-desktop:test --watch=false
Enter fullscreen mode Exit fullscreen mode
Lint and test the check-in desktop application.

Lint checks and the unit test suite run as expected.

Let's verify the changes to the end-to-end test suites.

// apps/check-in/check-in-desktop-e2e/src/app.po.ts
import { browser, by, element } from 'protractor';

export class AppPage {
  navigateTo(): Promise<unknown> {
    return browser.get(browser.baseUrl) as Promise<unknown>;
  }

  getTitleText(): Promise<string> {
    return element(by.css('check-in-root h1')).getText() as Promise<string>; // ?
  }
}
Enter fullscreen mode Exit fullscreen mode
App page object for the booking desktop application.

For some reason, Angular only leaves out the last name of the project-name for the root element. In this case, event though the application project's name is check-in-desktop, the root element name is check-in-root. The generate project tool respects this.

In the booking apps, the root element name is booking-root rather than booking-desktop-root and booking-mobile-root. This can also be seen in the index.html file of an application project generated using the Angular CLI.

ng run check-in-desktop-e2e:lint

ng run check-in-desktop-e2e:e2e
Enter fullscreen mode Exit fullscreen mode
Lint and run the end-to-end test suite of the check-in desktop application.

The end-to-end test suite is linted and tested successfully.

ng run check-in-desktop:serve
Enter fullscreen mode Exit fullscreen mode
Start the development server for the check-in desktop application.

Finally, start the application and verify that it renders the title. As no feature libraries have been added yet, only the title is shown, but no errors are thrown.

Mobile check-in application

The final application project is the mobile check-in web app.

npm run generate-project -- application check-in-mobile --scope=check-in --grouping-folder=check-in --npm-scope=nrwl-airlines
# or
yarn generate-project application check-in-mobile --scope=check-in --grouping-folder=check-in --npm-scope=nrwl-airlines
Enter fullscreen mode Exit fullscreen mode
Generate the check-in desktop application using its feature shell library.

The project is generated exactly as described in the previous section.

Let's review the file and folder structure that is generated for us.

apps/check-in
├── check-in-mobile
│   ├── src
│   │   ├── app
│   │   │   ├── app.component.css
│   │   │   ├── app.component.html
│   │   │   ├── app.component.spec.ts
│   │   │   ├── app.component.ts
│   │   │   └── app.module.ts
│   │   ├── assets
│   │   │   └── .gitkeep
│   │   ├── favicon.ico
│   │   ├── index.html
│   │   ├── main.ts
│   │   ├── polyfills.ts
│   │   ├── styles.css
│   │   └── test.ts
│   ├── browserslist
│   ├── karma.conf.js
│   ├── tsconfig.app.json
│   ├── tsconfig.spec.json
│   └── tslint.json
└── check-in-mobile-e2e
    ├── src
    │   ├── app.e2e-spec.ts
    │   └── app.po.ts
    ├── protractor.conf.js
    └── tsconfig.json
Enter fullscreen mode Exit fullscreen mode
The file and folder structure generated for the mobile check-in application.

With both check-in applications in place, we have the following project folder structure.

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

Conclusion

Start the mobile check-in application by running the ng run check-in-mobile:serve command.

The mobile check-in application with the NgRx Store Devtools open.

At this point, our workspace folder structure looks like the following figure.

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
└── tools
Enter fullscreen mode Exit fullscreen mode
Workspace folder structure after Part 4.

In this part of the tutorial, we used our generate project tool to generate the check-in data access library. This time, we added the --with-state parameter to automate the generation of feature state that we generate manually in Part 2.

Next, we generated the check-in feature shell library project and hooked up shared data access as well as check-in data access by importing their Angular modules in the check-in feature shell Angular module.

With data access in place, we generated the check-in desktop application and end-to-end test projects. We went through the steps that the generate project tool does for us. Changes we did manually in Parts 1 and 2.

Finally, we generated the mobile check-in application and end-to-end test projects.

We'll finish the monorepo in Part 5 where we create the seatmap domain and the shared buttons UI library as well as the shared formatting utilities library. We'll wrap up the tutorial series by looking at what other features Nx offers us that Angular CLI doesn't provide out-of-the-box.

Resources

For the impatient programmer, the full solution is in the LayZeeDK/ngx-nrwl-airlines-workspace GitHub repository.

💖 💪 🙅 🚩
layzee
Lars Gyrup Brink Nielsen

Posted on March 31, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related