This project is using Angular CLI version 15.0.0.
In this Task Manager example, I've used NgRx for reactivate state management. You can think of NgRx as a client-side cache or database. Angular NgRx is the equivalent of React Redux, but of course, NgRx is for Angular applications.
Install NgRx Store
$ npm i @ngrx/store
This is the folder structure I've set up in "src/app":
Let's start with Actions: I've created 4 Task Actions / Action Types, these 4 Task Actions will be used to handle CRUD (Create, Read, Update, Delete) functionality.
import { Action } from '@ngrx/store';
import { ITask } from '../../models/task.model';
export enum TaskActionType {
FETCH_TASKS = '[TASK] Fetch Tasks',
ADD_TASK = '[TASK] Add Task',
SET_TASK_TO_COMPLETE = '[TASK] Set Task To Complete',
DELETE_TASK = '[TASK] Delete Task',
}
export class FetchTasksAction implements Action {
readonly type = TaskActionType.FETCH_TASKS;
constructor(public tasks: ITask[]) { }
}
export class AddTaskAction implements Action {
readonly type = TaskActionType.ADD_TASK;
constructor(public task: ITask) { }
}
export class SetTaskToCompleteAction implements Action {
readonly type = TaskActionType.SET_TASK_TO_COMPLETE;
constructor(public id: number) { }
}
export class DeleteTaskAction implements Action {
readonly type = TaskActionType.DELETE_TASK;
constructor(public id: number) { }
}
export type TaskAction = FetchTasksAction
| AddTaskAction
| SetTaskToCompleteAction
| DeleteTaskAction;
In addition, we'll need a reducer to handle the state data changes. The TaskReducer consists of a switch statement that handles each Action Type and then returns the updated state.
import { ITask } from '../../models/task.model';
import { TaskAction, TaskActionType } from '../actions/task.action';
export const TaskReducer = (
state: ITask[] = new Array<ITask>(),
action: TaskAction
) => {
switch (action.type) {
case TaskActionType.FETCH_TASKS:
return action.tasks;
case TaskActionType.ADD_TASK:
return [
...state,
action.task
];
case TaskActionType.SET_TASK_TO_COMPLETE:
return state.map((task: ITask) => {
if (task.id === action.id) {
task = { ...task, complete: true };
}
return task;
});
case TaskActionType.DELETE_TASK:
return state.filter((task: ITask) =>
task.id !== action.id
);
default:
return state;
}
}
I've created a root reducer that maps the TaskReducer. You can bundle multiple reducers in the RootReducer map, but for simplicity, I'll just be showing you 1 example with TaskReducer.
import { ActionReducerMap } from '@ngrx/store';
import { IAppState } from 'src/app/models/app.state.model';
import { TaskReducer } from './task.reducer';
export const RootReducers: ActionReducerMap<IAppState, any> = {
task: TaskReducer
};
To enable the global store - all you have to do is set the following in @NgModule.imports in AppModule:
StoreModule.forRoot(RootReducers)
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { TaskManagerModule } from './components/task-manager/task-manager.module';
import { StoreModule } from '@ngrx/store';
import { RootReducers } from './store/reducers';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
TaskManagerModule,
StoreModule.forRoot(RootReducers)
],
bootstrap: [
AppComponent
]
})
export class AppModule { }
The global store has now been enabled. It can be accessed and used throughout the application.
I've created a mock API request in TaskService. The getInitialTaskData() method will be called in the ngOnInit lifecycle hook in TaskManagerComponent, which will set the initial task data to the store.
import { Injectable } from "@angular/core";
import { Observable, of } from "rxjs";
import { ITask } from "../models/task.model";
@Injectable()
export class TaskService {
public getInitialTaskData(): Observable<ITask[]> {
return of([
{ id: 1, complete: false },
{ id: 2, complete: false }
]);
}
}
import { Component, OnInit } from "@angular/core";
import { Store } from "@ngrx/store";
import { Observable } from 'rxjs';
import { FormBuilder, FormControl, FormGroup, Validators } from "@angular/forms";
import { AddTaskAction, DeleteTaskAction, FetchTasksAction, SetTaskToCompleteAction } from '../../store/actions/task.action';
import { IAppState } from '../../models/app.state.model';
import { TaskService } from '../../services/task.service';
import { ITaskForm } from '../../models/task-form.model';
import { ITask } from '../../models/task.model';
@Component({
selector: 'task-manager',
templateUrl: './task-manager.component.html'
})
export class TaskManagerComponent implements OnInit {
public taskData$: Observable<ITask[]> = this.store.select((store) => store.task);
public form: FormGroup;
constructor(
private store: Store<IAppState>,
private taskService: TaskService,
private fb: FormBuilder
) {
this.form = this.fb.group<ITaskForm>({
id: new FormControl<number | null>(null, Validators.required)
});
}
ngOnInit(): void {
this.taskService.getInitialTaskData().subscribe(
(data: ITask[]) => this.store.dispatch(new FetchTasksAction(data))
);
}
public addTask(values: ITask): void {
if (this.form.valid) {
values.complete = false;
this.store.dispatch(new AddTaskAction(values));
this.form.reset();
}
}
public deleteTask(id: number): void {
this.store.dispatch(new DeleteTaskAction(id));
}
public setToComplete(id: number): void {
this.store.dispatch(new SetTaskToCompleteAction(id));
}
}
The taskData$ Observable selects the task data from the store. Existing, new, or updated data will be outputted every single time on page load or when state changes have been made.
public taskData$: Observable<ITask[]> = this.store.select((store) => store.task);
As you can see, TaskManagerComponent dispatches to the store (followed by an Action object) in the following methods: ngOnInit, addTask, deleteTask, and setToComplete.
this.store.dispatch(new FetchTasksAction(data));
this.store.dispatch(new AddTaskAction(values));
this.store.dispatch(new DeleteTaskAction(id));
this.store.dispatch(new SetTaskToCompleteAction(id));
When we dispatch an action, it triggers a state change. The TaskReducer is called and will handle the state changes based on the given Action Type.
Once the state changes have been made, the updated task data will then be outputted from the store and set to taskData$.
I've provided an example of how NgRx works in Angular applications. To get a deeper understanding, I recommend you download this repo and play around with the user interface and code.
Run ng serve
for a dev server. Navigate to http://localhost:4200/
. The application will automatically reload if you change any of the source files.
Run ng generate component component-name
to generate a new component. You can also use ng generate directive|pipe|service|class|guard|interface|enum|module
.
Run ng build
to build the project. The build artifacts will be stored in the dist/
directory.
Run ng test
to execute the unit tests via Karma.
Run ng e2e
to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
To get more help on the Angular CLI use ng help
or go check out the Angular CLI Overview and Command Reference page.