User Account feature example

The following is a walkthrough of creating the Account feature.

Example change request

Overview

The User Account feature will be a new view that displays user account information to the authenticated user.

Acceptance criteria

  • Login as any user

  • Navigate to the user account view

    • New view should be available at the following URL: /account

  • Observe user information is displayed

    • Created at

    • Display name

    • Email

    • Phone number

    • Uid

    • Updated at

  • Update the application header 'Authentication' navigation menu to include an 'Account' link

    • Link should be displayed as the last menu item

  • Logout from user account

  • Navigate to /account

  • Observe unauthenticated user is redirected to /sign-in

Technical requirements

  • Account feature module

  • Account state management

  • Account service

    • Retrieve account information from the /users API

  • Account loaded guard

Generate Account feature library

Run the below command to generate the Account feature library.

nx g lib --directory=features --name=account --routing --simpleName --style=scss --flat

The following project files should be created:

libs
 ...
 - features
   - account
      - src
        - lib
          - account.module.ts
          - lib.routes.ts
        - index.ts
        - test-setup.ts
      - .eslintrc.json
      - jest.config.ts
      - project.json
      - README.md
      - tsconfig.json|lib|spec

Project file tsconfig.base.json has also been updated to include the new project path and references should refer to this path.

@full-stack-typescript/features/account

Create Account component

Run the below command to generate the Account component

nx g component --project=features-account --flat --style=scss

The following project files should be created:

libs
 ...
 account
  - src
    - lib
      - account.component.ts|html|scss|spec
      - account.module.ts                    (updated)

Update Account route

Rename the routes file libs\features\account\src\lib\lib.routes.ts

  • From lib.routes.ts

  • To account-routing.module.ts

Update the account-routing.module.ts to be:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AccountComponent } from './account.component';

const routes: Routes = [
	{
		path: '',
		component: AccountComponent ,
		resolve: []
	}
];

@NgModule({
	imports: [RouterModule.forChild(routes)],
	exports: [RouterModule]
})
export class AccountRoutingModule {}

Update libs\features\account\src\lib\account.module.ts by removing all routing references and importing the new AccountRoutingModule

import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { AccountRoutingModule } from './account-routing.module';

@NgModule({
  imports: [CommonModule, AccountRoutingModule],
})
export class AccountModule {}

Update libs\features\account\src\index.ts and remove the routing export

Update application routing

Update the application routing file: apps\full-stack-typescript\src\app\app.routes.ts

...

export const appRoutes: Route[] = [
    ...
    {
        path: 'account',
        loadChildren: () => import('@full-stack-typescript/features/account').then((m) => m.AccountModule)
        title: 'Account Details - Full Stack Typescript'     
    },
    ...
];

Update the application navigation: apps\full-stack-typescript\src\app\app.component.html

<button mat-menu-item routerLink="account">Account</button>

The /account view should now be accessible.

Create Account service

Run the below command to create the Account service library:

nx g lib --name=account --directory=services

The following files will be generated:

libs
  - services
    ...
    - account
      - src
        - lib
          - services-account.module.ts
        - index.ts
        - test-setup.ts
      - .eslintrc.json
      - jest.config.ts
      - project.json
      - README.md
      - tsconfig.json|lib|spec

Delete the services-account.module.ts file.

Update the index.ts file to remove the module export.

Run the below command to create the Account Service

nx g service --name=account --path=libs/services/account/src/lib --project=services-account

Update the index.ts file to export the account service file.

export * from './lib/account.service';

Implement NgRx state management

Run the below command to create the NgRx boilerplate.

nx g @nrwl/angular:ngrx account --module=libs/features/account/src/lib/account.module.ts --facade --no-interactive

The following files should be created.

libs
 ...
 - features
   - account
      - src
        - lib
          - +state
            - account.actions.ts|spec
            - account.effects.ts|spec
            - account.facade.ts|spec
            - account.models.ts
            - account.reducer.ts|spec
            - account.selectors.ts|spec
          - account.module.ts              (updated)

