Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save PhilippMeissner/fd4c15ad8b6cea7828c93a740a841ed7 to your computer and use it in GitHub Desktop.
Save PhilippMeissner/fd4c15ad8b6cea7828c93a740a841ed7 to your computer and use it in GitHub Desktop.
Angular force app update and block view until then
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