Skip to content

Commit ba8d300

Browse files
koumatsumotobrandonroberts
authored andcommitted
feat(example): add logout confirmation (#1287)
Closes #1271
1 parent 466e2cd commit ba8d300

File tree

9 files changed

+215
-5
lines changed

9 files changed

+215
-5
lines changed

projects/example-app/src/app/auth/actions/auth.actions.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export enum AuthActionTypes {
77
LoginSuccess = '[Auth] Login Success',
88
LoginFailure = '[Auth] Login Failure',
99
LoginRedirect = '[Auth] Login Redirect',
10+
LogoutConfirmation = '[Auth] Logout Confirmation',
11+
LogoutConfirmationDismiss = '[Auth] Logout Confirmation Dismiss',
1012
}
1113

1214
export class Login implements Action {
@@ -35,9 +37,19 @@ export class Logout implements Action {
3537
readonly type = AuthActionTypes.Logout;
3638
}
3739

40+
export class LogoutConfirmation implements Action {
41+
readonly type = AuthActionTypes.LogoutConfirmation;
42+
}
43+
44+
export class LogoutConfirmationDismiss implements Action {
45+
readonly type = AuthActionTypes.LogoutConfirmationDismiss;
46+
}
47+
3848
export type AuthActionsUnion =
3949
| Login
4050
| LoginSuccess
4151
| LoginFailure
4252
| LoginRedirect
43-
| Logout;
53+
| Logout
54+
| LogoutConfirmation
55+
| LogoutConfirmationDismiss;

projects/example-app/src/app/auth/auth.module.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,18 @@ import { StoreModule } from '@ngrx/store';
55
import { EffectsModule } from '@ngrx/effects';
66
import { LoginPageComponent } from './containers/login-page.component';
77
import { LoginFormComponent } from './components/login-form.component';
8+
import { LogoutConfirmationDialogComponent } from './components/logout-confirmation-dialog.component';
89

910
import { AuthEffects } from './effects/auth.effects';
1011
import { reducers } from './reducers';
1112
import { MaterialModule } from '../material';
1213
import { AuthRoutingModule } from './auth-routing.module';
1314

14-
export const COMPONENTS = [LoginPageComponent, LoginFormComponent];
15+
export const COMPONENTS = [
16+
LoginPageComponent,
17+
LoginFormComponent,
18+
LogoutConfirmationDialogComponent,
19+
];
1520

1621
@NgModule({
1722
imports: [
@@ -23,5 +28,6 @@ export const COMPONENTS = [LoginPageComponent, LoginFormComponent];
2328
EffectsModule.forFeature([AuthEffects]),
2429
],
2530
declarations: COMPONENTS,
31+
entryComponents: [LogoutConfirmationDialogComponent],
2632
})
2733
export class AuthModule {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`Logout Confirmation Dialog should compile 1`] = `
4+
<ng-component>
5+
<h2
6+
_ngcontent-c0=""
7+
class="mat-dialog-title"
8+
id="mat-dialog-title-0"
9+
mat-dialog-title=""
10+
>
11+
Logout
12+
</h2><mat-dialog-content
13+
_ngcontent-c0=""
14+
class="mat-dialog-content"
15+
>
16+
Are you sure you want to logout?
17+
</mat-dialog-content><mat-dialog-actions
18+
_ngcontent-c0=""
19+
class="mat-dialog-actions"
20+
>
21+
<button
22+
_ngcontent-c0=""
23+
aria-label="Close dialog"
24+
class="mat-button"
25+
mat-button=""
26+
ng-reflect-dialog-result="false"
27+
type="button"
28+
>
29+
<span
30+
class="mat-button-wrapper"
31+
>
32+
Cancel
33+
</span>
34+
<div
35+
class="mat-button-ripple mat-ripple"
36+
matripple=""
37+
ng-reflect-centered="false"
38+
ng-reflect-disabled="false"
39+
ng-reflect-trigger="[object HTMLButtonElement]"
40+
/>
41+
<div
42+
class="mat-button-focus-overlay"
43+
/>
44+
</button>
45+
<button
46+
_ngcontent-c0=""
47+
aria-label="Close dialog"
48+
class="mat-button"
49+
mat-button=""
50+
ng-reflect-dialog-result="true"
51+
type="button"
52+
>
53+
<span
54+
class="mat-button-wrapper"
55+
>
56+
OK
57+
</span>
58+
<div
59+
class="mat-button-ripple mat-ripple"
60+
matripple=""
61+
ng-reflect-centered="false"
62+
ng-reflect-disabled="false"
63+
ng-reflect-trigger="[object HTMLButtonElement]"
64+
/>
65+
<div
66+
class="mat-button-focus-overlay"
67+
/>
68+
</button>
69+
</mat-dialog-actions>
70+
</ng-component>
71+
`;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
import { MatButtonModule, MatDialogModule } from '@angular/material';
3+
import { LogoutConfirmationDialogComponent } from './logout-confirmation-dialog.component';
4+
5+
describe('Logout Confirmation Dialog', () => {
6+
let fixture: ComponentFixture<LogoutConfirmationDialogComponent>;
7+
8+
beforeEach(() => {
9+
TestBed.configureTestingModule({
10+
imports: [MatButtonModule, MatDialogModule],
11+
declarations: [LogoutConfirmationDialogComponent],
12+
});
13+
14+
fixture = TestBed.createComponent(LogoutConfirmationDialogComponent);
15+
});
16+
17+
it('should compile', () => {
18+
fixture.detectChanges();
19+
20+
expect(fixture).toMatchSnapshot();
21+
});
22+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Component } from '@angular/core';
2+
3+
/**
4+
* The dialog will close with true if user clicks the ok button,
5+
* otherwise it will close with undefined.
6+
*/
7+
@Component({
8+
template: `
9+
<h2 mat-dialog-title>Logout</h2>
10+
<mat-dialog-content>Are you sure you want to logout?</mat-dialog-content>
11+
<mat-dialog-actions>
12+
<button mat-button [mat-dialog-close]="false">Cancel</button>
13+
<button mat-button [mat-dialog-close]="true">OK</button>
14+
</mat-dialog-actions>
15+
`,
16+
styles: [
17+
`
18+
:host {
19+
display: block;
20+
width: 100%;
21+
max-width: 300px;
22+
}
23+
24+
mat-dialog-actions {
25+
display: flex;
26+
justify-content: flex-end;
27+
}
28+
29+
[mat-button] {
30+
padding: 0;
31+
}
32+
`,
33+
],
34+
})
35+
export class LogoutConfirmationDialogComponent {}

projects/example-app/src/app/auth/effects/auth.effects.spec.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import { TestBed } from '@angular/core/testing';
2+
import { MatDialog } from '@angular/material';
23
import { Router } from '@angular/router';
34
import { Actions } from '@ngrx/effects';
45
import { provideMockActions } from '@ngrx/effects/testing';
56
import { cold, hot } from 'jasmine-marbles';
6-
import { empty, Observable } from 'rxjs';
7+
import { empty, Observable, of } from 'rxjs';
78
import {
89
Login,
910
LoginFailure,
1011
LoginRedirect,
1112
LoginSuccess,
1213
Logout,
14+
LogoutConfirmation,
15+
LogoutConfirmationDismiss,
1316
} from '../actions/auth.actions';
1417
import { Authenticate, User } from '../models/user';
1518
import { AuthService } from '../services/auth.service';
@@ -20,6 +23,7 @@ describe('AuthEffects', () => {
2023
let authService: any;
2124
let actions$: Observable<any>;
2225
let routerService: any;
26+
let dialog: any;
2327

2428
beforeEach(() => {
2529
TestBed.configureTestingModule({
@@ -34,13 +38,20 @@ describe('AuthEffects', () => {
3438
provide: Router,
3539
useValue: { navigate: jest.fn() },
3640
},
41+
{
42+
provide: MatDialog,
43+
useValue: {
44+
open: jest.fn(),
45+
},
46+
},
3747
],
3848
});
3949

4050
effects = TestBed.get(AuthEffects);
4151
authService = TestBed.get(AuthService);
4252
actions$ = TestBed.get(Actions);
4353
routerService = TestBed.get(Router);
54+
dialog = TestBed.get(MatDialog);
4455

4556
spyOn(routerService, 'navigate').and.callThrough();
4657
});
@@ -109,4 +120,34 @@ describe('AuthEffects', () => {
109120
});
110121
});
111122
});
123+
124+
describe('logoutConfirmation$', () => {
125+
it('should dispatch a Logout action if dialog closes with true result', () => {
126+
const action = new LogoutConfirmation();
127+
const completion = new Logout();
128+
129+
actions$ = hot('-a', { a: action });
130+
const expected = cold('-b', { b: completion });
131+
132+
dialog.open = () => ({
133+
afterClosed: jest.fn(() => of(true)),
134+
});
135+
136+
expect(effects.logoutConfirmation$).toBeObservable(expected);
137+
});
138+
139+
it('should dispatch a LogoutConfirmationDismiss action if dialog closes with falsy result', () => {
140+
const action = new LogoutConfirmation();
141+
const completion = new LogoutConfirmationDismiss();
142+
143+
actions$ = hot('-a', { a: action });
144+
const expected = cold('-b', { b: completion });
145+
146+
dialog.open = () => ({
147+
afterClosed: jest.fn(() => of(false)),
148+
});
149+
150+
expect(effects.logoutConfirmation$).toBeObservable(expected);
151+
});
152+
});
112153
});