Default NgRx schematics implements the entity pattern. This feature only loads a single user account and can use a simplified approach.

Delete the account.models.ts file.

Update actions

Update the libs\features\account\src\lib+state\account.actions.ts file to be:

import { User } from '@full-stack-typescript/models';
import { createAction, props } from '@ngrx/store';

export const loadAccount = createAction('[Account] Load account');

export const loadAccountSuccess = createAction(
  '[Account] Load account Success',
  props<{ account: User }>()
);

export const loadAccountFailure = createAction(
  '[Account] Load account Failure',
  props<{ error: any }>()
);

Update reducer

Create the AccountState interface at libs\models\src\lib\account\account-state.interface.ts

import { User } from "../user";

export interface AccountState extends User {
    error?: any;
    isLoaded: boolean;
    isLoading: boolean;
}

Create the index.ts barrel file at libs\models\src\lib\account\index.ts

Update the barrel file to export the account-state.interface.ts file

Update the models barrel file at libs\models\src\index.ts to export the account folder

Update the libs\features\account\src\lib+state\account.reducer.ts file to be:

import { AccountState } from '@full-stack-typescript/models';
import { Action, createReducer, on } from '@ngrx/store';
import * as accountActions from './account.actions';

export const initialState: AccountState = {
    email: '',
    error: null,
    isLoaded: false,
    isLoading: false,
    uid: ''
}

const accountReducer = createReducer(
    initialState,
    on(accountActions.loadAccount, (state) => ({
        ...state,
        isLoading: true,
    })),
    on(accountActions.loadAccountFailure, (state, { error }) => ({
        ...state,
        error,
        isLoaded: false,
        isLoading: false
    })),
    on(accountActions.loadAccountSuccess, (state, { account }) => ({
        ...state,
        ...account,
        error: null,
        isLoaded: true,
        isLoading: false,
    })),
);

export function reducer(state: AccountState | undefined, action: Action) {
    return accountReducer(state, action);
}

Update selectors

Create the ACCOUNT_FEATURE_KEY constant at libs\constants\src\lib\account\account-feature-key.constant.ts

export const ACCOUNT_FEATURE_KEY = 'account';

Update the barrel file to export the account-feature-key.constant.ts file

Update the constants barrel file at libs\constants\src\index.ts to export the account folder

Update the libs\features\account\src\lib+state\account.selectors.ts file to be:

import { ACCOUNT_FEATURE_KEY } from '@full-stack-typescript/constants';
import { AccountState } from '@full-stack-typescript/models';
import { createFeatureSelector, createSelector } from '@ngrx/store';

// Lookup the 'Account' feature state managed by NgRx
export const getAccountState = createFeatureSelector<AccountState>(ACCOUNT_FEATURE_KEY);

export const getAccountIsLoaded = createSelector(
    getAccountState,
    (state: AccountState) => state.isLoaded
);

export const getAccountIsLoading = createSelector(
    getAccountState,
    (state: AccountState) => state.isLoading
);

export const getAccountError = createSelector(
    getAccountState,
    (state: AccountState) => state.error
);

Update facade

Create the UserPartialState interface at libs\models\src\lib\account\account-partial-state.interface.ts

Update index.ts barrel file as required.

Update the libs\features\account\src\lib+state\account.facade.ts file to be:

import { Injectable } from '@angular/core';
import { AccountPartialState } from '@full-stack-typescript/models';
import { select, Store } from '@ngrx/store';
import { loadAccount } from './account.actions';
import * as accountSelectors from './account.selectors';

@Injectable({ providedIn: 'root' })
export class AccountFacade {

    account$ = this.store.pipe(select(accountSelectors.getAccountState));
    error$ = this.store.pipe(select(accountSelectors.getAccountError));
    isLoaded$ = this.store.pipe(select(accountSelectors.getAccountIsLoaded));
    isLoading$ = this.store.pipe(select(accountSelectors.getAccountIsLoading));

