-
Notifications
You must be signed in to change notification settings - Fork 155
Reactive Workshop Steps
Lukas Ruebbelke edited this page Oct 24, 2018
·
2 revisions
import { Project } from './../../projects/project.model';
const initialProjects: Project[] = [
{
id: '1',
title: 'Project One',
details: 'This is a sample project',
percentComplete: 20,
approved: false,
customerId: null
},
{
id: '2',
title: 'Project Two',
details: 'This is a sample project',
percentComplete: 40,
approved: false,
customerId: null
},
{
id: '3',
title: 'Project Three',
details: 'This is a sample project',
percentComplete: 100,
approved: true,
customerId: null
}
];
const createProject = (projects, project) => [...projects, project];
const updateProject = (projects, project) => projects.map(p => {
return p.id === project.id ? Object.assign({}, project) : p;
});
const deleteProject = (projects, project) => projects.filter(w => project.id !== w.id);
// Reducer
export interface ProjectsState {
selectedProjectId: string | null;
projects: Project[];
}
export const initialState: ProjectsState = {
selectedProjectId: null,
projects: initialProjects
};
export function projectsReducer(
state = initialState,
action: Action
): ProjectsState {
switch (action.type) {
default:
return state;
}
}
// state/index.ts
import * as fromCustomers from './customers/customers.reducer';
import * as fromProjects from './projects/projects.reducer';
export interface AppState {
customers: fromCustomers.CustomersState,
projects: fromProjects.ProjectsState
}
export const reducers: ActionReducerMap<AppState> = {
customers: fromCustomers.customersReducer,
projects: fromProjects.projectsReducer
};
// common-data/index.ts
export { ProjectsState } from './lib/state/projects/projects.reducer';
// Component
import { Component, OnInit } from '@angular/core';
import { select, Store } from '@ngrx/store';
import { Project, ProjectsService, ProjectsState } from '@workshop/core-data';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export class ProjectsComponent implements OnInit {
projects$: Observable<Project[]>;
projects: Project[];
currentProject: Project;
constructor(
private projectsService: ProjectsService,
private store: Store<ProjectsState>
) {
this.projects$ = store.pipe(
select('projects'),
map((state: ProjectsState) => state.projects)
);
}
}
// Reducer
export function projectsReducer(
state = initialState,
action: Action
): ProjectsState {
switch (action.type) {
case 'select':
return {
selectedProjectId: action.payload.id,
projects: state.projects
}
case 'create':
return {
selectedProjectId: state.selectedProjectId,
projects: createProject(state.projects, action.payload)
}
case 'update':
return {
selectedProjectId: state.selectedProjectId,
projects: updateProject(state.projects, action.payload)
}
case 'delete':
return {
selectedProjectId: state.selectedProjectId,
projects: deleteProject(state.projects, action.payload)
}
default:
return state;
}
}
// Component
export class ProjectsComponent implements OnInit {
projects$: Observable<Project[]>;
currentProject: Project;
constructor(
private store: Store<ProjectsState>
) {
this.projects$ = store.pipe(
select('projects'),
map((state: ProjectsState) => state.projects)
);
}
ngOnInit() {
this.resetCurrentProject();
}
resetCurrentProject() {
this.currentProject = { id: null, name: '', price: 0, description: '' };
}
selectProject(project) {
this.currentProject = project;
}
reset(project) {
this.resetCurrentProject();
}
getProjects() {
// Pending
}
saveProject(project) {
if (!project.id) {
this.createProject(project);
} else {
this.updateProject(project);
}
}
createProject(project) {
this.store.dispatch({type: 'create', payload: project});
this.resetCurrentProject();
}
updateProject(project) {
this.store.dispatch({type: 'update', payload: project});
this.resetCurrentProject();
}
deleteProject(project) {
this.store.dispatch({type: 'delete', payload: project});
this.resetCurrentProject();
}
}
// Actions
import { Project } from '@workshop/core-data';
import { Action } from '@ngrx/store';
export enum ProjectsActionTypes {
ProjectSelected = '[Projects] Selected',
AddProject = '[Projects] Add Data',
UpdateProject = '[Projects] Update Data',
DeleteProject = '[Projects] Delete Data'
}
export class SelectProject implements Action {
readonly type = ProjectsActionTypes.ProjectSelected;
constructor(public payload) {}
}
export class AddProject implements Action {
readonly type = ProjectsActionTypes.AddProject;
constructor(public payload: Project) {}
}
export class UpdateProject implements Action {
readonly type = ProjectsActionTypes.UpdateProject;
constructor(public payload: Project) {}
}
export class DeleteProject implements Action {
readonly type = ProjectsActionTypes.DeleteProject;
constructor(public payload: Project) {}
}
export type ProjectsActions = SelectProject
| AddProject
| UpdateProject
| DeleteProject
;
// common-data/index.ts
export { SelectProject, AddProject, UpdateProject, DeleteProject } from './lib/state/projects/projects.actions';
// Reducer
export function projectsReducer(
state = initialState,
action: ProjectsActions
): ProjectsState {
switch (action.type) {
case ProjectsActionTypes.ProjectSelected:
return {
selectedProjectId: action.payload,
projects: state.projects
}
case ProjectsActionTypes.AddProject:
return {
selectedProjectId: state.selectedProjectId,
projects: createProject(state.projects, action.payload)
}
case ProjectsActionTypes.UpdateProject:
return {
selectedProjectId: state.selectedProjectId,
projects: updateProject(state.projects, action.payload)
}
case ProjectsActionTypes.DeleteProject:
return {
selectedProjectId: state.selectedProjectId,
projects: deleteProject(state.projects, action.payload)
}
default:
return state;
}
}
// Component
export class ProjectsComponent implements OnInit {
// ...
createProject(project) {
this.store.dispatch(new AddProject(project));
this.resetCurrentProject();
}
updateProject(project) {
this.store.dispatch(new UpdateProject(project));
this.resetCurrentProject();
}
deleteProject(project) {
this.store.dispatch(new DeleteProject(project));
this.resetCurrentProject();
}
}
// Actions
export enum ProjectsActionTypes {
ProjectSelected = '[Projects] Selected',
LoadProjects = '[Projects] Load Data',
AddProject = '[Projects] Add Data',
UpdateProject = '[Projects] Update Data',
DeleteProject = '[Projects] Delete Data'
}
export class SelectProject implements Action {
readonly type = ProjectsActionTypes.ProjectSelected;
constructor(public payload) {}
}
export type ProjectsActions = SelectProject
| LoadProjects
| AddProject
| UpdateProject
| DeleteProject;
// Reducer
export interface ProjectsState extends EntityState<Project> {
selectedProjectId: string | null;
}
export const adapter: EntityAdapter<Project> = createEntityAdapter<Project>();
export const initialState: ProjectsState = adapter.getInitialState({
selectedProjectId: null
});
export function projectsReducer(
state = initialState,
action: ProjectsActions
): ProjectsState {
switch (action.type) {
case ProjectsActionTypes.ProjectSelected:
return Object.assign({}, state, { selectedProjectId: action.payload});
case ProjectsActionTypes.LoadProjects:
return adapter.addAll(action.payload, state);
case ProjectsActionTypes.AddProject:
return adapter.addOne(action.payload, state);
case ProjectsActionTypes.UpdateProject:
return adapter.upsertOne(action.payload, state);
case ProjectsActionTypes.DeleteProject:
return adapter.removeOne(action.payload.id, state);
default:
return state;
}
}
// common-data/index.ts
export { initialProjects, ProjectsState } from './lib/state/projects/projects.reducer';
// Component
export class ProjectsComponent implements OnInit {
projects$: Observable<Project[]>;
currentProject: Project;
constructor(
private store: Store<ProjectsState>
) {
this.projects$ = store.pipe(
select('projects'),
map(data => data.entities),
map(data => Object.keys(data).map(k => data[k]))
);
}
ngOnInit() {
this.getProjects();
this.resetCurrentProject();
}
//...
getProjects() {
this.store.dispatch(new LoadProjects(initialProjects));
}
//...
}
// Reducer
export const getSelectedProjectId = (state: ProjectsState) => state.selectedProjectId;
// get the selectors
const { selectIds, selectEntities, selectAll } = adapter.getSelectors();
export const selectProjectIds = selectIds;
export const selectProjectEntities = selectEntities;
export const selectAllProjects = selectAll;
// state/index.ts
export const selectProjectsState = createFeatureSelector<fromProjects.ProjectsState>('projects');
export const selectProjectIds = createSelector(
selectProjectsState,
fromProjects.selectProjectIds
)
export const selectProjectEntities = createSelector(
selectProjectsState,
fromProjects.selectProjectEntities
)
export const selectAllProjects = createSelector(
selectProjectsState,
fromProjects.selectAllProjects
)
// common-data/index.ts
export { selectAllProjects } from './lib/state';
// Component
export class ProjectsComponent implements OnInit {
constructor(
private store: Store<ProjectsState>
) {
this.projects$ = store.pipe(select(selectAllProjects));
}
}
// HELPFUL SNIPPET
import { Injectable } from '@angular/core';
import { Actions, Effect } from '@ngrx/effects';
import { DataPersistence } from '@nrwl/nx';
import { map } from 'rxjs/operators';
import { Project } from './../../core/projects/project.model';
import { ProjectsService } from './../../core/projects/projects.service';
import { ProjectsActionTypes } from './projects.actions';
import { ProjectsState } from './projects.reducer';
@Injectable({providedIn: 'root'})
export class ProjectsEffects {
constructor(
private actions$: Actions,
private dataPersistence: DataPersistence<ProjectsState>,
private projectsService: ProjectsService
) {}
}
// Actions
export enum ProjectsActionTypes {
ProjectSelected = '[Projects] Selected',
LoadProjects = '[Projects] Load Data',
ProjectsLoaded = '[Projects] Data Loaded',
AddProject = '[Projects] Add Data',
ProjectAdded = '[Projects] Data Added',
UpdateProject = '[Projects] Update Data',
DeleteProject = '[Projects] Delete Data'
}
export class ProjectsLoaded implements Action {
readonly type = ProjectsActionTypes.ProjectsLoaded;
constructor(public payload: Project[]) {}
}
export class ProjectAdded implements Action {
readonly type = ProjectsActionTypes.ProjectAdded;
constructor(public payload: Project) {}
}
export type ProjectsActions = SelectProject
| LoadProjects
| ProjectsLoaded
| AddProject
| ProjectAdded
| UpdateProject
| DeleteProject;
// Effects
export class ProjectsEffects {
@Effect()
loadProjects$ = this.dataPersistence.fetch(ProjectsActionTypes.LoadProjects, {
run: (action: LoadProjects, state: ProjectsState) => {
return this.projectsService.all().pipe(map((res: Project[]) => new ProjectsLoaded(res)))
},
onError: (action: LoadProjects, error) => {
console.error('Error', error);
}
});
@Effect()
addProject$ = this.dataPersistence.pessimisticUpdate(ProjectsActionTypes.AddProject, {
run: (action: AddProject, state: ProjectsState) => {
return this.projectsService.create(action.payload).pipe(map((res: Project) => new ProjectAdded(res)))
},
onError: (action: AddProject, error) => {
console.error('Error', error);
}
});
constructor(
private actions$: Actions,
private dataPersistence: DataPersistence<ProjectsState>,
private projectsService: ProjectsService
) {}
}
// Module
@NgModule({
imports: [
CommonModule,
NxModule.forRoot(),
StoreModule.forRoot(reducers),
StoreDevtoolsModule.instrument({ maxAge: 5 }),
EffectsModule.forRoot([CustomersEffects, ProjectsEffects]),
],
declarations: []
})
// Reducer
export function projectsReducer(
state = initialState,
action: ProjectsActions
): ProjectsState {
switch (action.type) {
//...
case ProjectsActionTypes.ProjectsLoaded:
return adapter.addAll(action.payload, state);
case ProjectsActionTypes.ProjectAdded:
return adapter.addOne(action.payload, state);
//...
default:
return state;
}
}
// Components
getProjects() {
this.store.dispatch(new LoadProjects());
}
// Actions
import { Project } from '@workshop/core-data';
import { Action } from '@ngrx/store';
export enum ProjectsActionTypes {
ProjectSelected = '[Projects] Selected',
LoadProjects = '[Projects] Load Data',
ProjectsLoaded = '[Projects] Data Loaded',
AddProject = '[Projects] Add Data',
ProjectAdded = '[Projects] Data Added',
UpdateProject = '[Projects] Update Data',
ProjectUpdated = '[Projects] Data Updated',
DeleteProject = '[Projects] Delete Data',
ProjectDeleted = '[Projects] Delete Data'
}
export class SelectProject implements Action {
readonly type = ProjectsActionTypes.ProjectSelected;
constructor(public payload) { }
}
export class LoadProjects implements Action {
readonly type = ProjectsActionTypes.LoadProjects;
}
export class ProjectsLoaded implements Action {
readonly type = ProjectsActionTypes.ProjectsLoaded;
constructor(public payload: Project[]) { }
}
export class AddProject implements Action {
readonly type = ProjectsActionTypes.AddProject;
constructor(public payload: Project) { }
}
export class ProjectAdded implements Action {
readonly type = ProjectsActionTypes.ProjectAdded;
constructor(public payload: Project) { }
}
export class UpdateProject implements Action {
readonly type = ProjectsActionTypes.UpdateProject;
constructor(public payload: Project) { }
}
export class ProjectUpdated implements Action {
readonly type = ProjectsActionTypes.ProjectUpdated;
constructor(public payload: Project) { }
}
export class DeleteProject implements Action {
readonly type = ProjectsActionTypes.DeleteProject;
constructor(public payload: Project) { }
}
export class ProjectDeleted implements Action {
readonly type = ProjectsActionTypes.ProjectDeleted;
constructor(public payload: Project) { }
}
export type ProjectsActions = SelectProject
| LoadProjects
| ProjectsLoaded
| AddProject
| ProjectAdded
| UpdateProject
| ProjectUpdated
| DeleteProject
| ProjectDeleted
;
// Effects
import { Injectable } from '@angular/core';
import { Actions, Effect } from '@ngrx/effects';
import { DataPersistence } from '@nrwl/nx';
import { map } from 'rxjs/operators';
import { Project } from './../../core/projects/project.model';
import { ProjectsService } from './../../core/projects/projects.service';
import {
AddProject,
DeleteProject,
LoadProjects,
UpdateProject,
ProjectAdded,
ProjectDeleted,
ProjectsActionTypes,
ProjectsLoaded,
ProjectUpdated,
} from './projects.actions';
import { ProjectsState } from './projects.reducer';
@Injectable({providedIn: 'root'})
export class ProjectsEffects {
@Effect()
loadProjects$ = this.dataPersistence.fetch(ProjectsActionTypes.LoadProjects, {
run: (action: LoadProjects, state: ProjectsState) => {
return this.projectsService.all().pipe(map((res: Project[]) => new ProjectsLoaded(res)))
},
onError: (action: LoadProjects, error) => {
console.error('Error', error);
}
});
@Effect()
addProject$ = this.dataPersistence.pessimisticUpdate(ProjectsActionTypes.AddProject, {
run: (action: AddProject, state: ProjectsState) => {
return this.projectsService.create(action.payload).pipe(map((res: Project) => new ProjectAdded(res)))
},
onError: (action: AddProject, error) => {
console.error('Error', error);
}
});
@Effect()
updateProject$ = this.dataPersistence.pessimisticUpdate(ProjectsActionTypes.UpdateProject, {
run: (action: UpdateProject, state: ProjectsState) => {
return this.projectsService.update(action.payload).pipe(map((res: Project) => new ProjectUpdated(res)))
},
onError: (action: UpdateProject, error) => {
console.error('Error', error);
}
});
@Effect()
deleteProject$ = this.dataPersistence.pessimisticUpdate(ProjectsActionTypes.DeleteProject, {
run: (action: DeleteProject, state: ProjectsState) => {
return this.projectsService.delete(action.payload).pipe(map((res: Project) => new ProjectDeleted(res)))
},
onError: (action: DeleteProject, error) => {
console.error('Error', error);
}
});
constructor(
private actions$: Actions,
private dataPersistence: DataPersistence<ProjectsState>,
private projectsService: ProjectsService
) {}
}
// state/index.ts
export const selectCurrentProjectId = createSelector(
selectProjectsState,
fromProjects.getSelectedProjectId
);
export const selectCurrentProject = createSelector(
selectProjectEntities,
selectCurrentProjectId,
(projectEntities, projectId) => {
return projectId ? projectEntities[projectId] : emptyProject;
}
);
// common-date/index.ts
export { selectAllProjects, selectCurrentProject } from './lib/state';
// Component
export class ProjectsComponent implements OnInit {
projects$: Observable<Project[]>;
currentProject$: Observable<Project>;
constructor(
private store: Store<ProjectsState>
) {
this.projects$ = store.pipe(select(selectAllProjects));
this.currentProject$ = store.pipe(select(selectCurrentProject));
}
ngOnInit() {
this.getProjects();
this.resetCurrentProject();
}
resetCurrentProject() {
this.selectProject(null);
}
selectProject(project) {
this.store.dispatch(new SelectProject(project.id));
}
//...
}
// HELPFUL SNIPPET
import { Injectable } from '@angular/core';
import { ActionsSubject, select, Store } from '@ngrx/store';
import { filter } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class ProjectsFacade {
}
// Facade
import { Injectable } from '@angular/core';
import { select, Store } from '@ngrx/store';
import { selectAllProjects, selectCurrentProject } from './..';
import * as ProjectsActions from './projects.actions';
import { ProjectsState } from './projects.reducer';
export class ProjectsFacade {
allProjects$ = this.store.pipe(select(selectAllProjects));
currentProject$ = this.store.pipe(select(selectCurrentProject));
constructor(private store: Store<ProjectsState>) { }
selectProject(projectId) {
this.store.dispatch(new ProjectsActions.SelectProject(projectId));
}
loadAll() {
this.store.dispatch(new ProjectsActions.LoadProjects());
}
createProject(project) {
this.store.dispatch(new ProjectsActions.AddProject(project));
}
updateProject(project) {
this.store.dispatch(new ProjectsActions.UpdateProject(project));
}
deleteProject(project) {
this.store.dispatch(new ProjectsActions.DeleteProject(project));
}
}
// Component
export class ProjectsComponent implements OnInit {
projects$: Observable<Project[]>;
currentProject$: Observable<Project>;
constructor(
private facade: ProjectsFacade
) {
this.projects$ = facade.allProjects$
this.currentProject$ = facade.currentProject$
}
ngOnInit() {
this.getProjects();
this.resetCurrentProject();
}
resetCurrentProject() {
this.selectProject({id: null});
}
selectProject(project) {
this.facade.selectProject(project.id);
}
reset(project) {
this.resetCurrentProject();
}
getProjects() {
this.facade.loadAll();
}
saveProject(project) {
if (!project.id) {
this.createProject(project);
} else {
this.updateProject(project);
}
}
createProject(project) {
this.facade.createProject(project);
}
updateProject(project) {
this.facade.updateProject(project);
}
deleteProject(project) {
this.facade.deleteProject(project.id);
}
}
// Facade
export class ProjectsFacade {
allProjects$ = this.store.pipe(select(selectAllProjects));
currentProject$ = this.store.pipe(select(selectCurrentProject));
mutations$ = this.actions$.pipe(
filter(action =>
action.type === ProjectsActionTypes.AddProject
|| action.type === ProjectsActionTypes.UpdateProject
|| action.type === ProjectsActionTypes.DeleteProject
)
);
constructor(private store: Store<ProjectsState>, private actions$: ActionsSubject) { }
}
// Component
@Component({
selector: 'app-projects',
templateUrl: './projects.component.html',
styleUrls: ['./projects.component.scss']
})
export class ProjectsComponent implements OnInit {
customers$: Observable<Customer[]> = this.customersFacade.allCustomers$;
projects$: Observable<Project[]> = this.projectsFacade.allProjects$;
currentProject$: Observable<Project> = this.projectsFacade.currentProject$;
constructor(
private projectsFacade: ProjectsFacade,
private customersFacade: CustomersFacade
) { }
ngOnInit() {
this.customersFacade.loadCustomers();
this.projectsFacade.loadProjects();
this.projectsFacade.mutations$.subscribe(_ => this.resetCurrentProject());
this.resetCurrentProject();
}
resetCurrentProject() {
this.selectProject({id: null});
}
selectProject(project) {
this.projectsFacade.selectProject(project.id);
}
saveProject(project) {
if (!project.id) {
this.projectsFacade.addProject(project);
} else {
this.projectsFacade.updateProject(project);
}
}
deleteProject(project) {
this.projectsFacade.deleteProject(project);
}
}