UNPKG

@rx-angular/state

Version:

@rx-angular/state is a light-weight, flexible, strongly typed and tested tool dedicated to reduce the complexity of managing component state and side effects in angular

1 lines 15.8 kB
{"version":3,"file":"state-effects.mjs","sources":["../../../../libs/state/effects/src/lib/utils.ts","../../../../libs/state/effects/src/lib/effects.service.ts","../../../../libs/state/effects/src/lib/rx-effects.ts","../../../../libs/state/effects/src/state-effects.ts"],"sourcesContent":["import { Observable } from 'rxjs';\nimport { filter, map, shareReplay, take } from 'rxjs/operators';\nimport { HookProps, SingleShotProps } from './model';\n\nexport function isSingleShotHookNameGuard<T>(\n name: unknown,\n): name is keyof SingleShotProps {\n return !!name && typeof name === 'string' && name !== '';\n}\n\n/**\n * @internal\n * Operator to filter values for single shot observables\n */\nconst singleShotOperators = (o$: Observable<true>): Observable<true> =>\n o$.pipe(\n filter((v) => v === true),\n take(1),\n shareReplay(),\n );\n\n/**\n * This is an operator that is used to listen to Angular lifecycle hooks.\n * It plucks a defined lefe cycle name `HookProps` and forwards values for the particular value and in the behavior of the hook e.g. single shot\n *\n * @param name\n */\nexport function toHook<H extends keyof HookProps>(name: H) {\n const operators = isSingleShotHookNameGuard(name)\n ? singleShotOperators\n : (o: Observable<HookProps[H]>): Observable<true> => o;\n return (o$: Observable<HookProps>): Observable<HookProps[H]> =>\n o$.pipe(\n map((p) => p[name]),\n operators,\n );\n}\n","import { DestroyRef, ErrorHandler, inject, Injectable } from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport {\n EMPTY,\n from,\n Observable,\n ObservableInput,\n PartialObserver,\n pipe,\n Subject,\n Subscription,\n} from 'rxjs';\nimport {\n catchError,\n filter,\n map,\n mergeAll,\n share,\n takeUntil,\n tap,\n} from 'rxjs/operators';\nimport { DestroyProp, OnDestroy$ } from './model';\nimport { toHook } from './utils';\n\n/**\n * @deprecated - use rxEffects instead\n *\n * Reduces subscription boilerplate for performing observable-based side-effects in components.\n *\n * Before:\n * ```ts\n * @Component({\n * // ...\n * })\n * export class FooComponent implements OnDestroy {\n * private readonly destroy$ = new Subject<void>();\n *\n * constructor() {\n * obs$.pipe(takeUntil(this.destroy$)).subscribe(doSideEffect);\n * }\n *\n * ngOnDestroy(): void {\n * this.destroy$.next();\n * this.destroy$.complete();\n * }\n * }\n * ```\n *\n * After:\n * ```ts\n * @Component({\n * // ...\n * providers: [RxEffects],\n * })\n * export class FooComponent {\n * constructor(effects: RxEffects) {\n * effects.register(obs$, doSideEffect);\n * // OR\n * effects.register(obs$.pipe(tap(doSideEffect)));\n * // OR\n * effects.register(obs$.subscribe(doSideEffect));\n * }\n * }\n * ```\n *\n * NOTE: Avoid calling register/unregister/subscribe inside the side-effect function.\n */\n@Injectable()\nexport class RxEffects implements OnDestroy$ {\n private static nextId = 0;\n private readonly destroyRef = inject(DestroyRef);\n private readonly errorHandler = inject(ErrorHandler, { optional: true });\n readonly _hooks$ = new Subject<DestroyProp>();\n private readonly observables$ = new Subject<Observable<unknown>>();\n // we have to use publish here to make it hot (composition happens without subscriber)\n private readonly effects$ = this.observables$.pipe(mergeAll(), share());\n private readonly subscription = this.effects$.subscribe();\n onDestroy$: Observable<boolean> = this._hooks$.pipe(toHook('destroy'));\n private readonly destroyers: Record<number, Subject<void>> = {};\n\n constructor() {\n this.destroyRef.onDestroy(() => {\n this._hooks$.next({ destroy: true });\n this.subscription.unsubscribe();\n });\n }\n\n /**\n * Performs a side-effect whenever a source observable emits, and handles its subscription.\n *\n * @example\n * effects.register(\n * colorMode$,\n * mode => localStorage.setItem('colorMode', mode)\n * );\n *\n * @param sourceObs Source observable input\n * @param sideEffectFn Function with side-effect\n * @returns Effect ID (can be used to unregister imperatively)\n */\n register<T>(\n sourceObs: ObservableInput<T>,\n sideEffectFn: (value: T) => void,\n ): number;\n\n /**\n * Subscribe to source observable using an observer object.\n *\n * @example\n * effects.register(\n * colorMode$,\n * {\n * next: mode => localStorage.setItem('colorMode', mode),\n * error: err => {\n * console.error('Color mode error: ', err);\n * localStorage.removeItem('colorMode');\n * }\n * }\n * );\n *\n * @param sourceObs Source observable input\n * @param observer Observer object\n */\n register<T>(\n sourceObs: ObservableInput<T>,\n // tslint:disable-next-line: unified-signatures\n observer: PartialObserver<T>,\n ): number;\n\n /**\n * Handles subscription for an observable with a side-effect.\n *\n * @example\n * effects.register(\n * colorMode$.pipe(\n * tap(mode => localStorage.setItem('colorMode', mode))\n * )\n * );\n *\n * @param sideEffectObs Observable input with side-effect\n * @returns Effect ID (can be used to unregister imperatively)\n */\n register(sideEffectObs: ObservableInput<unknown>): number;\n\n /**\n * Handles subscription to an observable with a side-effect.\n *\n * @example\n * effects.register(\n * colorMode$.subscribe(mode => localStorage.setItem('colorMode', mode))\n * );\n *\n * @param subscription Subscription to observable with side-effect\n */\n // tslint:disable-next-line: unified-signatures\n register(subscription: Subscription): void;\n\n register<T>(\n obsOrSub: ObservableInput<T> | Subscription,\n fnOrObj?: ((value: T) => void) | PartialObserver<T>,\n ): number | void {\n if (obsOrSub instanceof Subscription) {\n this.subscription.add(obsOrSub);\n return;\n }\n const effectId = RxEffects.nextId++;\n const destroy$ = (this.destroyers[effectId] = new Subject<void>());\n const applyBehavior = pipe(\n map(() => effectId),\n takeUntil(destroy$),\n );\n if (fnOrObj != null) {\n this.observables$.next(\n from(obsOrSub).pipe(\n // ternary expression is to help Typescript infer overloads\n typeof fnOrObj === 'function' ? tap(fnOrObj) : tap(fnOrObj),\n catchError((err) => {\n this.errorHandler?.handleError(err);\n return EMPTY;\n }),\n applyBehavior,\n ),\n );\n } else {\n this.observables$.next(from(obsOrSub).pipe(applyBehavior));\n }\n return effectId;\n }\n\n /**\n * Imperatively cancel a side-effect while the component is still running.\n *\n * Note that all effects are automatically cancelled when a component is destroyed,\n * so you most often won't need to call this method.\n * @param effectId Effect ID (returned by register method)\n */\n unregister(effectId: number): void {\n this.destroyers[effectId]?.next();\n }\n\n /**\n * Fires a sideEffect when the instances `OnDestroy` hook is fired.\n *\n * @example\n * effects.registerOnDestroy(mode => localStorage.setItem('colorMode', mode));\n *\n * @param sideEffect\n */\n registerOnDestroy(sideEffect: (value: boolean) => void): number | void {\n return this.register(this.onDestroy$, sideEffect);\n }\n\n /**\n * Operator that unsubscribes based on emission of an registered effect.\n *\n * @NOTICE\n * This operator has to be placed always at the end of the operator chain (before the subscription).\n * Otherwise we may leak as a subsequent operator could instantiate new ongoing Observables which will not get unsubscribed.\n *\n * @example\n * const effectId1 = effects.register(\n * colorMode$.subscribe(mode => localStorage.setItem('colorMode', mode))\n * );\n *\n * someValue$.pipe(\n * effect.untilEffect(effectId1)\n * )\n *\n */\n untilEffect(effectId: number) {\n return <V>(source: Observable<V>) =>\n source.pipe(\n takeUntilDestroyed(this.destroyRef),\n takeUntil(this.effects$.pipe(filter((eId) => eId === effectId))),\n );\n }\n}\n","import {\n assertInInjectionContext,\n DestroyRef,\n ErrorHandler,\n inject,\n} from '@angular/core';\nimport { from, Subscription } from 'rxjs';\nimport { SideEffectFnOrObserver, SideEffectObservable } from './types';\n\ninterface RxEffects {\n register<T>(\n observable: SideEffectObservable<T>,\n sideEffectOrObserver?: SideEffectFnOrObserver<T>\n ): Fn;\n onDestroy: (fn: Fn) => Fn;\n}\n\ntype Fn = () => void;\n\nexport type RxEffectsSetupFn = (\n cfg: Pick<RxEffects, 'register' | 'onDestroy'>\n) => void;\n\n/**\n * @description\n * Functional way to setup observable based side effects with RxEffects.\n * It's a creation function for RxEffects that destroys itself when the provided\n * `DestroyRef` is destroyed.\n *\n * @example\n * ```ts\n * import { rxEffects } from '@rx-angular/state/effects';\n *\n * \\@Component({})\n * export class FooComponent {\n * const readonly util = inject(Util);\n * readonly effects = rxEffects(({ register }) => {\n * register(this.util.windowResize$, () => {\n * console.log('window was resized');\n * })\n * });\n *\n * ngOnInit() {\n * this.effects.register(this.util.rotationChanged$, () => {\n * console.log('viewport rotation changed');\n * });\n * }\n * }\n * ```\n *\n * @param {RxEffectsSetupFn} setupFn\n * @returns RxEffects\n *\n * @docsCategory RxEffects\n * @docsPage RxEffects\n *\n */\nexport function rxEffects(setupFn?: RxEffectsSetupFn): RxEffects {\n assertInInjectionContext(rxEffects);\n const errorHandler = inject(ErrorHandler, { optional: true });\n const destroyRef = inject(DestroyRef);\n const runningEffects: Subscription[] = [];\n destroyRef.onDestroy(() => runningEffects.forEach((ef) => ef.unsubscribe()));\n\n /**\n * Subscribe to observables and trigger side effect.\n *\n * @example\n *\n * /@Component({\n * template: `<button name=\"save\" (click)=\"save()\">Save</button>`\n * })\n * class ListComponent {\n * private ef = rxEffects(({register}) => {\n * register(timer(0, this.backupInterval), console.log));\n * }\n * }\n *\n * @param {SideEffectObservable} obs$ Source observable input\n * @param {SideEffectFnOrObserver} sideEffect Observer object\n *\n * @return {Function} - unregisterFn\n */\n function register<T>(\n obs$: SideEffectObservable<T>,\n sideEffect?: SideEffectFnOrObserver<T>\n ): () => void {\n const observer =\n typeof sideEffect === 'object'\n ? {\n ...sideEffect,\n // preserve original logic\n error: (e: unknown) => {\n sideEffect.error?.(e);\n errorHandler?.handleError(e);\n },\n }\n : {\n next: sideEffect,\n error: (e: unknown) => errorHandler?.handleError(e),\n };\n const sub = from(obs$).subscribe(observer);\n runningEffects.push(sub);\n return () => sub.unsubscribe();\n }\n\n /**\n * Register custom cleanup logic.\n *\n * @example\n *\n * /@Component({\n * template: `<button name=\"save\" (click)=\"save()\">Save</button>`\n * })\n * class ListComponent {\n * private ef = rxEffects(({onDestroy}) => {\n * onDestroy(() => console.log('done'));\n * }\n * }\n *\n * @param {Fn} callback onDestroy callback\n *\n * @return {Fn} unregisterFn\n */\n function onDestroy(callback: Fn): Fn {\n return destroyRef.onDestroy(callback);\n }\n\n const effects = {\n register,\n onDestroy,\n };\n\n setupFn?.(effects);\n\n return effects;\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}