    constructor(private store: Store<AccountPartialState>) {}

    loadAccount() {
        this.store.dispatch(loadAccount());
    }

}

Update effects

Update libs\features\account\src\lib+state\account.effects.ts to be:

import { Injectable } from '@angular/core';
import { AuthState, uid } from '@full-stack-typescript/features/auth';
import { User } from '@full-stack-typescript/models';
import { AccountService } from '@full-stack-typescript/services/account';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { select, Store } from '@ngrx/store';
import { catchError, of } from 'rxjs';
import { concatMap, exhaustMap, map, withLatestFrom } from 'rxjs/operators';
import * as accountActions from './account.actions';

@Injectable()
export class AccountEffects {

	constructor(
		private actions$: Actions,
		private accountService: AccountService,
		private auth: Store<AuthState>
	) {}

	loadAccount$ = createEffect(() =>
		this.actions$.pipe(
		ofType(accountActions.loadAccount),
		concatMap((action) =>
			of(action).pipe(withLatestFrom(this.auth.pipe(select(uid))))
		),
		exhaustMap(([action, userUid]) =>
			this.accountService.loadAccount(userUid).pipe(
				map((account: User) => {
					return accountActions.loadAccountSuccess({ account });
				}),
				catchError((error) => of(accountActions.loadAccountFailure({ error })))
			)
		)
		)
	);
}

Update Account module

Update libs\features\account\src\lib\account.module.ts for update import paths and dependencies.

...
import { ACCOUNT_FEATURE_KEY } from '@full-stack-typescript/constants';
import { EffectsModule } from '@ngrx/effects';
import { StoreModule } from '@ngrx/store';
import * as fromAccount from './+state/account.reducer';
import { AccountEffects } from './+state/account.effects';

@NgModule({
  imports: [
    ...
    StoreModule.forFeature(
      ACCOUNT_FEATURE_KEY,
      fromAccount.reducer
    ),
    EffectsModule.forFeature([AccountEffects]),
  ],
  ...
  providers: [],
})
export class AccountModule {}

Create account loaded resolver function

Generate an account library in the resolvers directory by running the below command:

nx g lib --directory=resolvers --name=account

libs
  - resolvers
    ...
    - account
      - src
        - lib
          - resolvers-account.module.ts
        - index.ts
        - test-setup.ts
      - .eslintrc.json
      - jest.config.ts
      - project.json
      - README.md
      - tsconfig.json|lib|spec

Delete the resolvers-account.module.ts file.

Generate an account loaded resolver function in the resolvers directory by running the below command:

nx g resolver --name=account-loaded --flat --functional --path=libs/resolvers/account/src/lib --project=resolvers-account

Update the account-loaded.resolver.ts file to be:

import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { AccountFacade } from '@full-stack-typescript/features/account';

export const accountLoadedResolver: ResolveFn<void> = () => {

	return inject(AccountFacade).loadAccount();

};

Updated the index.ts barrel file to export the resolver function.

Update the application routes to include the new guard.

apps\full-stack-typescript\src\app\app.routes.ts

    { path: 'account', resolve: { account: accountLoadedResolver }, loadChildren: () => import('@full-stack-typescript/features/account').then((m) => m.AccountModule) },

Update Account component to retrieve account information

Update libs\features\account\src\lib\account.component.ts to be:

import { Component } from '@angular/core';
import { AccountFacade } from './+state/account.facade';

@Component({
	selector: 'full-stack-typescript-account',
	templateUrl: './account.component.html',
	styleUrls: ['./account.component.scss'],
})
export class AccountComponent {

	account$ = this.accountFacade.account$;

	constructor(private accountFacade: AccountFacade) {}

}

Update Account template to display account information

Update libs\features\account\src\lib\account.component.html to be:

<div>
    {{ account$ | async | json }}
</div>

Last updated