Skip to content

Commit

Permalink
feat: allow back navigation without loosing state
Browse files Browse the repository at this point in the history
  • Loading branch information
gion-andri committed Oct 9, 2023
1 parent f402e9a commit d97d484
Show file tree
Hide file tree
Showing 7 changed files with 262 additions and 94 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 16.1.0.

## Tecnical explanations

### Back routing

To restore the events in the events list after back routing, a router reuse strategy is defined.
For all the routes that contain a `reuseRouteKey` parameter in the route data, the `RouteReuseStrategy` will be used. `reuseRouteKey` should be unique.

To have access to `onAttach` and `onDetach` lifecycle events, we implement our own router outlet. This router outlet will be used in the `app.component.html` file.

## Development server

Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
Expand Down
63 changes: 35 additions & 28 deletions src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {EventsListComponent} from "./pages/events/events-list/events-list.component";
import {EventsDetailsComponent} from "./pages/events/events-details/events-details.component";
import {LoginComponent} from "./pages/u/login/login.component";
import {LogoutComponent} from "./pages/u/logout/logout.component";
import {notAuthGuard} from "./routing/not-auth.guard";
import {authGuard} from "./routing/auth.guard";
import {NotFoundComponent} from "./pages/static/not-found/not-found.component";
import {canMatchEventId} from "./routing/match-event-id.guard";
import {HelpComponent} from "./pages/static/help/help.component";
import {ContactComponent} from "./pages/static/contact/contact.component";
import {OrganisationComponent} from "./pages/static/organisation/organisation.component";
import {ImprintComponent} from "./pages/static/imprint/imprint.component";
import {PrivacyComponent} from "./pages/static/privacy/privacy.component";
import {EventsComponent} from "./pages/u/events/events.component";
import {ForgotPasswordComponent} from "./pages/u/forgot-password/forgot-password.component";
import {RegisterComponent} from "./pages/u/register/register.component";
import {NewEventComponent} from "./pages/admin/new-event/new-event.component";
import {MyEventsComponent} from "./pages/admin/my-events/my-events.component";
import {MySubscriptionsComponent} from "./pages/admin/my-subscriptions/my-subscriptions.component";
import {ProfileComponent} from "./pages/admin/profile/profile.component";
import {ConfirmEmailComponent} from "./pages/u/confirm-email/confirm-email.component";
import {ConfirmPasswordComponent} from "./pages/u/confirm-password/confirm-password.component";
import {ChangePasswordComponent} from "./pages/admin/change-password/change-password.component";
import {ModeratorEventsComponent} from "./pages/moderator/moderator-events/moderator-events.component";
import {UsersComponent} from "./pages/administrator/users/users.component";
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { EventsListComponent } from "./pages/events/events-list/events-list.component";
import { EventsDetailsComponent } from "./pages/events/events-details/events-details.component";
import { LoginComponent } from "./pages/u/login/login.component";
import { LogoutComponent } from "./pages/u/logout/logout.component";
import { notAuthGuard } from "./routing/not-auth.guard";
import { authGuard } from "./routing/auth.guard";
import { NotFoundComponent } from "./pages/static/not-found/not-found.component";
import { canMatchEventId } from "./routing/match-event-id.guard";
import { HelpComponent } from "./pages/static/help/help.component";
import { ContactComponent } from "./pages/static/contact/contact.component";
import { OrganisationComponent } from "./pages/static/organisation/organisation.component";
import { ImprintComponent } from "./pages/static/imprint/imprint.component";
import { PrivacyComponent } from "./pages/static/privacy/privacy.component";
import { EventsComponent } from "./pages/u/events/events.component";
import { ForgotPasswordComponent } from "./pages/u/forgot-password/forgot-password.component";
import { RegisterComponent } from "./pages/u/register/register.component";
import { NewEventComponent } from "./pages/admin/new-event/new-event.component";
import { MyEventsComponent } from "./pages/admin/my-events/my-events.component";
import { MySubscriptionsComponent } from "./pages/admin/my-subscriptions/my-subscriptions.component";
import { ProfileComponent } from "./pages/admin/profile/profile.component";
import { ConfirmEmailComponent } from "./pages/u/confirm-email/confirm-email.component";
import { ConfirmPasswordComponent } from "./pages/u/confirm-password/confirm-password.component";
import { ChangePasswordComponent } from "./pages/admin/change-password/change-password.component";
import { ModeratorEventsComponent } from "./pages/moderator/moderator-events/moderator-events.component";
import { UsersComponent } from "./pages/administrator/users/users.component";

