Created
January 22, 2021 10:31
-
-
Save PhilippMeissner/fd4c15ad8b6cea7828c93a740a841ed7 to your computer and use it in GitHub Desktop.
Angular force app update and block view until then
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
index.html | |
``` | |
<script> | |
body > .splash > .spinner { | |
width: 50px; | |
height: 50px; | |
margin: 100px auto; | |
background-color: #676767; | |
border-radius: 100%; | |
-webkit-animation: sk-scaleout 1.0s infinite ease-in-out; | |
animation: sk-scaleout 1.0s infinite ease-in-out; | |
} | |
@-webkit-keyframes sk-scaleout { | |
0% { | |
-webkit-transform: scale(0) | |
} | |
100% { | |
-webkit-transform: scale(1.0); | |
opacity: 0; | |
} | |
} | |
@keyframes sk-scaleout { | |
0% { | |
-webkit-transform: scale(0); | |
transform: scale(0); | |
} | |
100% { | |
-webkit-transform: scale(1.0); | |
transform: scale(1.0); | |
opacity: 0; | |
} | |
} | |
.hide-splash { | |
opacity: 0 !important; | |
} | |
body > .splash.hidden { | |
display: none !important; | |
} | |
</script> | |
<body> | |
<div class="splash"> | |
<div class="spinner"></div> | |
</div> | |
<em-root></em-root> | |
</body> | |
``` | |
AppComponent | |
``` | |
@Component({ | |
selector: 'em-root', | |
templateUrl: './app.component.html', | |
styleUrls: ['./app.component.scss'], | |
}) | |
export class AppComponent implements OnInit { | |
constructor( | |
private readonly _store: Store, | |
private readonly _appref: ApplicationRef, | |
private readonly _appUpdates: AppUpdatesService, | |
private readonly _ngZone: NgZone, | |
@Inject(DOCUMENT) private readonly _document: Document, | |
@Inject(WINDOW) private readonly _window: Window, | |
@Inject(INITIALIZABLE) @Optional() private readonly _initializables?: IInitializable[], | |
) { | |
/* | |
// DO NOT FORGET TO WRAP WHATEVER SIDE-EFFECT YOU WANT TO HAPPEN HERE INSIDE NG-ZONE!!!!!!!!! | |
// DO NOT FORGET TO WRAP WHATEVER SIDE-EFFECT YOU WANT TO HAPPEN HERE INSIDE NG-ZONE!!!!!!!!! | |
// DO NOT FORGET TO WRAP WHATEVER SIDE-EFFECT YOU WANT TO HAPPEN HERE INSIDE NG-ZONE!!!!!!!!! | |
*/ | |
this._appref.isStable | |
.pipe(take(1)) | |
.subscribe(() => this._ngZone.run(() => this._store.dispatch(startApplicationUpdate()))); | |
} | |
ngOnInit() { | |
this._store.select(getAppBlockerActive) | |
.pipe( | |
tap((isActive) => isActive ? this._showLoading() : this._hideLoading()), | |
).subscribe(); | |
} | |
private _showLoading(): void { | |
const splash = this._document.getElementsByClassName('splash').item(0); | |
if (!splash) return; | |
splash.classList.remove('hide-splash', 'hidden'); | |
} | |
private _hideLoading(): void { | |
const splash = this._document.getElementsByClassName('splash').item(0); | |
if (!splash) return; | |
setTimeout(() => { | |
splash.classList.add('hidden'); | |
splash.classList.add('hide-splash'); | |
}, 1000); | |
} | |
} | |
``` | |
AppUpdatesService | |
``` | |
import {DOCUMENT} from '@angular/common'; | |
import {Inject, Injectable} from '@angular/core'; | |
import {SwUpdate} from '@angular/service-worker'; | |
import {Store} from '@ngrx/store'; | |
import {environment} from './environments/environment'; | |
import {finishApplicationUpdate} from './state/actions'; | |
@Injectable({ | |
providedIn: 'root', | |
}) | |
export class AppUpdatesService { | |
private _updateAvailable = false; | |
constructor( | |
private readonly _store: Store, | |
private readonly _swUpdate: SwUpdate, | |
@Inject(DOCUMENT) private readonly _document: Document, | |
) { | |
this._swUpdate.unrecoverable.subscribe((event) => { | |
console.log( | |
`An error occurred that we cannot recover from:\n${event.reason}\n\n` + | |
'Please reload the page.'); | |
}); | |
} | |
async checkForUpdate() { | |
// No service-worker enabled in dev | |
if (!environment.production) return this._finishUpdate(); | |
// When user SHIFT + CMD + reloads (`navigator.serviceWorker.controller` will be `null` then) | |
if (navigator.serviceWorker && !navigator.serviceWorker.controller) { | |
await navigator.serviceWorker.ready; | |
this._document.location?.reload(); | |
return; | |
} | |
// Service worker activated in browser | |
if (!this._swUpdate.isEnabled) { | |
this._finishUpdate(); | |
return; | |
} | |
this._registerListeners(); | |
// Happens *after* `swUpdate.available` emitted a value | |
await this._swUpdate.checkForUpdate(); | |
this._updateAvailable ? await this._doUpdate() : this._finishUpdate(); | |
} | |
private async _doUpdate() { | |
console.log('A newer version is now available. Forcing update.'); | |
await this._swUpdate.activateUpdate(); | |
// Latest version downloaded and activated. To reduce caching issues, we reload the page now | |
this._document.location?.reload(); | |
} | |
private _registerListeners() { | |
// Happens before `checkForUpdate` resolves | |
this._swUpdate.available.subscribe(() => { | |
this._updateAvailable = true; | |
console.log('A newer version is available, but not downloaded yet'); | |
}); | |
} | |
private _finishUpdate() { | |
// Tell global effects that we are on the latest version available | |
this._store.dispatch(finishApplicationUpdate()); | |
} | |
} | |
``` | |
Global AppEffects | |
``` | |
@Injectable({ | |
providedIn: 'root', | |
}) | |
export class AppEffects { | |
startUpdate$ = createEffect(() => | |
this._actions$ | |
.pipe( | |
ofType(startApplicationUpdate), | |
tap(() => this._update.checkForUpdate()), | |
), | |
{dispatch: false}); | |
validateJwt$ = createEffect(() => | |
this._actions$ | |
.pipe( | |
ofType(finishApplicationUpdate), | |
map(() => { | |
if (!this._session.hasToken()) return redirectToLogin(); | |
return this._session.isJwtValid() ? jwtValid() : jwtInvalid(); | |
}), | |
), | |
); | |
logout$ = createEffect(() => | |
this._actions$ | |
.pipe( | |
ofType(jwtInvalid), | |
switchMap(() => this._session.logout()), | |
map(() => redirectToLogin()), | |
), | |
); | |
redirectToLogin$ = createEffect(() => | |
this._actions$ | |
.pipe( | |
ofType(redirectToLogin), | |
switchMap(() => { | |
if (this._isRouteHandledSeparately()) { | |
return of(true); | |
} else { | |
this._router.navigateByUrl(AppConfiguration.loginRoute); | |
return of(false); | |
} | |
}), | |
exhaustMap((dispatchActions) => { | |
return dispatchActions ? of(initAngularRouting(), hideAppBlocker()) : of(hideAppBlocker()); | |
}), | |
), | |
); | |
startAngularRouting$ = createEffect(() => | |
this._actions$.pipe( | |
ofType(jwtValid, initAngularRouting), | |
tap(() => this._router.initialNavigation()), | |
mapTo(startAngularRouting()), | |
), | |
); | |
constructor( | |
private readonly _update: AppUpdatesService, | |
private readonly _actions$: Actions, | |
private readonly _session: SessionService, | |
private readonly _router: Router, | |
@Inject(WINDOW) private readonly _window: Window, | |
) { | |
} | |
private readonly _seperatelyHandledRoutes: string[] = [ | |
'/foobar', | |
]; | |
// We happen to have routes that should not trigger the routing guards implemented with angular | |
private _isRouteHandledSeparately(): boolean { | |
return this._seperatelyHandledRoutes.includes(this._window.location.pathname); | |
} | |
} | |
``` | |
Global App-Reducer | |
``` | |
export interface IState { | |
appBlockerActive: boolean; | |
updateStatus: AppUpdateStatus; | |
} | |
export const initialState: IState = { | |
appBlockerActive: true, | |
updateStatus: AppUpdateStatus.UNKNOWN, | |
}; | |
export const appStateReducers = createReducer(initialState, | |
on(showAppBlocker, (state) => ({ | |
...state, | |
appBlockerActive: true, | |
})), | |
on(hideAppBlocker, jwtValid, (state) => ({ | |
...state, | |
appBlockerActive: false, | |
})), | |
on(startApplicationUpdate, (state) => ({ | |
...state, | |
updateStatus: AppUpdateStatus.IN_PROGRESS, | |
})), | |
on(finishApplicationUpdate, (state) => ({ | |
...state, | |
updateStatus: AppUpdateStatus.FINISHED, | |
})), | |
); | |
export function reducer(state: IState | undefined, action: Action) { | |
return appStateReducers(state, action); | |
} | |
``` |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment