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

269 lines (263 loc) 8.72 kB
import * as i0 from '@angular/core'; import { inject, DestroyRef, ErrorHandler, Injectable, assertInInjectionContext } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { Subject, Subscription, pipe, from, EMPTY } from 'rxjs'; import { filter, take, shareReplay, map, mergeAll, share, takeUntil, tap, catchError } from 'rxjs/operators'; function isSingleShotHookNameGuard(name) { return !!name && typeof name === 'string' && name !== ''; } /** * @internal * Operator to filter values for single shot observables */ const singleShotOperators = (o$) => o$.pipe(filter((v) => v === true), take(1), shareReplay()); /** * This is an operator that is used to listen to Angular lifecycle hooks. * 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 * * @param name */ function toHook(name) { const operators = isSingleShotHookNameGuard(name) ? singleShotOperators : (o) => o; return (o$) => o$.pipe(map((p) => p[name]), operators); } /** * @deprecated - use rxEffects instead * * Reduces subscription boilerplate for performing observable-based side-effects in components. * * Before: * ```ts * @Component({ * // ... * }) * export class FooComponent implements OnDestroy { * private readonly destroy$ = new Subject<void>(); * * constructor() { * obs$.pipe(takeUntil(this.destroy$)).subscribe(doSideEffect); * } * * ngOnDestroy(): void { * this.destroy$.next(); * this.destroy$.complete(); * } * } * ``` * * After: * ```ts * @Component({ * // ... * providers: [RxEffects], * }) * export class FooComponent { * constructor(effects: RxEffects) { * effects.register(obs$, doSideEffect); * // OR * effects.register(obs$.pipe(tap(doSideEffect))); * // OR * effects.register(obs$.subscribe(doSideEffect)); * } * } * ``` * * NOTE: Avoid calling register/unregister/subscribe inside the side-effect function. */ class RxEffects { static { this.nextId = 0; } constructor() { this.destroyRef = inject(DestroyRef); this.errorHandler = inject(ErrorHandler, { optional: true }); this._hooks$ = new Subject(); this.observables$ = new Subject(); // we have to use publish here to make it hot (composition happens without subscriber) this.effects$ = this.observables$.pipe(mergeAll(), share()); this.subscription = this.effects$.subscribe(); this.onDestroy$ = this._hooks$.pipe(toHook('destroy')); this.destroyers = {}; this.destroyRef.onDestroy(() => { this._hooks$.next({ destroy: true }); this.subscription.unsubscribe(); }); } register(obsOrSub, fnOrObj) { if (obsOrSub instanceof Subscription) { this.subscription.add(obsOrSub); return; } const effectId = RxEffects.nextId++; const destroy$ = (this.destroyers[effectId] = new Subject()); const applyBehavior = pipe(map(() => effectId), takeUntil(destroy$)); if (fnOrObj != null) { this.observables$.next(from(obsOrSub).pipe( // ternary expression is to help Typescript infer overloads typeof fnOrObj === 'function' ? tap(fnOrObj) : tap(fnOrObj), catchError((err) => { this.errorHandler?.handleError(err); return EMPTY; }), applyBehavior)); } else { this.observables$.next(from(obsOrSub).pipe(applyBehavior)); } return effectId; } /** * Imperatively cancel a side-effect while the component is still running. * * Note that all effects are automatically cancelled when a component is destroyed, * so you most often won't need to call this method. * @param effectId Effect ID (returned by register method) */ unregister(effectId) { this.destroyers[effectId]?.next(); } /** * Fires a sideEffect when the instances `OnDestroy` hook is fired. * * @example * effects.registerOnDestroy(mode => localStorage.setItem('colorMode', mode)); * * @param sideEffect */ registerOnDestroy(sideEffect) { return this.register(this.onDestroy$, sideEffect); } /** * Operator that unsubscribes based on emission of an registered effect. * * @NOTICE * This operator has to be placed always at the end of the operator chain (before the subscription). * Otherwise we may leak as a subsequent operator could instantiate new ongoing Observables which will not get unsubscribed. * * @example * const effectId1 = effects.register( * colorMode$.subscribe(mode => localStorage.setItem('colorMode', mode)) * ); * * someValue$.pipe( * effect.untilEffect(effectId1) * ) * */ untilEffect(effectId) { return (source) => source.pipe(takeUntilDestroyed(this.destroyRef), takeUntil(this.effects$.pipe(filter((eId) => eId === effectId)))); } /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxEffects, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } /** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxEffects }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxEffects, decorators: [{ type: Injectable }], ctorParameters: () => [] }); /** * @description * Functional way to setup observable based side effects with RxEffects. * It's a creation function for RxEffects that destroys itself when the provided * `DestroyRef` is destroyed. * * @example * ```ts * import { rxEffects } from '@rx-angular/state/effects'; * * \@Component({}) * export class FooComponent { * const readonly util = inject(Util); * readonly effects = rxEffects(({ register }) => { * register(this.util.windowResize$, () => { * console.log('window was resized'); * }) * }); * * ngOnInit() { * this.effects.register(this.util.rotationChanged$, () => { * console.log('viewport rotation changed'); * }); * } * } * ``` * * @param {RxEffectsSetupFn} setupFn * @returns RxEffects * * @docsCategory RxEffects * @docsPage RxEffects * */ function rxEffects(setupFn) { assertInInjectionContext(rxEffects); const errorHandler = inject(ErrorHandler, { optional: true }); const destroyRef = inject(DestroyRef); const runningEffects = []; destroyRef.onDestroy(() => runningEffects.forEach((ef) => ef.unsubscribe())); /** * Subscribe to observables and trigger side effect. * * @example * * /@Component({ * template: `<button name="save" (click)="save()">Save</button>` * }) * class ListComponent { * private ef = rxEffects(({register}) => { * register(timer(0, this.backupInterval), console.log)); * } * } * * @param {SideEffectObservable} obs$ Source observable input * @param {SideEffectFnOrObserver} sideEffect Observer object * * @return {Function} - unregisterFn */ function register(obs$, sideEffect) { const observer = typeof sideEffect === 'object' ? { ...sideEffect, // preserve original logic error: (e) => { sideEffect.error?.(e); errorHandler?.handleError(e); }, } : { next: sideEffect, error: (e) => errorHandler?.handleError(e), }; const sub = from(obs$).subscribe(observer); runningEffects.push(sub); return () => sub.unsubscribe(); } /** * Register custom cleanup logic. * * @example * * /@Component({ * template: `<button name="save" (click)="save()">Save</button>` * }) * class ListComponent { * private ef = rxEffects(({onDestroy}) => { * onDestroy(() => console.log('done')); * } * } * * @param {Fn} callback onDestroy callback * * @return {Fn} unregisterFn */ function onDestroy(callback) { return destroyRef.onDestroy(callback); } const effects = { register, onDestroy, }; setupFn?.(effects); return effects; } /** * Generated bundle index. Do not edit. */ export { RxEffects, rxEffects }; //# sourceMappingURL=state-effects.mjs.map