Last active
December 18, 2021 00:14
-
-
Save Andrielson/c1874a8005ade170ca55d9e0fb789437 to your computer and use it in GitHub Desktop.
A generic observable state implementing a interface that is compatible with BehaviorSubject from RxJS.
This file contains hidden or 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
import { | |
Inject, | |
Injectable, | |
InjectionToken, | |
ValueProvider, | |
} from "@angular/core"; | |
import { BehaviorSubject, fromEvent, Observable, Subject } from "rxjs"; | |
import { map, takeUntil } from "rxjs/operators"; | |
const MENU_STATE_ID = "UNIQUE_MENU_STATE_ID"; | |
export const MENU_SERVICE_STATE = new InjectionToken<ObservableState<boolean>>( | |
MENU_STATE_ID | |
); | |
export const WEB_STORAGE_API = new InjectionToken<Storage>("WEB_STORAGE_API"); | |
/** | |
* A generic interface compatible with BehaviorSubject from RxJS | |
*/ | |
export interface ObservableState<T> { | |
next(value: T): void; | |
complete(): void; | |
asObservable(): Observable<T>; | |
getValue(): T; | |
readonly value: T; | |
} | |
/** | |
* A generic class that implements ObservableState and can replace BehaviorSubject. | |
* It uses window custom events which facilitate communication between microfrontends. | |
*/ | |
export class ObservableStateFromEventStorage<T> implements ObservableState<T> { | |
protected readonly complete$ = new Subject<void>(); | |
constructor( | |
protected readonly defaultValue: T, | |
protected readonly id: string, | |
protected readonly storage: Storage = sessionStorage | |
) {} | |
protected set storageValue(value: T) { | |
this.storage.setItem(this.id, JSON.stringify(value)); | |
} | |
protected get storageValue(): T { | |
const value = this.storage.getItem(this.id); | |
return value === undefined ? this.defaultValue : JSON.parse(value); | |
} | |
public get value(): T { | |
return this.storageValue; | |
} | |
asObservable(): Observable<T> { | |
return fromEvent<CustomEvent<T>>(window, this.id).pipe( | |
map(({ detail }) => { | |
this.storageValue = detail; | |
return detail; | |
}), | |
takeUntil(this.complete$) | |
); | |
} | |
next(detail: T): void { | |
const event = new CustomEvent<T>("", { detail }); | |
window.dispatchEvent(event); | |
} | |
complete(): void { | |
this.complete$.next(); | |
this.complete$.complete(); | |
this.storage.removeItem(this.id); | |
} | |
getValue(): T { | |
return this.value; | |
} | |
} | |
/** | |
* Example of a Service that uses an ObservableState. | |
*/ | |
@Injectable({ providedIn: "root" }) | |
export class MenuService { | |
constructor( | |
@Inject(MENU_SERVICE_STATE) private readonly state: ObservableState<boolean> | |
) {} | |
get visible(): boolean { | |
return this.state.value; | |
} | |
get menu$(): Observable<boolean> { | |
return this.state.asObservable(); | |
} | |
show(): void { | |
this.state.next(true); | |
} | |
hide(): void { | |
this.state.next(false); | |
} | |
toggle(): void { | |
return this.visible ? this.hide() : this.show(); | |
} | |
destroy(): void { | |
this.state.complete(); | |
} | |
} | |
/** | |
* Implementation of Storage interface using a global Map | |
*/ | |
export class WindowStorage implements Storage { | |
constructor(private readonly id: string) { | |
if (!window[id]) { | |
window[id] = new Map<string, string>(); | |
} | |
} | |
private get state(): Map<string, string> { | |
return window[this.id]; | |
} | |
public get length(): number { | |
return this.state.size; | |
} | |
clear(): void { | |
this.state.clear(); | |
} | |
getItem(key: string): string { | |
return this.state.get(key); | |
} | |
key(index: number): string { | |
if (index >= this.length) return null; | |
return Array.from(this.state.keys())[index]; | |
} | |
removeItem(key: string): void { | |
this.state.delete(key); | |
} | |
setItem(key: string, value: string): void { | |
this.state.set(key, value); | |
} | |
} | |
/** | |
* A value provider for MENU_SERVICE_STATE using the ObservableStateFromEventStorage class and SessionStorage. | |
*/ | |
export const menuStateSessionStorageProvider: ValueProvider = { | |
provide: MENU_SERVICE_STATE, | |
useValue: new ObservableStateFromEventStorage<boolean>( | |
false, | |
MENU_STATE_ID, | |
sessionStorage | |
), | |
}; | |
/** | |
* A value provider for MENU_SERVICE_STATE using the ObservableStateFromEventStorage class and a custom Storage implementation. | |
*/ | |
export const menuStateWindowStorageProvider: ValueProvider = { | |
provide: MENU_SERVICE_STATE, | |
useValue: new ObservableStateFromEventStorage<boolean>( | |
false, | |
MENU_STATE_ID, | |
new WindowStorage(MENU_STATE_ID) | |
), | |
}; | |
/** | |
* A value provider for MENU_SERVICE_STATE using the BehaviorSubject class from RxJS. | |
*/ | |
export const menuStateBehaviorProvider: ValueProvider = { | |
provide: MENU_SERVICE_STATE, | |
useValue: new BehaviorSubject<boolean>(false), | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment