@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
JavaScript
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