🖥️
Full stack Typescript
  • Introduction
  • Environment setup
    • Workspace setup
    • Firebase project setup
    • Firebase authentication
    • Firestore database
    • Firebase hosting
  • Getting started with the Full Stack Typescript repository
  • Workflows
    • Development workflow
      • Component workflow
        • Application Toolbar component example
      • Feature workflow
        • User Account feature example
      • Function workflow
        • Update User Account callable function example
      • Web3 function workflow
        • Query Ethereum balance callable function example
    • Icon workflow
    • Push notification workflow
    • PWA workflow
      • Making your PWA Google Play Store ready
    • Secret Manager API workflow
  • Styleguide
    • Architecture overview
    • Naming conventions
    • Single-responsibility principle
  • Change Requests
    • Request for changes
      • Change pattern proposal template
      • New pattern proposal template
Powered by GitBook
On this page
  • Example change request
  • Generate Account feature library
  • Update application routing
  • Create Account service
  • Implement NgRx state management
  • Create account loaded resolver function
  • Update Account component to retrieve account information
  • Update Account template to display account information
  1. Workflows
  2. Development workflow
  3. Feature workflow

User Account feature example

The following is a walkthrough of creating the Account feature.

PreviousFeature workflowNextFunction workflow

Last updated 2 years ago

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>
Example change request
Overview
Acceptance criteria
Technical requirements
Generate Account feature library
Create Account component
Update Account route
Update application routing
Create Account service
Implement NgRx state management
Update actions
Update reducer
Update selectors
Update facade
Update effects
Update Account module
Create account loaded resolver function
Update Account component to retrieve account information
Update Account template to display account information