Skip to content

Commit 60700dd

Browse files
committed
reading activities from API and introducing ngrx #63
1 parent b2f3cd0 commit 60700dd

16 files changed

+257
-42
lines changed

package-lock.json

Lines changed: 19 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
"@angular/platform-browser": "~9.0.3",
2121
"@angular/platform-browser-dynamic": "~9.0.3",
2222
"@angular/router": "~9.0.3",
23+
"@ngrx/effects": "^9.0.0",
24+
"@ngrx/store": "^9.0.0",
25+
"@ngrx/store-devtools": "^9.0.0",
2326
"bootstrap": "^4.4.1",
2427
"jquery": "^3.4.1",
2528
"minimist": "^1.2.5",

src/app/app.module.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ import { SearchProjectComponent } from './modules/shared/components/search-proje
2929
import { HomeComponent } from './modules/home/home.component';
3030
import { LoginComponent } from './modules/login/login.component';
3131

32+
import { StoreModule } from '@ngrx/store';
33+
import { EffectsModule } from '@ngrx/effects';
34+
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
35+
36+
import { ActivityEffects } from './modules/activities-management/store/activity-management.effects';
37+
import { activityManagementReducer } from './modules/activities-management/store';
38+
3239
@NgModule({
3340
declarations: [
3441
AppComponent,
@@ -56,7 +63,19 @@ import { LoginComponent } from './modules/login/login.component';
5663
FilterProjectPipe,
5764
SearchProjectComponent,
5865
],
59-
imports: [CommonModule, BrowserModule, AppRoutingModule, FormsModule, ReactiveFormsModule, HttpClientModule],
66+
imports: [
67+
CommonModule,
68+
BrowserModule,
69+
AppRoutingModule,
70+
FormsModule,
71+
ReactiveFormsModule,
72+
HttpClientModule,
73+
StoreModule.forRoot({ activities: activityManagementReducer }),
74+
EffectsModule.forRoot([ActivityEffects]),
75+
StoreDevtoolsModule.instrument({
76+
maxAge: 15, // Retains last 15 states
77+
}),
78+
],
6079
providers: [],
6180
bootstrap: [AppComponent],
6281
})

src/app/modules/activities-management/components/activity-list/activity-list.component.spec.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,31 @@
1+
import { allActivities } from './../../store/activity-management.selectors';
2+
import { Activity } from './../../../shared/models/activity.model';
3+
import { ActivityState } from './../../store/activity-management.reducers';
14
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
25

6+
import { provideMockStore, MockStore } from '@ngrx/store/testing';
37
import { ActivityListComponent } from './activity-list.component';
8+
import { MemoizedSelector, State } from '@ngrx/store';
49

