Example change request
Overview
The User Account feature will be a new view that displays user account information to the authenticated user.
Acceptance criteria
Navigate
to the user account view
New view should be available at the following URL: /account
Observe
user information is displayed
Update
the application header 'Authentication' navigation menu to include an 'Account' link
Link should be displayed as the last menu item
Observe
unauthenticated user is redirected to /sign-in
Technical requirements
Account
service
Retrieve
account information from the /users
API
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
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
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
libs\features\account\src\lib\account.component.html
to be:
<div>
{{ account$ | async | json }}
</div>