Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
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
3 changes: 3 additions & 0 deletions apps/angular/55-back-button-navigation/src/app/app.routes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Routes } from '@angular/router';
import { canDeactivateGuard } from './can-deactivate.guard';
import { HomeComponent } from './home/home.component';
import { SensitiveActionComponent } from './sensitive-action/sensitive-action.component';
import { SimpleActionComponent } from './simple-action/simple-action.component';
Expand All @@ -16,9 +17,11 @@ export const APP_ROUTES: Routes = [
{
path: 'simple-action',
component: SimpleActionComponent,
canDeactivate: [canDeactivateGuard],
},
{
path: 'sensitive-action',
component: SensitiveActionComponent,
canDeactivate: [canDeactivateGuard],
},
];
33 changes: 33 additions & 0 deletions apps/angular/55-back-button-navigation/src/app/base-dialog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {
AfterContentInit,
ChangeDetectionStrategy,
Component,
inject,
} from '@angular/core';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { DialogData, DialogStrategyType } from './dialog-strategy';
import { DialogService } from './dialog.service';

@Component({
// eslint-disable-next-line @angular-eslint/component-selector
selector: '',
template: '',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BaseDialogComponent implements AfterContentInit {
readonly #data = inject<DialogData>(MAT_DIALOG_DATA);
protected readonly dialogService = inject(DialogService);

ngAfterContentInit(): void {
this.initStrategy();
}

private initStrategy() {
const strategyType: DialogStrategyType = this.#data?.strategy?.type
? this.#data?.strategy?.type
: 'default';

this.dialogService.setStrategy(strategyType);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { CanDeactivateFn, UrlTree } from '@angular/router';
import { Observable } from 'rxjs';

export type CanDeactivateType =
| Observable<boolean | UrlTree>
| Promise<boolean | UrlTree>
| boolean
| UrlTree;

export interface CanComponentDeactivate {
canDeactivate: () => CanDeactivateType;
}

export const canDeactivateGuard: CanDeactivateFn<CanComponentDeactivate> = (
component: CanComponentDeactivate,
) => {
return component.canDeactivate ? component.canDeactivate() : true;
};
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

👍

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<h2 mat-dialog-title>Confirm Action</h2>
<mat-dialog-content>
Are you sure you want to perform this action?
</mat-dialog-content>
<mat-dialog-actions>
<button
mat-button
mat-dialog-close
(click)="dialogService.closeActiveDialog()">
No
</button>
<button
mat-button
mat-dialog-close
cdkFocusInitial
(click)="dialogService.closeAll()">
Yes
</button>
</mat-dialog-actions>
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import {
MatDialogActions,
MatDialogClose,
MatDialogContent,
MatDialogTitle,
} from '@angular/material/dialog';
import { BaseDialogComponent } from '../base-dialog';

@Component({
selector: 'app-confirm-dialog',
templateUrl: './confirm-dialog.component.html',
standalone: true,
imports: [
MatButtonModule,
MatDialogActions,
MatDialogClose,
MatDialogTitle,
MatDialogContent,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ConfirmDialogComponent extends BaseDialogComponent {
constructor() {
super();
}
}
47 changes: 47 additions & 0 deletions apps/angular/55-back-button-navigation/src/app/dialog-strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Injectable, inject } from '@angular/core';
import { ConfirmDialogComponent } from './confirm-dialog/confirm-dialog.component';
import { DialogService } from './dialog.service';

export type DialogStrategyType = 'default' | 'confirm';

export type DialogData = {
strategy: {
type: DialogStrategyType;
};
};

export abstract class DialogStrategy {
protected readonly dialogService = inject(DialogService);

abstract onBackBrowserNavigation(): boolean;
}

@Injectable({
providedIn: 'root',
})
export class DefaultDialogStrategy extends DialogStrategy {
override onBackBrowserNavigation(): boolean {
this.dialogService.closeActiveDialog();

return false;
}
}

@Injectable({
providedIn: 'root',
})
export class ConfirmDialogStrategy extends DialogStrategy {
override onBackBrowserNavigation(): boolean {
this.dialogService.openDialog(ConfirmDialogComponent, {
width: '250px',
closeOnNavigation: false,
});

return false;
}
}

export const DialogStrategyMap = new Map<DialogStrategyType, any>([
['default', DefaultDialogStrategy],
['confirm', ConfirmDialogStrategy],
]);
109 changes: 109 additions & 0 deletions apps/angular/55-back-button-navigation/src/app/dialog.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { ComponentType } from '@angular/cdk/portal';
import { Injectable, Injector, inject } from '@angular/core';
import { MatDialog, MatDialogConfig } from '@angular/material/dialog';
import { take, tap } from 'rxjs';
import {
DialogStrategy,
DialogStrategyMap,
DialogStrategyType,
} from './dialog-strategy';

@Injectable({
providedIn: 'root',
})
export class DialogService {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Nice service, a bit out of the challenge, but great generic service for people to add to their projects 👍

readonly #dialog = inject(MatDialog);
readonly #dialogsStrategyState: {
id: string;
type: DialogStrategy;
active: boolean;
}[] = [];

readonly #injector = inject(Injector);

openDialog<T extends ComponentType<any>, Z extends MatDialogConfig<any>>(
component: T,
config: Z,
) {
const dialogRef = this.#dialog.open<T, Z>(component, config);

dialogRef
.afterClosed()
.pipe(
take(1),
tap(() => this.removeDialogStrategyStateById(dialogRef.id)),
)
.subscribe();
}

setStrategy(type: DialogStrategyType) {
this.#dialog.openDialogs.forEach((d) => {
const state = this.getDialogStrategyStateById(d.id);

if (!state) {
this.addDialogStrategyState(d.id, type);
}
});
}

getStrategyType() {
const active = this.getActiveDialog();

if (!active) {
return null;
}

return this.getDialogStrategyStateById(active.id)?.type;
}

closeActiveDialog() {
const activeDialog = this.#dialogsStrategyState.find((d) => d.active);

if (!activeDialog) {
return;
}

this.#dialog.getDialogById(activeDialog.id)?.close();
}

closeAll() {
this.#dialogsStrategyState.splice(0, this.#dialogsStrategyState.length);

this.#dialog.closeAll();
}

getActiveDialog() {
return this.#dialogsStrategyState.find((d) => d.active);
}

addDialogStrategyState(id: string, type: DialogStrategyType) {
const t = this.#injector.get<DialogStrategy>(DialogStrategyMap.get(type));

this.#dialogsStrategyState.map((x) => (x.active = false));

this.#dialogsStrategyState.push({ id, type: t, active: true });
}

getDialogStrategyStateById(id: string) {
return this.#dialogsStrategyState.find((s) => s.id === id);
}

removeDialogStrategyStateById(id: string) {
const indexToRemove = this.#dialogsStrategyState.findIndex(
(d) => d.id === id,
);

if (indexToRemove > 0) {
this.#dialogsStrategyState[indexToRemove - 1] = {
...this.#dialogsStrategyState[indexToRemove - 1],
active: true,
};
}

this.#dialogsStrategyState.splice(indexToRemove, 1);
}

getDialogsStrategyState() {
return this.#dialogsStrategyState;
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import {
MatDialogActions,
MatDialogClose,
MatDialogContent,
MatDialogRef,
MatDialogTitle,
} from '@angular/material/dialog';
import { BaseDialogComponent } from '../base-dialog';

@Component({
selector: 'app-dialog-dialog',
selector: 'app-dialog',
templateUrl: './dialog.component.html',
standalone: true,
imports: [
Expand All @@ -21,6 +21,8 @@ import {
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DialogComponent {
readonly dialogRef = inject(MatDialogRef<DialogComponent>);
export class DialogComponent extends BaseDialogComponent {
constructor() {
super();
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<a mat-raised-button routerLink="/simple-action" routerLinkActive="active">
<a mat-raised-button routerLink="/simple-action">
Go to simple dialog action page
</a>

<a mat-raised-button routerLink="/sensitive-action" routerLinkActive="active">
<a mat-raised-button routerLink="/sensitive-action">
Go to sensitive dialog action page
</a>
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<button mat-raised-button (click)="openDialog()">
Open dialog with confirmation dialog on browser back button click
</button>

<a mat-raised-button routerLink="/">Home</a>
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
import { Component, inject } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog';
import { DialogComponent } from '../dialog/dialog.component';
import { RouterLink } from '@angular/router';
import { CanComponentDeactivate } from '../can-deactivate.guard';
import { DialogService } from '../dialog.service';
import { SensitiveDialogComponent } from '../sensitive-dialog/sensitive-dialog.component';

@Component({
standalone: true,
imports: [MatButtonModule],
imports: [MatButtonModule, RouterLink],
selector: 'app-sensitive-action',
templateUrl: './sensitive-action.component.html',
})
export class SensitiveActionComponent {
readonly #dialog = inject(MatDialog);
export class SensitiveActionComponent implements CanComponentDeactivate {
readonly #dialogService = inject(DialogService);

openDialog(): void {
this.#dialog.open(DialogComponent, {
width: '250px',
this.#dialogService.openDialog(SensitiveDialogComponent, {
width: '450px',
data: { strategy: { type: 'confirm' } },
closeOnNavigation: false,
});
}

canDeactivate() {
return (
this.#dialogService.getStrategyType()?.onBackBrowserNavigation() ?? true
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<h2 mat-dialog-title>Sensitive File Deletion</h2>
<mat-dialog-content>Would you like to delete cat.jpeg?</mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close>No</button>
<button mat-button mat-dialog-close cdkFocusInitial>Ok</button>
</mat-dialog-actions>
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import {
MatDialogActions,
MatDialogClose,
MatDialogContent,
MatDialogTitle,
} from '@angular/material/dialog';
import { BaseDialogComponent } from '../base-dialog';

@Component({
selector: 'app-sensitive-dialog',
templateUrl: './sensitive-dialog.component.html',
standalone: true,
imports: [
MatButtonModule,
MatDialogActions,
MatDialogClose,
MatDialogTitle,
MatDialogContent,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SensitiveDialogComponent extends BaseDialogComponent {
constructor() {
super();
}
}
Loading