In this lab you will refactor a simole TodoMVC angular to use the ngrx platform for state managment.
- this repo containes a number of projects.
- the main project you are refactoring is under the
src
folder. - solution for both exercises are under ther
projects
folder. - if you would like to run either one of the solutions you can do so with the following command in your terminal.
//exercise 1 npm run start:ex1 //exercise 2 npm run start:ex2
- the main project you are refactoring is under the
This lab already contains all the neccesary ngrx dependencies but when starting a clean project you should follow these steps
- Clone or dowload this repo.
- In your terminal
cd
into your local repo folder. - install dependencies
npm install
- run the project
npm start.
In this exercise we will setup our store create our first actions and reducers, and connect our container component to listen for state changes.
- Create an action file for Tasks related actions.
ng g action todo/Filter --group
- Go to the file created
todo/actions/filter.actions.ts
.- Create a new Action for handling the state transitions of the currentFilter that filters the list.
- Add a new action type to
FilterActionTypes
enum and remove the exsiting one.
export enum FilterActionTypes { FilterTasks = '[Tasks] filter tasks' }
- Create a new
FilterTasks
action class to define the Action and it's payload.
export class FilterTasks implements Action { readonly type = FilterActionTypes.FilterTasks; constructor(public payload:{ filter:Filter }){} }
- Add the FilterTasks class to
TasksActions
union type
export type FilterActions = FilterTasks;
- create the root state with the Cli
ng g store State --root --module app.module.ts
- Create the tasks feature state
ng g store todo/Tasks -m todo.module.ts
- Create the tasks list reducer of the feature state
ng generate reducer todo/Filter --reducers reducers/index.ts --group
- Inside the file created
todo/reducers/filter.reducer.ts
- Add the currentFilter property to the State interface, this props will hold the current filter the task list is filtered by.
export interface State { currentFilter: Filter }
- Add the intial value of the currentFilter to initial state object.
export const initialState: State = { currentFilter: Filter.ALL };
- Inside the reducer function add a case to the switch to handle the chages of the currentFilter state when the
FilterTasks
action is dispatched
export function reducer(state = initialState, action: FilterActions): State { switch (action.type) { case FilterActionTypes.FilterTasks: return { ...state, currentFilter: action.payload.filter }; default: return state; } }
- Inside
todo/reducers/index.ts
create selectors for the selecting the Tasks current filter- create a feature selector
export const getTasks = createFeatureSelector<State>('tasks');
- create a selector for the list state
export const getFilter = createSelector( getTasks, (state) => state.filter )
- create a selector for the currentFilter
export const getCurrentFilter= createSelector( getFilter, (state) => state.currentFilter )
- Inside the
todo/todo-container/todo-container.component.ts
add the Store service to the component.
constructor(
private store: Store<fromTasks.State>,
private taskService: TaskService) {}
- Update the
currentFilter
to be anobservable<Filter>
and add the $ suffix to it's name.
private currentFilter$: Observable<Filter>;
- Inside the
ngOnInit
method set the newcurrentFilter$
observable to select the currentFilter from the state via the store service. - Add the
tap
operator for the observable, so that on every filter change new tasks will be fetched.
this.currentFilter$ = this.store.select( getCurrentFilter )
.pipe(
tap( (filter) => this.fetchTasks(filter))
);
- In the
submitTask
,removeTask
andtoggleTask
methods, pipe thewithLatestFrom
operator to get the latest filter value from thecurrentFilter$
observable.submitTask(title: string) { this.taskService.addTask(title) .pipe( withLatestFrom(this.currentFilter$) ) .subscribe( ([task,filter]) => this.fetchTasks(filter) ); } removeTask(task: Task) { this.taskService.deleteTask(task) .pipe( withLatestFrom(this.currentFilter$) ) .subscribe( ([task, filter]) => this.fetchTasks(filter) ); } toggleTask(task: Task) { this.taskService.updateTask(task) .pipe( withLatestFrom(this.currentFilter$) ) .subscribe( ( [task,filter] ) => this.fetchTasks(filter) ); }
- update the
filterTasks
method to dispatch theFilterTasks
action.filterTasks(filter: Filter) { this.store.dispatch(new FilterTasks({filter})); }
- Create the task entity reducer and actions using the cli
ng g entity todo/Task --reducers reducers/index.ts --group true
This will setup entity actions and reducer.
- Because all actions realted to the Task entity are also async we will need to setup effects and some additional actions
- in the
todo/actions/task.actions.ts
file add to theTaskActionTypes
enum actions types for fetching, creating, updating, and removing tasks.
export enum TaskActionTypes {
...
FetchTasks="[Task] Fetch Tasks",
CreateTask="[Task] Create Tasks",
PutTask="[Task] Put Tasks",
RemoveTask = "[Task] Remove Task"
}
- Add action classes for every type
export class FetchTasks implements Action {
readonly type = TaskActionTypes.FetchTasks;
constructor(public payload: {filter:Filter}){}
}
export class CreateTask implements Action {
readonly type = TaskActionTypes.CreateTask;
constructor(public payload: {task:Task}){}
}
export class PutTask implements Action {
readonly type = TaskActionTypes.PutTask;
constructor(public payload: {task:Task}){}
}
export class RemoveTask implements Action {
readonly type = TaskActionTypes.RemoveTask;
constructor(public payload: {id:number}){}
}
- Inside
todo/reducers/index.ts
create selectors for selecting the Tasks, by using theselectAll
selector from the entity adapter.
export const getTask = createSelector( getTasks, (state) => state.task );
export const getTaskEntities = createSelector(getTask, fromTask.selectAll);
- create an Effects service for the tasks actions
ng g effect todo/Task -m todo/todo.module.ts --group true
- Inject to the consturctor the
TaskService
;
constructor(private actions$: Actions, private taskService: TaskService) {}
- create an Effect for fetching task that should listen to the
FetchTasks
action and after fetching the tasks should dispatch aLoadTasks
action.
@Effect()
tasks$ = this.actions$.pipe(
ofType<FetchTasks>(TaskActionTypes.FetchTasks),
switchMap(action => {
if (action.payload.filter !== Filter.ALL) {
return this.taskService.searchTasks(
action.payload.filter === Filter.COMPLETED
);
}
return this.taskService.getTasks();
}),
map(tasks => new LoadTasks({ tasks }))
);
- Create an effect for upserting tasks that will listen to the
CreateTask
andPutTask
. at the end the effect should dispatch theUpsertTask
action.@Effect() upsertTask$ = this.actions$ .pipe( ofType<CreateTask | PutTask>( TaskActionTypes.CreateTask, TaskActionTypes.PutTask ), switchMap(action => { if (action.type === TaskActionTypes.CreateTask) { return this.taskService.addTask(action.payload.task.title); } else { return this.taskService.updateTask(action.payload.task); } }), map(task => new UpsertTask({ task })) );
- Create an effect for removing tasks the will listen to the
RemoveTask
action and at the end should dispatch aDeleteTask
action.@Effect() removeTask$ = this.actions$ .pipe( ofType<RemoveTask>(TaskActionTypes.RemoveTask), switchMap(action => this.taskService.deleteTask(action.payload.id)), map(id => new DeleteTask({ id: id.toString() }) ) )
- In the
src/App.module.ts
and to the imports anEffectsModule.forRoot
import.
@NgModule({
declarations: [
AppComponent,
ThemeContainerComponent
],
imports: [
BrowserModule,
HttpClientModule,
HttpClientInMemoryWebApiModule.forRoot(TaskdbService,{put204:false, delete404:false}),
EffectsModule.forRoot([]),
StoreModule.forRoot(reducers, { metaReducers }),
!environment.production ? StoreDevtoolsModule.instrument() : [],
SharedModule,
TodoModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
- Inside the
todo/todo-container/todo-container.component.ts
add the Store service to the component. - Update the
fetchTasks
method to only dispatch afetchTasks
actionthis.store.dispatch(new FetchTasks({ filter }));
- Update the
submitTask
,removeTask
andtoggleTask
method to only dispatch actions.
fetchTasks(filter: Filter){
this.store.dispatch(new FetchTasks({ filter }));
}
submitTask(title: string) {
this.store.dispatch(new CreateTask( {task:new Task(null, title)}));
}
removeTask(task: Task) {
this.store.dispatch(new RemoveTask({id:task.id}));
}
toggleTask(task: Task) {
this.store.dispatch(new PutTask({task}));
}
- Change to
tasks
property to an Observable ofTask[]
tasks$: Observable<Task[]>;
- Inside the
ngOnInit
initialize it by selecting it from the state
ngOnInit() {
this.tasks$ = this.store.select(getTaskEntities);
this.currentFilter$ = this.store.select( getCurrentFilter )
.pipe(
tap( (filter) => this.fetchTasks(filter))
);
}
- Inside the
todo/todo-container/todo-container.component.html
update the tasks binding
<app-tasks
[todos]="tasks$ | async"
[filter]="currentFilter$ | async"
(taskFiltered)="filterTasks($event)"
(taskSubmitted)="submitTask($event)"
(taskToggle)="toggleTask($event)"
(taskDeleted)="removeTask($event)"
>
</app-tasks>