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
Loginas any userNavigateto the user account viewNew view should be available at the following URL:
/account
Observeuser information is displayedCreated atDisplay nameEmailPhone numberUidUpdated at
Updatethe application header 'Authentication' navigation menu to include an 'Account' linkLink should be displayed as the last menu item
Logoutfrom user accountNavigateto/accountObserveunauthenticated user is redirected to/sign-in
Technical requirements
Accountfeature moduleAccountstate managementAccountserviceRetrieveaccount information from the/usersAPI
Accountloaded 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 --flatThe 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|specProject 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/accountCreate Account component
Run the below command to generate the Account component
nx g component --project=features-account --flat --style=scssThe 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
Fromlib.routes.tsToaccount-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=servicesThe 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|specDelete 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-accountUpdate 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-interactiveThe 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|specDelete 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-accountUpdate 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