510
describe('ActivityListComponent', () => {
611
let component: ActivityListComponent;
712
let fixture: ComponentFixture<ActivityListComponent>;
13+
let mockActivitiesSelector;
14+
15+
const state = { data: [{id: 'id', name: 'name', description: 'description'}], isLoading: false, message: '' };
16+
17+
let store: MockStore<ActivityState>;
818

919
beforeEach(async(() => {
1020
TestBed.configureTestingModule({
11-
declarations: [ ActivityListComponent ]
21+
declarations: [ ActivityListComponent ],
22+
providers: [ provideMockStore({ initialState: state }) ]
1223
})
1324
.compileComponents();
25+
26+
store = TestBed.inject(MockStore);
27+
28+
mockActivitiesSelector = store.overrideSelector( allActivities, state );
1429
}));
1530

1631
beforeEach(() => {
@@ -22,4 +37,21 @@ describe('ActivityListComponent', () => {
2237
it('should create', () => {
2338
expect(component).toBeTruthy();
2439
});
40+
41+
it('onInit, LoadActivities action is dispatched', () => {
42+
spyOn(store, 'dispatch');
43+
44+
component.ngOnInit();
45+
46+
expect(store.dispatch).toHaveBeenCalled();
47+
});
48+
49+
it('onInit, activities field is populated with data from store', () => {
50+
component.ngOnInit();
51+
52+
expect(component.activities).toBe(state.data);
53+
});
54+
55+
afterEach(() => { fixture.destroy(); });
56+
2557
});
Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,27 @@
1-
import { Input } from '@angular/core';
2-
import { Component } from '@angular/core';
3-
import { Activity } from '../../../shared/models';
1+
import { LoadActivities } from './../../store/activity-management.actions';
2+
import { ActivityState } from './../../store/activity-management.reducers';
3+
import {Input, OnInit} from '@angular/core';
4+
import {Component} from '@angular/core';
5+
import {Activity} from '../../../shared/models';
6+
import { Store, select } from '@ngrx/store';
7+
import { allActivities } from '../../store';
48

5-
@Component({
6-
selector: 'app-activity-list',
7-
templateUrl: './activity-list.component.html',
8-
styleUrls: ['./activity-list.component.scss']
9-
})
10-
export class ActivityListComponent {
9+
@Component({selector: 'app-activity-list', templateUrl: './activity-list.component.html', styleUrls: ['./activity-list.component.scss']})
10+
export class ActivityListComponent implements OnInit {
1111

12-
@Input() activities: Activity[] = [];
12+
@Input()activities: Activity[] = [];
13+
public isLoading: boolean;
1314

14-
constructor() { }
15+
constructor(private store: Store<ActivityState>) { }
16+
17+
ngOnInit() {
18+
this.store.dispatch(new LoadActivities());
19+
const activities$ = this.store.pipe(select(allActivities));
20+
21+
activities$.subscribe(response => {
22+
this.isLoading = response.isLoading;
23+
this.activities = response.data;
24+
});
25+
}
1526

1627
}

src/app/modules/activities-management/services/activity.service.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,15 @@ describe('Activity Service', () => {
2828
expect(httpClient).toBeTruthy();
2929
}));
3030

31-
it('activities are read using GET from assets/activities.json URL', () => {
31+
it('activities are read using GET from baseUrl', () => {
3232
const activitiesFoundSize = activities.length;
33+
service.baseUrl = 'foo';
3334
service
3435
.getActivities()
3536
.subscribe(activitiesInResponse => {
3637
expect(activitiesInResponse.length).toBe(activitiesFoundSize);
3738
});
38-
const getActivitiesRequest = httpMock.expectOne('assets/activities.json');
39+
const getActivitiesRequest = httpMock.expectOne(service.baseUrl);
3940
expect(getActivitiesRequest.request.method).toBe('GET');
4041
getActivitiesRequest.flush(activities);
4142
});

src/app/modules/activities-management/services/activity.service.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { environment } from './../../../../environments/environment';
12
import { Injectable } from '@angular/core';
23
import { HttpClient } from '@angular/common/http';
34
import { Observable } from 'rxjs';
@@ -8,11 +9,11 @@ import { Activity } from '../../shared/models';
89
})
910
export class ActivityService {
1011

11-
url = 'assets/activities.json';
12+
baseUrl = `${environment.timeTrackerApiUrl}/activities`;
1213

1314
constructor(private http: HttpClient) {}
1415

1516
getActivities(): Observable<Activity[]> {
16-
return this.http.get<Activity[]>(this.url);
17+
return this.http.get<Activity[]>(this.baseUrl);
1718
}
1819
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { LoadActivitiesFail } from './activity-management.actions';
2+
import { LoadActivitiesSuccess, ActivityManagementActionTypes } from './activity-management.actions';
3+
describe('LoadActivitiesSuccess', () => {
4+
5+
it('LoadActivitiesSuccess type is ActivityManagementActionTypes.LoadActivitiesSuccess', () => {
6+
const loadActivitiesSuccess = new LoadActivitiesSuccess([]);
7+
expect(loadActivitiesSuccess.type).toEqual(ActivityManagementActionTypes.LoadActivitiesSuccess);
8+
});
9+
10+
it('LoadActivitiesFail type is ActivityManagementActionTypes.LoadActivitiesFail', () => {
11+
const loadActivitiesFail = new LoadActivitiesFail('error');
12+
expect(loadActivitiesFail.type).toEqual(ActivityManagementActionTypes.LoadActivitiesFail);
13+
});
14+
15+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Activity } from './../../shared/models/activity.model';
2+
import { Action } from '@ngrx/store';
3+
4+
export enum ActivityManagementActionTypes {
5+
LoadActivities = '[ActivityManagement] Load Activities',
6+
LoadActivitiesSuccess = '[ActivityManagement] Load Activities Successs',
7+
LoadActivitiesFail = '[ActivityManagement] Load Activities Fail',
8+
}
9+
10+
11+
export class LoadActivities implements Action {
12+
public readonly type = ActivityManagementActionTypes.LoadActivities;
13+
}
14+
15+
export class LoadActivitiesSuccess implements Action {
16+
public readonly type = ActivityManagementActionTypes.LoadActivitiesSuccess;
17+
18+
constructor(public payload: Activity[]) { }
19+
}
20+
21+
export class LoadActivitiesFail implements Action {
22+
public readonly type = ActivityManagementActionTypes.LoadActivitiesFail;
23+
24+
constructor(public error) { }
25+
}
26+
27+
export type ActivityManagementActions = LoadActivities | LoadActivitiesSuccess | LoadActivitiesFail;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { ActivityManagementActionTypes, LoadActivitiesSuccess, LoadActivitiesFail } from './activity-management.actions';
2+
import { Activity } from './../../shared/models/activity.model';
3+
import { ActivityService } from './../services/activity.service';
4+
import { Injectable } from '@angular/core';
5+
import { Actions, Effect, ofType } from '@ngrx/effects';
6+
import { Observable, of } from 'rxjs';
7+
import { Action } from '@ngrx/store';
8+
import { catchError, map, mergeMap } from 'rxjs/operators';
9+
10+
@Injectable()
11+
export class ActivityEffects {
12+
13+
constructor(private actions$: Actions, private activityService: ActivityService) { }
14+
15+
@Effect()
16+
getActivities$: Observable<Action> = this.actions$.pipe(
17+
ofType(ActivityManagementActionTypes.LoadActivities),
18+
mergeMap(() =>
19+
this.activityService.getActivities().pipe(
20+
map((activities: Activity[]) => {
21+
return new LoadActivitiesSuccess(activities);
22+
}),
23+
catchError((error) =>
24+
of(new LoadActivitiesFail(error)))
25+
)
26+
));
27+
}

0 commit comments

Comments
 (0)