projects/example-app/src/app/auth/effects/auth.effects.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Injectable } from '@angular/core';
2+
import { MatDialog } from '@angular/material';
23
import { Router } from '@angular/router';
34
import { Actions, Effect, ofType } from '@ngrx/effects';
45
import { of } from 'rxjs';
@@ -9,9 +10,12 @@ import {
910
Login,
1011
LoginFailure,
1112
LoginSuccess,
13+
Logout,
14+
LogoutConfirmationDismiss,
1215
} from '../actions/auth.actions';
1316
import { Authenticate } from '../models/user';
1417
import { AuthService } from '../services/auth.service';
18+
import { LogoutConfirmationDialogComponent } from '../components/logout-confirmation-dialog.component';
1519

1620
@Injectable()
1721
export class AuthEffects {
@@ -41,9 +45,25 @@ export class AuthEffects {
4145
})
4246
);
4347

48+
@Effect()
49+
logoutConfirmation$ = this.actions$.pipe(
50+
ofType(AuthActionTypes.LogoutConfirmation),
51+
exhaustMap(() => {
52+
const dialogRef = this.dialog.open<
53+
LogoutConfirmationDialogComponent,
54+
undefined,
55+
boolean
56+
>(LogoutConfirmationDialogComponent);
57+
58+
return dialogRef.afterClosed();
59+
}),
60+
map(result => (result ? new Logout() : new LogoutConfirmationDismiss()))
61+
);
62+
4463
constructor(
4564
private actions$: Actions,
4665
private authService: AuthService,
47-
private router: Router
66+
private router: Router,
67+
private dialog: MatDialog
4868
) {}
4969
}

projects/example-app/src/app/core/containers/app.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,6 @@ export class AppComponent {
6464
logout() {
6565
this.closeSidenav();
6666

67-
this.store.dispatch(new AuthActions.Logout());
67+
this.store.dispatch(new AuthActions.LogoutConfirmation());
6868
}
6969
}

projects/example-app/src/app/material/material.module.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
MatIconModule,
1010
MatToolbarModule,
1111
MatProgressSpinnerModule,
12+
MatDialogModule,
1213
} from '@angular/material';
1314

1415
@NgModule({
@@ -21,6 +22,7 @@ import {
2122
MatIconModule,
2223
MatToolbarModule,
2324
MatProgressSpinnerModule,
25+
MatDialogModule,
2426
],
2527
exports: [
2628
MatInputModule,
@@ -31,6 +33,7 @@ import {
3133
MatIconModule,
3234
MatToolbarModule,
3335
MatProgressSpinnerModule,
36+
MatDialogModule,
3437
],
3538
})
3639
export class MaterialModule {}

0 commit comments

Comments
 (0)