When we are working with
rest environment it becomes a necessity to manage the user's state at the client
side instead of making a call to the server every now and then.
Hey guys, in this post
we are going see how to manage the state of the user at the client side in angular
application with the help of NgRx.
[at the bottom of the post you will get the link to download the project]
Before starting, we need
to understand the following terms of NgRx in short.
1.Store
2. Actions
3. Reducer
4. Effects (go here NgRx effects)
Store - as the name suggests
it is the place where we keep user’s data.
Actions - with store we
have data, but to manipulate the data, we need to define some actions that will
be performed on data, like add or update action which will add or update the data.
Reducer - now we have
data and actions as well but, we must implement the logic for a given action, means
we need to write an implementation of add action.
Let’s do it step by
step.
At very first, install the
NgRx store which is the core module in NgRx.
Following is the
installation command and description.
npm install –save
@ngrx/store
D:\Angular-DemoApps\Angular 6 - ngrx
implementation> npm install --save @ngrx/store
npm WARN registry Using stale data from
https://registry.npmjs.org/ because the host is inaccessible -- are you
offline?
npm WARN registry Using stale package
data from https://registry.npmjs.org/ due to a request error during
revalidation.
npm WARN bootstrap@4.2.1 requires a peer
of jquery@1.9.1 - 3 but none is installed. You must install peer dependencies
yourself.
npm WARN bootstrap@4.2.1 requires a peer
of popper.js@^1.14.6 but none is installed. You must
install peer dependencies yourself.
npm WARN @ngrx/store@8.2.0 requires a
peer of @angular/core@^8.0.0 but none is installed. You
must install peer dependencies yourself.
npm WARN @ngrx/store@8.2.0 requires a
peer of rxjs@^6.4.0 but none is installed. You must install peer dependencies
yourself.
npm WARN optional SKIPPING OPTIONAL
DEPENDENCY: fsevents@1.2.4 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL
DEPENDENCY: Unsupported platform for fsevents@1.2.4: wanted
{"os":"darwin","arch":"any"}
(current: {"os":"win32","arch":"x64"})
+ @ngrx/store@8.2.0
updated 1 package in 21.757s
After completing the installation,
we are going to create the actions and reducer.
Here is the user model
which defines its properties.
export class User {
id: number;
userName: string;
password: string;
email: string;
contactNo: string;
address: string;
edited: boolean;
constructor() { };
}
After that, we are going
to define, what actions can be performed with the user
User action page defines
all the actions that can be performed on user data and the data needed to
complete that action accepted as a constructor parameter.
user.actions.ts
import { Action } from '@ngrx/store';
import { User } from 'src/app/user/user';
export const ADD_USER = '[second page] add user';
export const DELETE_USER = '[second page] delete user';
export const UPDATE_USER = '[second page] update user';
export class AddUser implements Action {
readonly type = ADD_USER;
constructor(public payload: User) { }
}
export class UpdateUser implements Action {
readonly type = UPDATE_USER;
constructor(public payload: { user: User, index: number }) { }
}
export class DeleteUser implements Action {
readonly type = DELETE_USER;
constructor(public payload: number) { }
}
export type userListActions = | AddUser | DeleteUser | UpdateUser;
In action page we have
defined add, delete, and update actions, which will respectively add delete and
update user in client-side state(data).
We have defined actions,
but we must need to specify how to perform the action on data so let’s
implement their functionality, that’s where reducer comes into the picture. Reducer
contains the business logic for the given action.
import * as UserActions from './user.actions';
import { User } from 'src/app/user/user';
export interface UserState {
userList: User[];
}
const initialState = {
userList: []
};
export function userActionReducer(state = initialState, action: UserActions.userListActions) {
switch (action.type) {
case UserActions.ADD_USER:
return {
...state,
userList: [...state.userList, action.payload]
};
case UserActions.DELETE_USER:
return {
...state,
userList: state.userList.filter((user, index) => {
return index != action.payload;
})
};
case UserActions.UPDATE_USER:
const oldUser = state.userList[action.payload.index];
const updatedUser = {
...oldUser,
...action.payload.user
};
const updatedUserList = [...state.userList];
updatedUserList[action.payload.index] = updatedUser;
return {
...state,
userList: updatedUserList
};
default: return state;
}
}
Here in the reducer we
first declared a UserState that will be a type of the store and initialized the
same, because if we don’t have the type how to validate and access the data from
store, that’s why we have defined the type of state/store. Here we are dealing
with the user’s data, so we have defined the type of the state as a list of users.
But this is all about
user and user-related page but, our applications are never going to be that
much small, we might have dozens or lot more than that type of data. To manage all
that data in the state, we have one common state from all states at app level that
is app reducer which will manage sub-states from app and map all reducers for
sub-state-specific data.
app.reducer.ts
import { ActionReducerMap } from '@ngrx/store';
import * as fromUserActionsReducer from '../user-store/user.reducer';
export interface AppState {
userList: fromUserActionsReducer.UserState;
}
export const appReducer: ActionReducerMap<AppState> = {
userList: fromUserActionsReducer.userActionReducer
};
Here we have declared
the common state that is AppState and in app state, we have mapped the sub
states and reducer to manage that state.
We are done with all
prerequisite for ngrx state now is going to use it.
a home page which will display the list of users
exist in the application also having action buttons to add, delete and edit the
user details.
homepage.component.html
<div class="container">
<div class="row no-gutters">
<div class="col-2"></div>
<div class="col-8">
<label class="h3">Welcome to ngrx demo</label><br>
</div>
<div class="col-2"></div>
<div class="col-2"></div>
<div class="col-8">
<table class="table table-bordered table-striped">
<thead>
<tr>
<th colspan="4">
Existing Users
</th>
<th>
<button class="btn btn-info ml-2" (click)="goToRegisteration()">Add User</button>
</th>
</tr>
<tr>
<th>
Action
</th>
<th>
User name
</th>
<th>
Email
</th>
<th>
Contact No
</th>
<th>
Address
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let userToShow of (localUserList | async).userList; let i = index">
<td>
<button class="btn btn-sm btn-info" (click)="editUser(i)">Edit</button>
<button class="btn btn-sm btn-danger ml-2" (click)="removeUser(i)">Delete</button>
</td>
<td>{{userToShow.userName}}</td>
<td>{{userToShow.email}}</td>
<td>{{userToShow.contactNo}}</td>
<td>{{userToShow.address}}</td>
</tr>
</tbody>
</table>
</div>
<div class="col-2"></div>
</div>
</div>
homepage.component.ts
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { User } from '../user/user';
import { Store } from '@ngrx/store';
import { Router } from '@angular/router';
import * as UserActions from "../user-store/user.actions";
import * as fromApp from '../store/app.reducer'
@Component({
selector: 'app-homepage',
templateUrl: './homepage.component.html',
styleUrls: ['./homepage.component.css']
})
export class HomepageComponent implements OnInit {
localUserList: Observable<{ userList: User[] }>;
constructor(
private router: Router,
private store: Store<fromApp.AppState>
) { }
ngOnInit() {
this.localUserList = this.store.select('userList');
}
goToRegisteration() {
this.router.navigate(['/secondpage']);
}
removeUser(index: number) {
this.store.dispatch(new UserActions.DeleteUser(index));
}
editUser(index: number) {
this.router.navigate([`/secondpage/${index}`]);
}
}
On
homepage we have created a table which will hold a list of added users, also we
have kept few buttons to add, edit and delete, etc the user.
At first, we read if
there is any user in the state(store) and displayed same on the home page, after that
on add and edit button we redirected user to the second page so he can add/edit
user, then on delete we have dispatched deleted user action on the store.
The second page will contain
code to add a new user to the state. Here it is.
second-page.components.html
<div class="container">
<div class="row no-gutters">
<div class="col-2">
</div>
<div class="col-6">
<form action="" #frm="ngForm">
<table class="table">
<thead>
<tr>
<th>
<label class="h3">Registration</label>
</th>
<th>
<button class="btn btn-info float-right" (click)="goToHome()">Back</button>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>User name</td>
<td><input [(ngModel)]="user.userName" class="form-control" type="text" name="userName"></td>
</tr>
<tr>
<td>password</td>
<td><input [(ngModel)]="user.password" class="form-control" type="password" name="password"></td>
</tr>
<tr>
<td>confirm password</td>
<td><input class="form-control" type="password" name="password"></td>
</tr>
<tr>
<td>email</td>
<td><input [(ngModel)]="user.email" class="form-control" type="text" name="email"></td>
</tr>
<tr>
<td>contact No</td>
<td><input [(ngModel)]="user.contactNo" class="form-control" type="text" name="contactNo"></td>
</tr>
<tr>
<td>address</td>
<td><input [(ngModel)]="user.address" class="form-control" type="text" name="address"></td>
</tr>
<tr>
<td></td>
<td><button class="btn btn-light">Cancel</button> <button class="btn btn-primary"
(click)="saveUser()">save</button></td>
</tr>
</tbody>
</table>
</form>
</div>
</div>
</div>
second-page.component.ts
import { Component, OnInit } from '@angular/core';
import { User } from '../user/user';
import { Store } from "@ngrx/store";
import { Observable } from 'rxjs';
import { Router, ActivatedRoute } from '@angular/router';
import * as UserActions from "../user-store/user.actions";
import * as fromApp from '../store/app.reducer';
@Component({
selector: 'app-second-page',
templateUrl: './second-page.component.html',
styleUrls: ['./second-page.component.css']
})
export class SecondPageComponent implements OnInit {
user: User;
localUserList: Observable<{ userList: User[] }>;
userIndex: number = -1;
constructor(
private router: Router,
private activatedRoute: ActivatedRoute,
private store: Store<fromApp.AppState>
) { }
ngOnInit() {
this.activatedRoute.params.subscribe(paramMap => {
this.userIndex = +paramMap["index"];
if (this.userIndex != undefined && this.userIndex > -1) {
this.store.select("userList").subscribe(storeMap => {
this.user = storeMap.userList[this.userIndex];
});
} else {
this.user = new User();
}
})
}
saveUser() {
let tmpUser = Object.assign({}, this.user);
if (this.userIndex != undefined && this.userIndex > -1) {
this.store.dispatch(new UserActions.UpdateUser({ user: tmpUser, index: this.userIndex }));
} else {
this.store.dispatch(new UserActions.AddUser(tmpUser));
}
this.goToHome();
}
goToHome() {
this.router.navigate(['/home']);
}
}
In the second page we have
designed a form to accept the basic details of the user.
Here we have used the same
page to edit as well as save new user, to achieve that we simply accepted the
index of user list from home page in case of edit otherwise it will be the new
user.
And finally, on save if
it is edited user, we have dispatched update user action on store otherwise add
user action.
Here is the output
And that’s it,
We have successfully
managed the state of the user at the client-side with the help of ngrx.
I hope you like it.
[We will implement effects next post]
Here is the link to download the project [Download the project]
Comments
Post a Comment