const routes: Routes = [
{
Expand Down Expand Up @@ -67,7 +67,14 @@ const routes: Routes = [
{path: 'organisation', pathMatch: 'full', component: OrganisationComponent},
{path: 'imprint', pathMatch: 'full', component: ImprintComponent},
{path: 'privacy', pathMatch: 'full', component: PrivacyComponent},
{path: '', pathMatch: 'full', component: EventsListComponent},
{
path: '',
pathMatch: 'full',
component: EventsListComponent,
data: {
reuseRouteKey: 'events-list'
}
},
{path: '**', pathMatch: 'full', component: NotFoundComponent},
];

Expand Down
2 changes: 1 addition & 1 deletion src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<app-messages/>

<div id="content">
<router-outlet></router-outlet>
<app-router-outlet></app-router-outlet>
</div>

<app-footer/>
Expand Down
122 changes: 66 additions & 56 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,61 @@
import {NgModule} from '@angular/core';
import {BrowserModule, provideClientHydration} from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { BrowserModule, provideClientHydration } from '@angular/platform-browser';

import {AppRoutingModule} from './app-routing.module';
import {AppComponent} from './app.component';
import {EventsListComponent} from './pages/events/events-list/events-list.component';
import {EventsDetailsComponent} from './pages/events/events-details/events-details.component';
import {HeaderComponent} from './components/header/header.component';
import {BackButtonComponent} from './components/back-button/back-button.component';
import {LoginComponent} from './pages/u/login/login.component';
import {LogoutComponent} from './pages/u/logout/logout.component';
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
import {HttpClient, HttpClientModule} from "@angular/common/http";
import {JWT_OPTIONS, JwtModule} from "@auth0/angular-jwt";
import {environment} from "../environments/environment";
import {HelpComponent} from './pages/static/help/help.component';
import {ContactComponent} from './pages/static/contact/contact.component';
import {ImprintComponent} from './pages/static/imprint/imprint.component';
import {PrivacyComponent} from './pages/static/privacy/privacy.component';
import {OrganisationComponent} from './pages/static/organisation/organisation.component';
import {NotFoundComponent} from './pages/static/not-found/not-found.component';
import {EventsComponent} from './pages/u/events/events.component';
import {ForgotPasswordComponent} from './pages/u/forgot-password/forgot-password.component';
import {RegisterComponent} from './pages/u/register/register.component';
import {NewEventComponent} from './pages/admin/new-event/new-event.component';
import {MyEventsComponent} from './pages/admin/my-events/my-events.component';
import {MySubscriptionsComponent} from './pages/admin/my-subscriptions/my-subscriptions.component';
import {ProfileComponent} from './pages/admin/profile/profile.component';
import {AuthenticationService} from "./services/authentication.service";
import {ShortDomainPipe} from './pipes/short-domain.pipe';
import {EventCardComponent} from './components/events/event-card/event-card.component';
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
import {EventFilterComponent} from './components/events/event-filter/event-filter.component';
import {FooterComponent} from './components/footer/footer.component';
import {MessagesComponent} from './components/messages/messages.component';
import {ConfirmEmailComponent} from './pages/u/confirm-email/confirm-email.component';
import {ConfirmPasswordComponent} from './pages/u/confirm-password/confirm-password.component';
import {ChangePasswordComponent} from './pages/admin/change-password/change-password.component';
import {NewEventButtonComponent} from './components/new-event-button/new-event-button.component';
import {ModeratorEventsComponent} from './pages/moderator/moderator-events/moderator-events.component';
import {StatusBadgeComponent} from './components/status-badge/status-badge.component';
import {PaginationComponent} from './components/pagination/pagination.component';
import {EventPreviewComponent} from './components/event-preview/event-preview.component';
import {EventDiffComponent} from './components/event-diff/event-diff.component';
import {DiffFieldComponent} from './components/event-diff/diff-field/diff-field.component';
import {UsersComponent} from "./pages/administrator/users/users.component";
import {RoleBadgeComponent} from './components/role-badge/role-badge.component';
import {UserComponent} from './components/forms/user/user.component';
import {ChangeUserComponent} from './components/change-user/change-user.component';
import {NgxFileDropModule} from "ngx-file-drop";
import {FileListComponent} from './components/file-list/file-list.component';
import {DeleteEventComponent} from './components/modals/delete-event/delete-event.component';
import {ReasonForChangeComponent} from './components/modals/reason-for-change/reason-for-change.component';
import {TranslateLoader, TranslateModule} from "@ngx-translate/core";
import {TranslateHttpLoader} from "@ngx-translate/http-loader";
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { EventsListComponent } from './pages/events/events-list/events-list.component';
import { EventsDetailsComponent } from './pages/events/events-details/events-details.component';
import { HeaderComponent } from './components/header/header.component';
import { BackButtonComponent } from './components/back-button/back-button.component';
import { LoginComponent } from './pages/u/login/login.component';
import { LogoutComponent } from './pages/u/logout/logout.component';
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { HttpClient, HttpClientModule } from "@angular/common/http";
import { JWT_OPTIONS, JwtModule } from "@auth0/angular-jwt";
import { environment } from "../environments/environment";
import { HelpComponent } from './pages/static/help/help.component';
import { ContactComponent } from './pages/static/contact/contact.component';
import { ImprintComponent } from './pages/static/imprint/imprint.component';
import { PrivacyComponent } from './pages/static/privacy/privacy.component';
import { OrganisationComponent } from './pages/static/organisation/organisation.component';
import { NotFoundComponent } from './pages/static/not-found/not-found.component';
import { EventsComponent } from './pages/u/events/events.component';
import { ForgotPasswordComponent } from './pages/u/forgot-password/forgot-password.component';
import { RegisterComponent } from './pages/u/register/register.component';
import { NewEventComponent } from './pages/admin/new-event/new-event.component';
import { MyEventsComponent } from './pages/admin/my-events/my-events.component';
import { MySubscriptionsComponent } from './pages/admin/my-subscriptions/my-subscriptions.component';
import { ProfileComponent } from './pages/admin/profile/profile.component';
import { AuthenticationService } from "./services/authentication.service";
import { ShortDomainPipe } from './pipes/short-domain.pipe';
import { EventCardComponent } from './components/events/event-card/event-card.component';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { EventFilterComponent } from './components/events/event-filter/event-filter.component';
import { FooterComponent } from './components/footer/footer.component';
import { MessagesComponent } from './components/messages/messages.component';
import { ConfirmEmailComponent } from './pages/u/confirm-email/confirm-email.component';
import { ConfirmPasswordComponent } from './pages/u/confirm-password/confirm-password.component';
import { ChangePasswordComponent } from './pages/admin/change-password/change-password.component';
import { NewEventButtonComponent } from './components/new-event-button/new-event-button.component';
import { ModeratorEventsComponent } from './pages/moderator/moderator-events/moderator-events.component';
import { StatusBadgeComponent } from './components/status-badge/status-badge.component';
import { PaginationComponent } from './components/pagination/pagination.component';
import { EventPreviewComponent } from './components/event-preview/event-preview.component';
import { EventDiffComponent } from './components/event-diff/event-diff.component';
import { DiffFieldComponent } from './components/event-diff/diff-field/diff-field.component';
import { UsersComponent } from "./pages/administrator/users/users.component";
import { RoleBadgeComponent } from './components/role-badge/role-badge.component';
import { UserComponent } from './components/forms/user/user.component';
import { ChangeUserComponent } from './components/change-user/change-user.component';
import { NgxFileDropModule } from "ngx-file-drop";
import { FileListComponent } from './components/file-list/file-list.component';
import { DeleteEventComponent } from './components/modals/delete-event/delete-event.component';
import { ReasonForChangeComponent } from './components/modals/reason-for-change/reason-for-change.component';
import { TranslateLoader, TranslateModule } from "@ngx-translate/core";
import { TranslateHttpLoader } from "@ngx-translate/http-loader";
import { RouteReuseStrategy } from '@angular/router';
import { RouterReuseStrategy } from './routing/router-reuse.strategy';
import { AppRouterOutletDirective } from './routing/app-router-outlet.directive';

export function jwtOptionsFactory(authService: AuthenticationService) {
return {
Expand Down Expand Up @@ -117,7 +120,8 @@ export function createTranslateLoader(http: HttpClient) {
ChangeUserComponent,
FileListComponent,
DeleteEventComponent,
ReasonForChangeComponent
ReasonForChangeComponent,
AppRouterOutletDirective,
],
imports: [
BrowserModule,
Expand All @@ -143,7 +147,13 @@ export function createTranslateLoader(http: HttpClient) {
defaultLanguage: 'rm'
}),
],
providers: [provideClientHydration()],
providers: [
{
provide: RouteReuseStrategy,
useClass: RouterReuseStrategy
},
provideClientHydration()
],
bootstrap: [AppComponent]
})
export class AppModule {
Expand Down
33 changes: 24 additions & 9 deletions src/app/pages/events/events-list/events-list.component.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import {EventsService} from "../../../services/events.service";
import {EventFilter, EventLookup} from "../../../data/event";
import { Component, OnDestroy, OnInit } from '@angular/core';
import { EventsService } from "../../../services/events.service";
import { EventFilter, EventLookup } from "../../../data/event";
import * as dayjs from 'dayjs'
import {rmLocale} from "../../../utils/day-js-locale";
import {NgbModal} from "@ng-bootstrap/ng-bootstrap";
import {EventFilterComponent} from "../../../components/events/event-filter/event-filter.component";
import {EventsFilterService} from "../../../services/events-filter.service";
import {Page} from "../../../data/page";
import { rmLocale } from "../../../utils/day-js-locale";
import { NgbModal } from "@ng-bootstrap/ng-bootstrap";
import { EventFilterComponent } from "../../../components/events/event-filter/event-filter.component";
import { EventsFilterService } from "../../../services/events-filter.service";
import { Page } from "../../../data/page";
import { OnAttach, OnDetach } from '../../../routing/app-router-outlet.directive';

const LOCALSTORAGE_EVENTS_LIST_SCROLL_POSITION = 'events-scroll-position';

@Component({
selector: 'app-events-list',
templateUrl: './events-list.component.html',
styleUrls: ['./events-list.component.scss']
})
export class EventsListComponent implements OnInit, OnDestroy {
export class EventsListComponent implements OnInit, OnDestroy, OnAttach, OnDetach {

public events: EventLookup[] = [];
public categorizedEvents: { date: string, formattedDate: string, events: EventLookup[] }[] = [];
Expand Down Expand Up @@ -56,6 +59,18 @@ export class EventsListComponent implements OnInit, OnDestroy {
}
}

onAttach(): void {
const scrollPosition = +(localStorage.getItem(LOCALSTORAGE_EVENTS_LIST_SCROLL_POSITION) || 0);
window.scrollTo({
top: scrollPosition,
behavior: 'instant',
});
}

onDetach(): void {
localStorage.setItem(LOCALSTORAGE_EVENTS_LIST_SCROLL_POSITION, window.scrollY.toString());
}

openFilter(): void {
const modalRef = this.modalService.open(EventFilterComponent, {size: 'lg'});
}
Expand Down
39 changes: 39 additions & 0 deletions src/app/routing/app-router-outlet.directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { ActivatedRoute, RouterOutlet } from '@angular/router';
import { ComponentRef, Directive } from '@angular/core';

export interface OnAttach {
/**
* A callback method that is invoked when the RouteReuseStrategy instructs
* to re-attach a previously detached component / subtree
*/
onAttach(activatedRoute: ActivatedRoute): void;
}

export interface OnDetach {
/**
* A callback method that is invoked when the RouteReuseStrategy instructs
* to detach component / subtree
*/
onDetach(): void;
}

@Directive({
selector: 'app-router-outlet',
})
export class AppRouterOutletDirective extends RouterOutlet {

override detach(): ComponentRef<any> {
const instance: any = this.component;
if (instance && typeof instance.onDetach === 'function') {
instance.onDetach();
}
return super.detach();
}

override attach(ref: ComponentRef<any>, activatedRoute: ActivatedRoute): void {
super.attach(ref, activatedRoute);
if (ref.instance && typeof ref.instance.onAttach === 'function') {
ref.instance.onAttach(activatedRoute);
}
}
}
Loading

0 comments on commit d97d484

Please sign in to comment.