Skip to content

Instantly share code, notes, and snippets.

@Andrielson
Last active December 18, 2021 00:14
Show Gist options
  • Save Andrielson/c1874a8005ade170ca55d9e0fb789437 to your computer and use it in GitHub Desktop.
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.
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