12

Recipes – Master/Detail, Data Tables, and NgRx

In this chapter, we complete the router-first architecture implementation on LemonMart by implementing the top two most used features in business applications: master/detail views and data tables. I demonstrate data tables with server-side pagination, highlighting the integration between the frontend and backend using LemonMart and LemonMart Server.

Make sure to have your lemon-mart-server up and running as you implement the recipes outlined in this chapter. Refer to Chapter 10, RESTful APIs and Full-Stack Implementation, for more information.

We leverage the concept of router orchestration to orchestrate how our components load data or render. We use resolve guards to reduce boilerplate code when loading data before navigating to a component. We use auxiliary routes to lay out components through the router configuration. We reuse the same component in multiple contexts.

We then dive into NgRx using the LocalCast Weather app and explore NgRx Data with LemonMart, so you can become familiar with more advanced application architecture concepts in Angular. By the end of this chapter, we will have touched upon most of the major functionality that Angular and Angular Material have to offer.

This chapter covers a lot of ground. It is organized in a recipe format, so you can quickly refer to a particular implementation when you are working on your projects. I cover the architecture, design, and major components of the implementation. I highlight important pieces of code to explain how the solution comes together. Leveraging what you've learned so far, I expect the reader to fill in routine implementation and configuration details. However, you can always refer to the GitHub repo if you get stuck.

In this chapter, you will learn about the following topics:

The most up-to-date versions of the sample code for the book are on GitHub at the repository linked in the following list. The repository contains the final and completed state of the code. You can verify your progress at the end of this chapter by looking for the end-of-chapter snapshot of code under the projects folder.

To get set up for this chapter's examples based on lemon-mart, do the following:

  1. Clone the repo at https://github.com/duluca/lemon-mart
  2. Execute npm install on the root folder to install dependencies
  3. The code sample for this chapter is under the following subfolder:
    projects/ch12
    
  4. To run the Angular app for this chapter, execute the following command:
    npx ng serve ch12
    
  5. To run Angular unit tests for this chapter, execute the following command:
    npx ng test ch12 --watch=false
    
  6. To run Angular e2e tests for this chapter, execute the following command:
    npx ng e2e ch12
    
  7. To build a production-ready Angular app for this chapter, execute the following command:
    npx ng build ch12 --prod
    

Note that the dist/ch12 folder at the root of the repository will contain the compiled result.

To prepare for this chapter's examples based on local-weather-app, implement these steps:

  1. Clone the repo at https://github.com/duluca/local-weather-app
  2. Execute npm install on the root folder to install dependencies
  3. The code sample for this chapter is under the following subfolder:
    projects/ch12
    
  4. To run the Angular app for this chapter, execute the following command:
    npx ng serve ch12
    
  5. To run Angular unit tests for this chapter, execute the following command:
    npx ng test ch12 --watch=false
    
  6. To run Angular e2e tests for this chapter, execute the following command:
    npx ng e2e ch12
    
  7. To build a production-ready Angular app for this chapter, execute the following command:
    npx ng build ch12 --prod
    

Remember that the dist/ch12 folder at the root of the repository will contain the compiled result.

Beware that the source code in the book or on GitHub may not always match the code generated by Angular CLI. There may also be slight differences in implementation between the code in the book and what's on GitHub because the ecosystem is ever-evolving. It is natural for the sample code to change over time. Also on GitHub, expect to find corrections, fixes to support newer versions of libraries, or side-by-side implementations of multiple techniques for the reader to observe. The reader is only expected to implement the ideal solution recommended in the book. If you find errors or have questions, please create an issue or submit a pull request on GitHub for the benefit of all readers.

You can read more about updating Angular in the Appendix C, Keeping Angular and Tools Evergreen. You can find this appendix online from https://static.packt-cdn.com/downloads/9781838648800_Appendix_C_Keeping_Angular_and_Tools_Evergreen.pdf or at https://expertlysimple.io/stay-evergreen.

In the next section, we will learn about resolve guards so that we can simplify our code and reduce the amount of boilerplate.

Editing existing users

In Chapter 11, Recipes – Reusability, Routing, and Caching, we created a ViewUserComponent with an editUser function. We need this functionality later in the chapter when implementing a master/detail view in the system, where a manager can see all users in the system and have the ability to edit them. Before we can enable the editUser functionality, we need to make sure that the ViewUserComponent component alongside the ProfileComponent can load any user given their ID.

Let's start by implementing a resolve guard we can use for both components.

Loading data with resolve guard

A resolve guard is a type of router guard, as mentioned in Chapter 8, Designing Authentication and Authorization. A resolve guard can load necessary data for a component by reading record IDs from route parameters, asynchronously load the data, and have it ready by the time the component activates and initializes.

The major advantages of a resolve guard include reusability of the loading logic, a reduction of boilerplate code, and the shedding of dependencies because the component can receive the data it needs without having to import any service:

  1. Create a new user.resolve.ts class under user/user:
    src/app/user/user/user.resolve.ts
    import { Injectable } from '@angular/core'
    import { ActivatedRouteSnapshot, Resolve } from '@angular/router'
    import { catchError, map } from 'rxjs/operators'
    import { transformError } from '../../common/common'
    import { IUser, User } from './user'
    import { UserService } from './user.service'
    @Injectable()
    export class UserResolve implements Resolve<IUser> { constructor(private userService: UserService) {}
      resolve(route: ActivatedRouteSnapshot) {
        return this.userService
          .getUser(route.paramMap.get('userId'))
          .pipe(map(User.Build), catchError(transformError))
      }
    }
    

    Note that similar to the updateUser method in UserService, we use map(User.Build) to hydrate the user object, so it is ready to be used when a component loads data from the route snapshot, as we'll see next.

  2. Provide the resolver in user.module.ts.

    Next, let's configure the router and ProfileComponent to be able to load an existing user.

  3. Modify user-routing.module.ts to add a new path, profile/:userId, with a route resolver and the canActivate AuthGuard:
    src/app/user/user-routing.module.ts
    ...
    {
        path: 'profile/:userId',
        component: ProfileComponent,
        resolve: {
          user: UserResolve,
        },
        canActivate: [AuthGuard],
      },
      ...
    

    Remember to provide UserResolve and AuthGuard in user.module.ts.

  4. Update the profile component to load the data from the route if it exists:
    src/app/user/profile/profile.component.ts
    ...
      constructor(
        ...
        private route: ActivatedRoute
      ) {
        super()
      }
      ngOnInit() {
        this.formGroup = this.buildForm()
        if (this.route.snapshot.data.user) {
          this.patchUser(this.route.snapshot.data.user)
        } else {
          this.subs.sink = combineLatest(
            [this.loadFromCache(), 
             this.authService.currentUser$]
           )
          .pipe(
            filter(
              ([cachedUser, me]) => 
                cachedUser != null || me != null
            ),
            tap(
              ([cachedUser, me]) => 
               this.patchUser(cachedUser || me)
            )
          )
          .subscribe()
        }
      }
    

We first check to see whether a user is present in the route snapshot. If so, we call patchUser to load this user. Otherwise, we fall back to our conditional cache-loading logic.

Note that the patchUser method also sets the currentUserId and nameInitialDate$ observables, as well as calling the patchUpdateData base to update the form data.

You can verify that the resolver is working by navigating to the profile with your user ID. Using the out-of-the-box settings, this URL will look something like http://localhost:5000/user/profile/5da01751da27cc462d265913.

Reusing components with binding and route data

Now, let's refactor the viewUser component so that we can reuse it in multiple contexts. User information is displayed in two places in the app as per the mock-ups that were created.

The first place is the Review step of the user profile that we implemented in the previous chapter. The second place is on the user management screen on the /manager/users route, as follows:

Figure 12.1: Manager user management mock-up

To maximize code reuse, we need to ensure that our shared ViewUser component can be used in both contexts.

For the Review step of the multi-step input form, we simply bind the current user to it. In the second use case, the component will need to load its own data using a resolve guard, so we don't need to implement additional logic to achieve our goal:

  1. Update the viewUser component to inject the ActivatedRoute object and set currentUser$ from the route in ngOnInit():
    src/app/user/view-user/view-user.component.ts
    ...
    import { ActivatedRoute } from '@angular/router'
    export class ViewUserComponent implements OnChanges, OnInit {
      ...
      constructor(
        private route: ActivatedRoute, private router: Router
        ) {} 
      ngOnInit() {
        if (this.route.snapshot.data.user) { 
          this.currentUser$.next(this.route.snapshot.data.user)
        }
      }
      ...
    }
    

    ngOnInit will only fire once when the component is first initialized or has been routed to. In this case, if any data for the route has been resolved, then it'll be pushed to this.currentUser$ with the next() function.

    We now have two independent events to update data; one for ngOnChanges, which handles updates to the @Input value and pushes to it to BehaviorSubject currentUser$ if this.user has been bound to.

    To be able to use this component across multiple lazy loaded modules, we must wrap it in its own module:

  2. Create a new shared-components.module.ts under src/app:
    src/app/shared-components.module.ts
    import { CommonModule } from '@angular/common'
    import { NgModule } from '@angular/core'
    import { FlexLayoutModule } from '@angular/flex-layout'
    import { ReactiveFormsModule } from '@angular/forms'
    import { AppMaterialModule } from './app-material.module'
    import { 
      ViewUserComponent 
    } from './user/view-user/view-user.component'
    @NgModule({
      imports: [
        CommonModule,
        ReactiveFormsModule,
        FlexLayoutModule,
        AppMaterialModule,
      ],
      declarations: [ViewUserComponent],
      exports: [ViewUserComponent],
    })
    export class SharedComponentsModule {}
    

    Ensure that you import the SharedComponentsModule module into each feature module you intended to use ViewUserComponent in. In our case, these will be UserModule and ManagerModule.

  3. Remove ViewUserComponent from the User module declarations
  4. Similarly declare and export NameInputComponent in SharedComponentsModule, and then clean up its other declarations
  5. Import the modules necessary to support ViewUserComponent and NameInputComponent in SharedComponentsModule as well, such as FieldErrorModule

We now have the key pieces in place to begin implementation of the master/detail view. Let's go over this next.

Master/detail view auxiliary routes

The true power of router-first architecture comes to fruition with the use of auxiliary routes, where we can influence the layout of components solely through router configuration, allowing for rich scenarios where we can remix the existing components into different layouts. Auxiliary routes are routes that are independent of each other where they can render content in named outlets that have been defined in the markup, such as <router-outlet name="master"> or <router-outlet name="detail">. Furthermore, auxiliary routes can have their own parameters, browser history, children, and nested auxiliaries.

In the following example, we will implement a basic master/detail view using auxiliary routes:

  1. Implement a simple component with two named outlets defined:
    src/app/manager/user-management/user-management.component.ts
      template: `
        <div class="horizontal-padding">
          <router-outlet name="master"></router-outlet>
          <div style="min-height: 10px"></div>
          <router-outlet name="detail"></router-outlet>
        </div>
      `
    
  2. Add a new userTable component under manager
  3. Update manager-routing.module.ts to define the auxiliary routes:
    src/app/manager/manager-routing.module.ts
      ...
        {
          path: 'users',
          component: UserManagementComponent,
          children: [
            { 
              path: '', component: UserTableComponent, 
               outlet: 'master' 
            },
            {
              path: 'user',
              component: ViewUserComponent,
              outlet: 'detail',
              resolve: {
                user: UserResolve,
              },
            },
          ],
          canActivate: [AuthGuard],
          canActivateChild: [AuthGuard],
          data: {
            expectedRole: Role.Manager,
          },
        },
    ...
    

    This means that when a user navigates to /manager/users, they'll see the UserTableComponent, because it is implemented with the default path.

  4. Provide UserResolve in manager.module.ts since viewUser depends on it
  5. Implement a temporary button in userTable:
    src/app/manager/user-table/user-table.component.html
    <a mat-button mat-icon-button [routerLink]="['/manager/users', 
        { outlets: { detail: ['user', { userId: row._id}] } }]"
        skipLocationChange>
      <mat-icon>visibility</mat-icon>
    </a>
    

    The skipLocationChange directive navigates without pushing a new record into history. So if the user views multiple records and hits the Back button, they will be taken back to the previous screen, instead of having to scroll through the records they viewed first.

    Imagine that a user clicks on a View detail button like the one defined previously – then, ViewUserComponent will be rendered for the user with the given userId. In the next screenshot, you can see what the View Details button will look like after we implement the data table in the next section:

    Figure 12.2: View Details button

    You can have as many combinations and alternative components defined for the master and detail, allowing for the infinite possibilities of dynamic layouts. However, setting up the routerLink can be a frustrating experience. Depending on the exact condition, you have to either supply or not supply all or some outlets in the link. For example, for the preceding scenario, if the link was ['/manager/users', { outlets: { master: [''], detail: ['user', {userId: row.id}] } }], the route will silently fail to load. Expect these quirks to be ironed out in future Angular releases.

    Now that we've completed the implementation of the resolve guard for ViewUserComponent, you can use Chrome DevTools to see the data being loaded correctly.

    Before debugging, ensure that the lemon-mart-server we created in Chapter 10, RESTful APIs and Full-Stack Implementation, is running.

  6. In Chrome DevTools, set a break point right after this.currentUser is assigned, as shown:

    Figure 12.3: Dev Tools debugging ViewUserComponent

