Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(example): add examples of effects not based on the Actions stream #1845

Merged
merged 2 commits into from
May 21, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion projects/example-app/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ export const routes: Routes = [
loadChildren: '@example-app/books/books.module#BooksModule',
canActivate: [AuthGuard],
},
{ path: '**', component: NotFoundPageComponent },
{
path: '**',
component: NotFoundPageComponent,
data: { title: 'Not found' },
},
];

@NgModule({
Expand Down
3 changes: 2 additions & 1 deletion projects/example-app/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { ROOT_REDUCERS, metaReducers } from '@example-app/reducers';

import { AppComponent } from '@example-app/core/containers/app.component';
import { AppRoutingModule } from '@example-app/app-routing.module';
import { UserEffects, RouterEffects } from '@example-app/core/effects';

@NgModule({
imports: [
Expand Down Expand Up @@ -69,7 +70,7 @@ import { AppRoutingModule } from '@example-app/app-routing.module';
*
* See: https://ngrx.io/guide/effects#registering-root-effects
*/
EffectsModule.forRoot([]),
EffectsModule.forRoot([UserEffects, RouterEffects]),

CoreModule,
],
Expand Down
4 changes: 3 additions & 1 deletion projects/example-app/src/app/auth/auth-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { LoginPageComponent } from '@example-app/auth/containers';

const routes: Routes = [{ path: 'login', component: LoginPageComponent }];
const routes: Routes = [
{ path: 'login', component: LoginPageComponent, data: { title: 'Login' } },
];

@NgModule({
imports: [RouterModule.forChild(routes)],
Expand Down
15 changes: 13 additions & 2 deletions projects/example-app/src/app/auth/effects/auth.effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import { Credentials } from '@example-app/auth/models';
import { AuthService } from '@example-app/auth/services';
import { LogoutConfirmationDialogComponent } from '@example-app/auth/components';
import { UserActions } from '@example-app/core/actions';

@Injectable()
export class AuthEffects {
Expand Down Expand Up @@ -60,12 +61,22 @@ export class AuthEffects {

return dialogRef.afterClosed();
}),
map(result =>
result ? AuthActions.logout() : AuthActions.logoutConfirmationDismiss()
map(
result =>
result
? AuthActions.logout()
: AuthActions.logoutConfirmationDismiss()
)
)
);

logoutIdleUser$ = createEffect(() =>
this.actions$.pipe(
ofType(UserActions.idleTimeout),
map(() => AuthActions.logout())
)
);

constructor(
private actions$: Actions,
private authService: AuthService,
Expand Down
13 changes: 11 additions & 2 deletions projects/example-app/src/app/books/books-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,22 @@ import { CollectionPageComponent } from '@example-app/books/containers/collectio
import { BookExistsGuard } from '@example-app/books/guards/book-exists.guard';

export const routes: Routes = [
{ path: 'find', component: FindBookPageComponent },
{
path: 'find',
component: FindBookPageComponent,
data: { title: 'Find book' },
},
{
path: ':id',
component: ViewBookPageComponent,
canActivate: [BookExistsGuard],
data: { title: 'Book details' },
},
{
path: '',
component: CollectionPageComponent,
data: { title: 'Collection' },
},
{ path: '', component: CollectionPageComponent },
];

@NgModule({
Expand Down
3 changes: 2 additions & 1 deletion projects/example-app/src/app/core/actions/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as LayoutActions from './layout.actions';
import * as UserActions from './user.actions';

export { LayoutActions };
export { LayoutActions, UserActions };
3 changes: 3 additions & 0 deletions projects/example-app/src/app/core/actions/user.actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { createAction } from '@ngrx/store';

export const idleTimeout = createAction('[User] Idle Timeout');
2 changes: 2 additions & 0 deletions projects/example-app/src/app/core/effects/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './user.effects';
export * from './router.effects';
41 changes: 41 additions & 0 deletions projects/example-app/src/app/core/effects/router.effects.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Router, ActivatedRoute, NavigationEnd } from '@angular/router';
import { Title } from '@angular/platform-browser';
import { TestBed } from '@angular/core/testing';

import { of } from 'rxjs';

import { RouterEffects } from '@example-app/core/effects';

describe('RouterEffects', () => {
let effects: RouterEffects;
let titleService: Title;

beforeEach(() => {
TestBed.configureTestingModule({
providers: [
RouterEffects,
{
provide: Router,
useValue: { events: of(new NavigationEnd(1, '', '')) },
},
{
provide: ActivatedRoute,
useValue: { data: of({ title: 'Search' }) },
},
{ provide: Title, useValue: { setTitle: jest.fn() } },
],
});

effects = TestBed.get(RouterEffects);
titleService = TestBed.get(Title);
});

describe('updateTitle$', () => {
it('should update the title on router navigation', () => {
effects.updateTitle$.subscribe();
expect(titleService.setTitle).toHaveBeenCalledWith(
'Book Collection - Search'
);
});
});
});
34 changes: 34 additions & 0 deletions projects/example-app/src/app/core/effects/router.effects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Injectable } from '@angular/core';
import { Router, NavigationEnd, ActivatedRoute } from '@angular/router';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move all @angular imports to the top of the file, following by rxjs and @ngrx

import { Title } from '@angular/platform-browser';

import { tap, filter, map, mergeMap } from 'rxjs/operators';

import { createEffect } from '@ngrx/effects';

@Injectable()
export class RouterEffects {
updateTitle$ = createEffect(
() =>
this.router.events.pipe(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have store-router package installed in the example app. Wouldn't that be better to listen to those changes?
@brandonroberts

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about that when I was writing the code :) The idea was to show how one would create effects not based on the Actions stream, so I went for this instead.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these are good for what we asked for

filter(event => event instanceof NavigationEnd),
map(() => {
let route = this.activatedRoute;
while (route.firstChild) route = route.firstChild;
return route;
}),
mergeMap(route => route.data),
map(data => `Book Collection - ${data['title']}`),
tap(title => this.titleService.setTitle(title))
),
{
dispatch: false,
}
);

constructor(
private router: Router,
private titleService: Title,
private activatedRoute: ActivatedRoute
) {}
}
65 changes: 65 additions & 0 deletions projects/example-app/src/app/core/effects/user.effects.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Action } from '@ngrx/store';
import { TestBed, fakeAsync, tick } from '@angular/core/testing';

import { UserEffects } from '@example-app/core/effects';
import { UserActions } from '@example-app/core/actions';

describe('UserEffects', () => {
let effects: UserEffects;
const eventsMap: { [key: string]: any } = {};

beforeAll(() => {
document.addEventListener = jest.fn((event, cb) => {
eventsMap[event] = cb;
});
});

beforeEach(() => {
TestBed.configureTestingModule({
providers: [UserEffects],
});

effects = TestBed.get(UserEffects);
});

describe('idle$', () => {
it(
'should trigger idleTimeout action after 5 minutes',
fakeAsync(() => {
let action: Action | undefined;
effects.idle$.subscribe(res => (action = res));

// Initial action to trigger the effect
eventsMap['click']();

tick(2 * 60 * 1000);
expect(action).toBeUndefined();

tick(3 * 60 * 1000);
expect(action).toBeDefined();
expect(action!.type).toBe(UserActions.idleTimeout.type);
})
);

it(
'should reset timeout on user activity',
fakeAsync(() => {
let action: Action | undefined;
effects.idle$.subscribe(res => (action = res));

// Initial action to trigger the effect
eventsMap['keydown']();

tick(4 * 60 * 1000);
eventsMap['mousemove']();

tick(4 * 60 * 1000);
expect(action).toBeUndefined();

tick(1 * 60 * 1000);
expect(action).toBeDefined();
expect(action!.type).toBe(UserActions.idleTimeout.type);
})
);
});
});
21 changes: 21 additions & 0 deletions projects/example-app/src/app/core/effects/user.effects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Injectable, Inject } from '@angular/core';

import { fromEvent, merge, timer } from 'rxjs';
import { map, switchMapTo } from 'rxjs/operators';

import { createEffect } from '@ngrx/effects';
import { UserActions } from '@example-app/core/actions';

@Injectable()
export class UserEffects {
clicks$ = fromEvent(document, 'click');
keys$ = fromEvent(document, 'keydown');
mouse$ = fromEvent(document, 'mousemove');

idle$ = createEffect(() =>
merge(this.clicks$, this.keys$, this.mouse$).pipe(
switchMapTo(timer(5 * 60 * 1000)), // 5 minute inactivity timeout
map(() => UserActions.idleTimeout())
)
);
}