You will observe that this.currentUser is correctly set without any boilerplate code for loading data inside the ngOnInit function, showing the true benefit of a resolve guard. ViewUserComponent is the detail view; now let's implement the master view as a data table with pagination.

Data table with pagination

We have created the scaffolding to lay out our master/detail view. In the master outlet, we will have a paginated data table of users, so let's implement UserTableComponent, which will contain a MatTableDataSource property named dataSource. We will need to be able to fetch user data in bulk using standard pagination controls such as pageSize and pagesToSkip and be able to further narrow down the selection with user-provided searchText.

Let's start by adding the necessary functionality to the UserService:

  1. Implement a new IUsers interface to describe the data structure of the paginated data:
    src/app/user/user/user.service.ts
    ...
    export interface IUsers {
      data: IUser[]
      total: number
    }
    
  2. Update the interface for UserService with a getUsers function:
    src/app/user/user/user.service.ts
    ...
    export interface IUserService {
      getUser(id: string): Observable<IUser>
      updateUser(id: string, user: IUser): Observable<IUser>
      getUsers(pageSize: number, searchText: string, 
        pagesToSkip: number): Observable<IUsers>
    }
    export class UserService extends CacheService implements IUserService {
    ...
    
  3. Add getUsers to UserService:
    src/app/user/user/user.service.ts
    ...
    getUsers(
        pageSize: number,
        searchText = '',
        pagesToSkip = 0,
        sortColumn = '',
        sortDirection: '' | 'asc' | 'desc' = 'asc'
      ): Observable<IUsers> {
        const recordsToSkip = pageSize * pagesToSkip
        if (sortColumn) {
          sortColumn =
            sortDirection === 'desc' ? `-${sortColumn}` : sortColumn
        }
        return this.httpClient.get<IUsers>(
          `${environment.baseUrl}/v2/users`, { 
            params: {
              filter: searchText,
              skip: recordsToSkip.toString(),
              limit: pageSize.toString(),
              sortKey: sortColumn,
            },
          })
        }
    ...
    

    Note that the sort direction is represented by the keywords asc for ascending and desc for descending. When we want to sort a column in ascending order, we pass the column name as a parameter to the server. To sort a column in descending order, we prepend the column name with a minus sign.

  4. Set up UserTable with pagination, sorting, and filtering:
    src/app/manager/user-table/user-table.component.ts
    ...
    @Component({
      selector: 'app-user-table',
      templateUrl: './user-table.component.html',
      styleUrls: ['./user-table.component.css'],
    })
    export class UserTableComponent implements OnDestroy, AfterViewInit {
      displayedColumns = ['name', 'email', 'role', '_id']
      items$: Observable<IUser[]>
      resultsLength = 0
      hasError = false
      errorText = ''
      private skipLoading = false
      private subs = new SubSink()
      readonly isLoadingResults$ = new BehaviorSubject(true)
      loading$: Observable<boolean>
      refresh$ = new Subject()
      search = new FormControl('', OptionalTextValidation)
      @ViewChild(MatPaginator, { static: false })
        paginator: MatPaginator 
      @ViewChild(MatSort, { static: false }) sort: MatSort
      constructor(
        private userService: UserService
      ) {
        this.loading$ = this.isLoadingResults$
      }
      getUsers(
        pageSize: number,
        searchText: string,
        pagesToSkip: number,
        sortColumn: string,
        sortDirection: SortDirection
      ): Observable<IUsers> {
        return this.userService.getUsers(
          pageSize,
          searchText,
          pagesToSkip,
          sortColumn,
          sortDirection
        )
      }
      ngOnDestroy(): void {
        this.subs.unsubscribe()
      }
      ngAfterViewInit() {
        this.subs.sink = this.sort.sortChange
          .subscribe(() => this.paginator.firstPage()) 
        if (this.skipLoading) {
          return
        }
        this.items$ = merge(
          this.refresh$,
          this.sort.sortChange,
          this.paginator.page,
          this.search.valueChanges.pipe(debounceTime(1000))
        ).pipe(
          startWith({}),
          switchMap(() => {
            this.isLoadingResults$.next(true)
            return this.getUsers(
              this.paginator.pageSize,
              this.search.value,
              this.paginator.pageIndex,
              this.sort.active,
              this.sort.direction
            )
          }),
          map((results: { total: number; data: IUser[] }) => {
            this.isLoadingResults$.next(false)
            this.hasError = false
            this.resultsLength = results.total
            return results.data
          }),
          catchError((err) => {
            this.isLoadingResults$.next(false)
            this.hasError = true
            this.errorText = err
            return of([])
          })
        )
        this.items$.subscribe()
      }
    }
    

    In ngAfterViewInit, we use the merge method, as highlighted in the preceding snippet, to listen for changes in pagination, sorting, and filter properties. If one property changes, the whole pipeline is triggered. This is similar to how we implemented the login routine in AuthService. The pipeline contains a call to this.userService.getUsers, which will retrieve users based on the pagination, sorting, and filter preferences passed in. Results are then piped into the this.items$ observable, which the data table subscribes to with an async pipe, so it can display the data.

  5. Create a ManagerMaterialModule containing the following Material modules:
    src/app/manager/manager-material.module.ts
        MatTableModule,
        MatSortModule,
        MatPaginatorModule,
        MatProgressSpinnerModule,
        MatSlideToggleModule,
    
  6. Ensure that manager.module.ts correctly imports the following:
    • The new ManageMaterialModule
    • The baseline AppMaterialModule
    • The following required modules: FormsModule, ReactiveFormsModule, and FlexLayoutModule
  7. Implement the CSS for userTable:
    src/app/manager/user-table/user-table.component.css
    .loading-shade {
      position: absolute;
      top: 0;
      left: 0;
      bottom: 56px;
      right: 0;
      background: rgba(0, 0, 0, 0.15);
      z-index: 1;
      display: flex;
      align-items: center;
      justify-content: center;
    }
    .filter-row {
      min-height: 64px;
      padding: 8px 24px 0;
    }
    .full-width {
      width: 100%;
    }
    .mat-paginator {
      background: transparent;
    }
    
  8. Finally, implement the userTable template:
    src/app/manager/user-table/user-table.component.html
    <div class="filter-row">
      <form style="margin-bottom: 32px">
        <div fxLayout="row">
          <mat-form-field class="full-width">
            <mat-icon matPrefix>search</mat-icon>
            <input matInput placeholder="Search" aria-label="Search" [formControl]="search" />
            <mat-hint>Search by e-mail or name</mat-hint>
            <mat-error *ngIf="search.invalid">
              Type more than one character to search
            </mat-error>
          </mat-form-field>
        </div>
      </form>
    </div>
    <div class="mat-elevation-z8">
      <div class="loading-shade" *ngIf="loading$ | async as loading">
        <mat-spinner *ngIf="loading"></mat-spinner>
        <div class="error" *ngIf="hasError">
          {{ errorText }}
        </div>
      </div>
      <table mat-table class="full-width" [dataSource]="items$ | async" matSort
        matSortActive="name" matSortDirection="asc" matSortDisableClear>
        <ng-container matColumnDef="name">
          <th mat-header-cell *matHeaderCellDef mat-sort-header> Name </th>
          <td mat-cell *matCellDef="let row">
            {{ row.fullName }}
          </td>
        </ng-container>
        <ng-container matColumnDef="email">
          <th mat-header-cell *matHeaderCellDef mat-sort-header> E-mail </th>
          <td mat-cell *matCellDef="let row"> {{ row.email }} </td>
        </ng-container>
        <ng-container matColumnDef="role">
          <th mat-header-cell *matHeaderCellDef mat-sort-header> Role </th>
          <td mat-cell *matCellDef="let row"> {{ row.role }} </td>
        </ng-container>
        <ng-container matColumnDef="_id">
          <th mat-header-cell *matHeaderCellDef>View Details
          </th>
          <td mat-cell *matCellDef="let row" style="margin-right: 8px">
            <a mat-button mat-icon-button [routerLink]="[
                '/manager/users',
                { outlets: { detail: ['user', { userId: row._id }] } }
              ]" skipLocationChange>
              <mat-icon>visibility</mat-icon>
            </a>
          </td>
        </ng-container>
        <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
        <tr mat-row *matRowDef="let row; columns: displayedColumns"> </tr>
      </table>
      <mat-toolbar>
        <mat-toolbar-row>
          <button mat-icon-button (click)="refresh$.next()">
            <mat-icon title="Refresh">refresh</mat-icon>
          </button>
          <span class="flex-spacer"></span>
          <mat-paginator [pageSizeOptions]="[5, 10, 25, 100]"
                         [length]="resultsLength">
          </mat-paginator>
        </mat-toolbar-row>
      </mat-toolbar>
    </div>
    

    With just the master view in place, the table is as shown in the following screenshot (make sure you've updated to the latest version of Angular!):

    Figure 12.4: User table

    If you click on the View icon, ViewUserComponent will get rendered in the detail outlet, as shown:

    Figure 12.5: Master/detail view

    In the previous chapter, we implemented the Edit button, passing the userId to the UserProfile so that the data can be edited and updated.

  9. Click on the Edit button to be taken to the ProfileComponent, edit the user record, and verify that you can update another user's record
  10. Confirm that you can view the updated user record in the data table

This demonstration of data tables with pagination completes the major functionality of LemonMart for the purpose of this book. Now, before we move on, let's make sure that all of our tests pass.

Updating unit tests

Let's go over some unit tests for ProfileComponent and UserTableComponent to see how we can leverage different techniques to test the components:

  1. Observe the unit test file for ProfileComponent and identify the use of the authServiceMock object to provide initial data for the component:
    src/app/user/profile/profile.component.spec.ts
    describe('ProfileComponent', () => {
      let component: ProfileComponent
      let fixture: ComponentFixture<ProfileComponent>
      let authServiceMock: jasmine.SpyObj<AuthService>
      beforeEach(async(() => {
        const authServiceSpy = autoSpyObj(
          AuthService,
          ['currentUser$', 'authStatus$'],
          ObservablePropertyStrategy.BehaviorSubject
        )
        TestBed.configureTestingModule({
          providers: commonTestingProviders.concat({
            provide: AuthService,
            useValue: authServiceSpy,
          }),
          imports: commonTestingModules.concat([
            UserMaterialModule,
            FieldErrorModule,
            LemonRaterModule,
          ]),
          declarations: [ProfileComponent, NameInputComponent, ViewUserComponent],
        }).compileComponents()
        authServiceMock = injectSpy(AuthService)
        fixture = TestBed.createComponent(ProfileComponent)
        component = fixture.debugElement.componentInstance
      }))
      it('should create', () => {
        authServiceMock.currentUser$.next(new User())
        authServiceMock.authStatus$.next(defaultAuthStatus)
        fixture.detectChanges()
        expect(component).toBeTruthy()
      })
    })
    

    Note that instead of using the createComponentMock function from angular-unit-test-helper to import NameInputComponent or ViewUserComponent, I import their actual implementations. This is because createComponentMock is not yet sophisticated enough to deal with binding data to child components. In the Further reading section, I've included a blog post by Aiko Klostermann that covers testing Angular components with @Input() properties.

  2. Open the spec file for UserTableComponent:

    After fixing up its providers and imports, you will notice that UserTableComponent is throwing an ExpressionChangedAfterItHasBeenCheckedError error. This is because the component initialization logic requires dataSource to be defined. If undefined, the component can't be created. However, we can easily modify component properties in the second beforeEach method, which executes after TestBed has injected real, mocked, or fake dependencies into the component class. See the highlighted changes in the following snippet for the test data setup:

    src/app/manager/user-table/user-table.component.spec.ts
    ...
    beforeEach(() => {
        fixture = TestBed.createComponent(UserTableComponent)
        component = fixture.componentInstance
        component.items$ = of([new User()])
        Object.assign(component, { skipLoading: true })
        fixture.detectChanges()
    })
    ...
    

    By now, you may have noticed that just by updating some of our central configuration files, such as commonTestingProviders and commonTestingModules, some tests are passing, and the rest of the tests can be resolved by applying the various patterns we have been using throughout the book. For example, user-management.component.spec.ts uses the common testing modules and providers we have created:

    src/app/manager/user-management/user-management.component.spec.ts
    providers: commonTestingProviders,
    imports: commonTestingModules.concat([ManagerMaterialModule]),
    

    When you are mocking providers, keep in mind what module, component, service, or class is under test and take care only to mock dependencies.

    ViewUserComponent is a special case where we can't use our common testing modules and providers, otherwise, we would end up creating a circular dependency. In this case, manually specify the modules that need to be imported.

  3. Fix the unit test configurations so all of them are passing and no warnings are generated.

With the heavy lifting of the implementation completed, we can now explore alternative architectures, tools, and libraries to better understand the best ways to architect Angular apps for various needs. Next, let's explore NgRx.

NgRx Store and Effects

As covered in Chapter 1, Introduction to Angular and Its Concepts, the NgRx library brings reactive state management to Angular based on RxJS. State management with NgRx allows developers to write atomic, self-contained, and composable pieces of code, creating actions, reducers, and selectors. This kind of reactive programming allows side effects in state changes to be isolated. In essence, NgRx is an abstraction layer over RxJS to fit the Flux pattern.

There are four major elements of NgRx:

Let's revisit the following Flux pattern diagram, which now highlights an Effect:

Figure 12.6: Flux pattern diagram

Let's demonstrate how NgRx works by going over a concrete example. In order to keep it simple, we will be leveraging the LocalCast Weather app.

Implementing NgRx for LocalCast Weather

We will be implementing NgRx to execute the search functionality in the LocalCast Weather app. Consider the following architecture diagram:

Figure 12.7: LocalCast Weather architecture

In order to achieve our implementation, we will use both the NgRx store and effects libraries. NgRx store actions are reflected in the diagram in light gray with a WeatherLoaded reducer and the app state. At the top, actions are represented as a stream of various data objects either dispatching actions or acting on dispatched actions, enabling us to implement the Flux pattern. The NgRx effects library extends the Flux pattern by isolating side effects in its own model without littering the store with temporary data.

The effects workflow, represented in dark gray, begins with Step 1:

  1. CitySearchComponent dispatches the search action
  2. The search action appears on the observable @ngrx/action stream (or data stream)
  3. CurrentWeatherEffects acts on the search action to perform a search
  4. WeatherService performs the search to retrieve current weather information from the OpenWeather API

Store actions, represented in light gray, begin with Step A:

  1. CurrentWeatherEffects dispatches the weatherLoaded action
  2. The weatherLoaded action appears on the data stream
  3. The weatherLoaded reducer acts on the weatherLoaded action
  4. The weatherLoaded reducer transforms the weather information to be stored as a new state
  5. The new state is a persisted search state, part of the appStore state

Note that there's a parent-level appStore state, which contains a child search state. I intentionally retained this setup to demonstrate how the parent-level state scales as you add different kinds of data elements to the store.

Finally, a view reads from the store, beginning with step a:

  1. The CurrentWeather component subscribes to the selectCurrentWeather selector using the async pipe
  2. The selectCurrentWeather selector listens for changes to the store.search.current property in the appStore state
  3. The appStore state retrieves the persisted data

Using NgRx, when a user searches for a city, the actions to retrieve, persist, and display that information on the CurrentWeatherComponent happens automatically via individual composable and immutable elements.

Comparing BehaviorSubject and NgRx

We will be implementing NgRx side by side with BehaviorSubjects, so you can see the differences in the implementation of the same feature. To do this, we will need a slide toggle to switch between the two strategies:

This section uses the local-weather-app repo. You can find the code samples for this chapter under the projects/ch12 folder.

  1. Start by implementing a <mat-slide-toggle> element on CitySearchComponent, as shown in the following screenshot:

    Figure 12.8: LocalCast Weather slide toggle

    Ensure that the field is backed by a property on your component named useNgRx.

  2. Refactor the doSearch method to extract the BehaviorSubject code as its own function named behaviorSubjectBasedSearch
  3. Stub out a function called ngRxBasedSearch:
    src/app/city-search/city-search.component.ts
    doSearch(searchValue: string) {
      const userInput = searchValue.split(',').map((s) => s.trim())
      const searchText = userInput[0]
      const country = userInput.length > 1 ? userInput[1] : undefined
      if (this.useNgRx) {
        this.ngRxBasedSearch(searchText, country)
      } else {
        this.behaviorSubjectBasedSearch(searchText, country)
      }
    }
    

We will be dispatching an action from the ngRxBasedSearch function that you just created.

Setting up NgRx

You may add the NgRx Store package with the following command:

$ npx ng add @ngrx/store

This will create a reducers folder with an index.ts file in it. Now add the NgRx effects package:

$ npx ng add @ngrx/effects --minimal

We use the --minimal option here to avoid creating unnecessary boilerplate.

Next, install the NgRx schematics library so you can take advantage of generators to create the boilerplate code for you:

$ npm i -D @ngrx/schematics

Implementing NgRx can be confusing due to its highly decoupled nature, which may necessitate some insight into the inner workings of the library.

The sample project under projects/ch12 configures @ngrx/store-devtools for debugging.

If you would like to be able to console.log NgRx actions for debugging or instrumentation during runtime, refer to the Appendix A, Debugging Angular.

Defining NgRx actions

Before we can implement effects or reducers, we first need to define the actions our app is going to be able to execute. For LocalCast Weather, there are two types of actions:

Create an action named search by running the following command:

$ npx ng generate @ngrx/schematics:action search --group --creators

Take the default options when prompted.

The --group option groups actions under a folder named action. The --creators option uses creator functions to implement actions and reducers, which is a more familiar and straightforward way to implement these components.

Now, let's implement the two actions using the createAction function, providing a name and an expected list of input parameters:

src/app/action/search.actions.ts
import { createAction, props, union } from '@ngrx/store'
import { ICurrentWeather } from '../interfaces'
export const SearchActions = {
  search: createAction(
    '[Search] Search',
    props<{ searchText: string; country?: string }>()
  ),
  weatherLoaded: createAction( 
    '[Search] CurrentWeather loaded',
    props<{ current: ICurrentWeather }>()
  ),
}
const all = union(SearchActions)
export type SearchActions = typeof all

The search action has the name '[Search] Search' and has searchText and an optional country parameter as inputs. The weatherLoaded action follows a similar pattern. At the end of the file, we create a union type of our actions, so we can group them under one parent type to use in the rest of the application.

Notice that action names are prepended by [Search]. This is a convention that helps developers visually group related actions together during debugging.

Now that our actions are defined, we can implement the effect to handle the search action and dispatch a weatherLoaded action.

Implementing NgRx Effects

As mentioned earlier, effects let us change the stored state without necessarily storing the event data that is causing the change. For example, we want our state to only have weather data, not the search text itself. Effects allow us to do this in one step, rather than forcing us to use an intermediate store for the searchText and a far more complicated chain of events to just turn that into weather data.

Otherwise, we would have to implement a reducer in between, to first store this value in the store, and then later retrieve it from a service and dispatch a weatherLoaded action. The effect will make it simpler to retrieve data from our service.

Now let's add CurrentWeatherEffects to our app:

$ npx ng generate @ngrx/schematics:effect currentWeather --module=app.module.ts --root --group --creators

Take the default options when prompted.

You will have a new current-weather.effects.ts file under the effects folder.

Once again, --group is used to group effects under a folder of the same name. --root registers the effect in app.module.ts and we use creator functions with the --creators option.

In the CurrentWeatherEffects file, start by implementing a private doSearch method:

src/app/effects/current-weather.effects.ts
private doSearch(action: { searchText: string; country?: string }) {
  return this.weatherService.getCurrentWeather(
    action.searchText,
    action.country
  ).pipe(
    map((weather) =>
      SearchActions.weatherLoaded({ current: weather })
    ),
    catchError(() => EMPTY)
  )
}

Note that we're choosing to ignore errors thrown with the EMPTY function. You can surface these errors to the user with a UiService like the one you've implemented for LemonMart.

This function takes an action with search parameters, calls getCurrentWeather, and upon receiving a response, dispatches the weatherLoaded action, passing in the current weather property.

Now let's create the effect itself, so we can trigger the doSearch function:

src/app/effects/current-weather.effects.ts
getCurrentWeather$ = createEffect(() =>
  this.actions$.pipe(
    ofType(SearchActions.search), 
    exhaustMap((action) => this.doSearch(action))
  )
)

This is where we tap into the observable action stream, this.actions$, and listen to actions of the SearchAction.search type. We then use the exhaustMap operator to register for the emitted event. Due to its unique nature, exhaustMap won't allow another search action to be processed until the doSearch function completes dispatching its weatherLoaded action.

Confused by all the different kinds of RxJS operators and worried you'll never remember them? See the Appendix B, Angular Cheat Sheet, for a quick reference.

Implementing reducers

With the weatherLoaded action triggered, we need a way to ingest the current weather information and store it in our appStore state. Reducers will help us handle specific actions, creating an isolated and immutable pipeline to store our data in a predictable way.

Let's create a search reducer:

$ npx ng generate @ngrx/schematics:reducer search 
    --reducers=reducers/index.ts --group --creators

Take the default options. Here, we use --group to keep files organized under the reducers folder and --creators to leverage the creator style of creating NgRx components. We also specify the location of our parent appStore state at reducers/index.ts with --reducers, so our new reducer can be registered with it.

You may observe that reducers.index.ts has been updated to register the new search.reducer.ts. Let's implement it step by step.

In the search state, we will be storing the current weather, so implement the interface to reflect this:

src/app/reducers/search.reducer.ts
export interface State {
  current: ICurrentWeather
}

Now let's specify the initialState. This is similar to how we need to define a default value of a BehaviorSubject. Refactor the WeatherService to export a const defaultWeather: ICurrentWeather object that you can use to initialize BehaviorSubject and initialState.

src/app/reducers/search.reducer.ts
export const initialState: 
  State = { current:
  defaultWeather,
}

Finally, implement searchReducer to handle the weatherLoaded action using the on operator:

src/app/reducers/search.reducer.ts
const searchReducer = createReducer(
  initialState,
  on(SearchActions.weatherLoaded, (state, action) => {
    return {
      ...state,
    current: action.current,
    }
  })
)

We simply register for the weatherLoaded action and unwrap the data stored in it and pass it into the search state.

This is, of course, a very simplistic case. However, it is easy to imagine a more complicated scenario, where we may need to flatten or process a piece of data received and store it in an easy-to-consume manner. Isolating such logic in an immutable way is the key value proposition of utilizing a library like NgRx.

Registering with Store using selector

We need CurrentWeatherComponent to register with the appStore state for updated current weather data.

Start by dependency injecting the appStore state and registering the selector to pluck current weather from the State object:

src/app/current-weather/current-weather.component.ts
import * as appStore from '../reducers'
export class CurrentWeatherComponent {
  current$: Observable<ICurrentWeather>
  constructor(private store: Store<appStore.State>) {
    this.current$ =
      this.store.pipe(select((state: State) => state.search.current))
  } 
  ...
}

We simply listen to state change events that flow through the store. Using the select function, we can implement an inline select to get the piece of data we need.

We can refactor this a bit and make our selector reusable by using a createSelector to create a selectCurrentWeather property on reducers/index.ts:

src/app/reducers/index.ts
export const selectCurrentWeather = createSelector(
  (state: State) => state.search.current,
  current => current
)

In addition, since we want to maintain the continued operation of the BehaviorSubject, we can implement a merge operator in CurrentWeatherComponent to listen to both WeatherService updates and appStore state updates:

src/app/current-weather/current-weather.component.ts
import * as appStore from '../reducers'
  constructor(
    private weatherService: WeatherService,
    private store: Store<appStore.State>
  ) {
    this.current$ = merge(
      this.store.pipe(select(appStore.selectCurrentWeather)),
      this.weatherService.currentWeather$
    )
  }

Now that we are able to listen to store updates, let's implement the final piece of the puzzle: dispatching the search action.

Dispatching store actions

We need to dispatch the search action so that our search effect can fetch current weather data and update the store. Earlier in this chapter, you implemented a stubbed function called ngRxBasedSearch in the CitySearchComponent.

Let's implement ngRxBasedSearch:

src/app/city-search/city-search.component.ts
ngRxBasedSearch(searchText: string, country?: string) {
  this.store.dispatch(SearchActions.search({ searchText, country }))
}

Don't forget to inject the appState store into the component!

And that's it! Now you should be able to run your code and test to see whether it all works.

As you can see, NgRx brings a lot of sophisticated techniques to the table to create ways to make data transformations immutable, well defined, and predictable. However, this comes with considerable implementation overhead. Use your best judgment to determine whether you really need the Flux pattern in your Angular app. Often, the frontend application code can be made much simpler by implementing RESTful APIs that return flat data objects, with complicated data manipulations handled server side.

Unit testing reducers and selectors

You can implement unit tests for the weatherLoaded reducer and the selectCurrentWeather selector in search.reducer.spec.ts:

src/app/reducers/search.reducer.spec.ts
import { SearchActions } from '../actions/search.actions'
import { defaultWeather } from '../weather/weather.service'
import { fakeWeather } from '../weather/weather.service.fake'
import { selectCurrentWeather } from './index'
import { initialState, reducer } from './search.reducer'
describe('Search Reducer', () => {
  describe('weatherLoaded', () => {
    it('should return current weather', () => {
      const action = SearchActions.weatherLoaded({ current: fakeWeather })
      const result = reducer(initialState, action)
      expect(result).toEqual({ current: fakeWeather })
    })
  })
})
describe('Search Selectors', () => { 
  it('should selectCurrentWeather', () => {
    const expectedWeather = defaultWeather
    expect(selectCurrentWeather({ search: { current: defaultWeather }
})).toEqual(
      expectedWeather
    )
  })
})

These unit tests are fairly straightforward and will ensure that no unintentional changes to the data structure can happen within the store.

Unit testing components with MockStore

You need to update the tests for CurrentWeatherComponent so that we can inject a mock Store into the component to test the value of the current$ property.

Let's look at the delta of what needs to be added to the spec file to configure the mock store:

src/app/current-weather/current-weather.component.spec.ts
import { MockStore, provideMockStore } from '@ngrx/store/testing'
describe('CurrentWeatherComponent', () => {
  ...
  let store: MockStore<{ search: { current: ICurrentWeather } }>
  const initialState = { search: { current: defaultWeather } }
  beforeEach(async(() => {
    ...
    TestBed.configureTestingModule({
      imports: [AppMaterialModule],
      providers: [
        ...
        provideMockStore({ initialState }),
      ],
    }).compileComponents()
    ...
    store = TestBed.inject(Store) as any
  }))
...
})

We can now update the 'should get currentWeather from weatherService' test to see whether CurrentWeatherComponent works with a mock store:

src/app/current-weather/current-weather.component.spec.ts
it('should get currentWeather from weatherService', (done) => {
  // Arrange
  store.setState({ search: { current: fakeWeather } })
  weatherServiceMock.currentWeather$.next(fakeWeather)
  // Act
  fixture.detectChanges() // triggers ngOnInit()
  // Assert
  expect(component.current$).toBeDefined()
  component.current$.subscribe(current => { 
    expect(current.city).toEqual('Bethesda')
    expect(current.temperature).toEqual(280.32)
    // Assert on DOM
    const debugEl = fixture.debugElement
    const titleEl: HTMLElement =
      debugEl.query(By.css('.mat-title')).nativeElement
    expect(titleEl.textContent).toContain('Bethesda')
    done()
  })
})

The mock store allows us to set the current state of the store, which in turn allows the selector call in the constructor to fire and grab the provided fake weather data.

TestBed is not a hard requirement for writing unit tests in Angular, a topic covered well at https://angular.io/guide/testing. My colleague and reviewer of this book, Brendon Caulkins, contributed a bed-less spec file for this chapter, named current-weather.component.nobed.spec.ts. He cites significant performance increases when running the tests, with fewer imports and less maintenance, but a higher level of care and expertise required to implement the tests. If you're on a large project, you should seriously consider skipping the TestBed.

You can find the sample code on GitHub under the projects/ch12 folder.

Go ahead and update the remainder of your tests and do not move on until they all start passing.

NgRx Data

If NgRx is a configuration-based framework, NgRx Data is a convention-based sibling of NgRx. NgRx Data automates the creation of stores, effects, actions, reducers, dispatches, and selectors. If most of your application actions are CRUD (Create, Retrieve, Update, and Delete) operations, then NgRx Data can achieve the same result as NgRx with a lot less code needing to be written.

NgRx Data may be a much better introduction to the Flux pattern for you and your team. Then you can go on to NgRx itself.

@ngrx/data works in tandem with the @ngrx/entity library. Together they offer a rich feature set, including transactional data management. Read more about it at https://ngrx.io/guide/data.

For this example, we will be switching back over to the LemonMart project.

Add NgRx Data to your project by executing the following commands:

$ npx ng add @ngrx/store --minimal
$ npx ng add @ngrx/effects --minimal
$ npx ng add @ngrx/entity
$ npx ng add @ngrx/data

The sample project under projects/ch12 configures @ngrx/store-devtools for debugging.

If you would like to be able to console.log NgRx actions for debugging or instrumentation during runtime, refer to the Appendix A, Debugging Angular.

Implementing NgRx/Data in LemonMart

In LemonMart, we have a great use case for the @ngrx/data library with the User class and the UserService. It neatly represents an entity that could support CRUD operations. With a few modifications and the least amount of effort, you can see the library in action.

This section uses the lemon-mart repo. You can find the code samples for this chapter under the projects/ch12 folder.

  1. Let's start by defining the User entity in entity-metadata.ts:
    src/app/entity-metadata.ts
    import { EntityMetadataMap } from '@ngrx/data'
    const entityMetadata: EntityMetadataMap = {
      User: {},
    }
    export const entityConfig = {
      entityMetadata,
    }
    
  2. Ensure that the entityConfig object is registered with EntityDataModule:
    src/app/app.module.ts
    imports: [
      ...
      StoreModule.forRoot({}),
      EffectsModule.forRoot([]),
      EntityDataModule.forRoot(entityConfig),
    ]
    
  3. Create a User entity service:
    src/app/user/user/user.entity.service.ts
    import { Injectable } from '@angular/core' 
    import {
      EntityCollectionServiceBase,
      EntityCollectionServiceElementsFactory,
    } from '@ngrx/data'
    import { User } from './user'
    @Injectable({ providedIn: 'root' })
    export class UserEntityService
      extends EntityCollectionServiceBase<User> {
      constructor(
        serviceElementsFactory: EntityCollectionServiceElementsFactory
      ) {
        super('User', serviceElementsFactory)
      }
    }
    

You now have all the basic elements in place to integrate the entity service with a component. In a sense, it is this easy to set up NgRx Data. However, we'll have to customize it somewhat to fit into our existing REST API structure, which will be covered in detail in the next section. If you were to follow the API implementation pattern that NgRx Data expects, then no changes would be necessary.

NgRx Data wants to access the REST API via the /api path, hosted on the same port as your Angular app. To accomplish this during development, we need to leverage Angular CLI's proxy feature.

Configuring proxy in Angular CLI

Normally, HTTP requests sent to our web server and our API server should have exactly the same URL. However, during development, we usually host both applications on two different ports of http://localhost. Certain libraries, including NgRx Data, require that HTTP calls be on the same port. This creates a challenge for creating a frictionless development experience. For this reason, Angular CLI ships with a proxy feature with which you can direct the /api path to a different endpoint on your localhost. This way, you can use one port to serve your web app and your API requests.

  1. Create a proxy.conf.json file under src, as shown:

    If you're working in the lemon-mart-server monorepo, this will be web-app/src.

    proxy.conf.json
    {
      "/api": {
        "target": "http://localhost:3000",
        "secure": false,
        "pathRewrite": {
           "^/api": ""
        }
      }
    }
    
  2. Register the proxy with angular.json:
    angular.json
    ...
    "serve": {
      "builder": "@angular-devkit/build-angular:dev-server",
      "options": {
        "browserTarget": "lemon-mart:build",
        "proxyConfig": "proxy.conf.json"
      },
      ...
    }
    

Now the server that is started when you run npm start or ng serve can rewrite the URLs of any call made to the /api route with http://localhost:3000. This is the port that lemon-mart-server runs by default.

If your API is running a different port, then use the correct port number and child route.

Next, let's use the UserEntityService.

Using Entity Service

We will be updating the User Management master view, so we can optionally use BehaviorSubject or the UserEntityService we just created.

  1. Start by implementing a toggle switch in user-table.component.ts, similar to the way we did for LocalCast Weather and NgRx earlier in the chapter:

    Figure 12.9: UserTableComponent with the NgRx slide toggle

  2. Inject the new service into UserTableComponent and merge its loading observable with the one that's present on the component:
    src/app/manager/user-table/user-table.component.ts
    useNgRxData = true
    readonly isLoadingResults$ = new BehaviorSubject(true) loading$: Observable<boolean>
    constructor(
      private userService: UserService,
      private userEntityService: UserEntityService
    ) {
      this.loading$ = merge(
        this.userEntityService.loading$, 
        this.isLoadingResults$
      )
    }
    

    Since EntityDataModule is registered in app.module.ts at the root of our application, we need to provide UserService in app.module.ts as well, so we can consume data from it in UserEntityService. Even though UserEntityService is provided in UserModule, the order of operations within NgRx Data doesn't lend itself to properly working with feature modules. This will probably be fixed at some point.

  3. You can add CRUD methods to the component, as shown in the following code. However, we will be focused on just updating the getUsers function so there is no need to add the others:
    src/app/manager/user-table/user-table.component.ts
    getUsers() {
      return this.userEntityService.getAll().pipe(
        map((value) => {
          return { total: value.length, data: value }
        })
      )
    }
    add(user: User) { 
      this.userEntityService.add(user)
    }
    delete(user: User) { 
      this.userEntityService.delete(user._id)
    }
    update(user: User) { 
      this.userEntityService.update(user)
    }
    
  4. In ngAfterViewInit, refactor the call to this.userService.getUsers so that it is called from a method named getUsers
  5. Then implement a conditional call to this.userEntityService.getAll() and map out the return value so that it fits the IUsers interface:
    src/app/manager/user-table/user-table.component.ts
    ...
      getUsers(pageSize: number, searchText = '', pagesToSkip = 0)
        : Observable<IUsers> {
          if (this.useNgRxData) {
            return this.userEntityService.getAll().pipe(   
              map((value) => {
                return { total: value.length, data: value }
              })
            )
          } else {
            return this.userService.getUsers(
              pageSize,
              searchText,
              pagesToSkip,
              sortColumn,
              sortDirection
            )
          }
    

Now your component can attempt to get data from either source by toggling the slide toggle and entering some new search text. However, our endpoint does not provide the data in the shape that NgRx Data expects, so we need to customize the entity service to overcome this issue.

Customizing Entity Service

You can customize the behavior of NgRx Data in numerous places. We are interested in overriding the behavior of the getAll() function, so the data we're receiving is properly hydrated and the data can be extracted from the item's object.

For this example, we will not attempt to restore the full pagination functionality using NgRx Data. To keep it simple, we focus on just getting an array of data into the data table.

Update User Entity Service to inject UserService and implement a getAll function that uses it:

src/app/user/user/user.entity.service.ts
...
getAll(options?: EntityActionOptions): Observable<User[]> {
  return this.userService
    .getUsers(10)
    .pipe(map((users) => users.data.map(User.Build)))
}
...

As you can see, we're iterating through the item's object and hydrating objects with our builder function, thus flattening and transforming Observable<IUsers> to Observable<User[]>.

After implementing this change, you should be able to see data flow into the user table as follows:

Figure 12.10: User table with NgRx Data

Note that all seven users are displayed at once, and as magnified in the preceding screenshot, the pagination functionality is not working. However, this implementation is adequate enough to demonstrate what NgRx Data brings to the table.

So, should you implement NgRx Data in your next app? It depends. Since the library is an abstraction layer on top of NgRx, you may find yourself lost and restricted if you don't have a good understanding of the internals of NgRx. However, the library holds a lot of promise for reducing boilerplate code regarding entity data management and CRUD operations. If you're doing lots of CRUD operations in your app, you may save time, but be careful to keep the scope of your implementation only to the areas that need it. Either way, you should keep an eye out for the evolution of this great library.

Summary

In this chapter, we completed going over all major Angular app design considerations using router-first architecture, along with our recipes, to implement a line-of-business app with ease. We went over how to edit existing users, leverage a resolve guard to load user data, and hydrate and reuse a component in different contexts.

We implemented a master/detail view using auxiliary routes and demonstrated how to build data tables with pagination. We then learned about NgRx and the @ngrx/data libraries and their impact on our code base using the local-weather-app and lemon-mart projects.

Overall, by using the router-first design, architecture, and implementation approach, we tackled our application's design with a good high-level understanding of what we wanted to achieve. By identifying code reuse opportunities early on, we were able to optimize our implementation strategy to implement reusable components ahead of time, without running the risk of grossly over-engineering our solution.

In the next chapter, we will set up a highly available infrastructure on AWS to host LemonMart. We will update the project with new scripts to enable no-downtime blue-green deployments. Finally, in the last chapter, we will update LemonMart with Google Analytics and go over advanced Cloud Ops concerns.

Further reading

Questions

Answer the following questions as best as you can to ensure that you've understood the key concepts from this chapter without Googling. Do you need help answering the questions? See Appendix D, Self-Assessment Answers online at https://static.packt-cdn.com/downloads/9781838648800_Appendix_D_Self-Assessment_Answers.pdf or visit https://expertlysimple.io/angular-self-assessment.

  1. What is a resolve guard?
  2. What are the benefits of router orchestration?
  3. What is an auxiliary route?
  4. How does NgRx differ from using RxJS/Subject?
  5. What's the value of NgRx data?
  6. In UserTableComponent, why do we use readonly isLoadingResults$: BehaviorSubject<Boolean> over a simple Boolean to drive the loading spinner?