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

464 lines (457 loc) 15.9 kB
import * as i0 from '@angular/core'; import { InjectionToken, inject, DestroyRef, signal, Injector, isSignal, computed, Injectable, assertInInjectionContext } from '@angular/core'; import { defaultAccumulator, select, createAccumulationObservable, createSideEffectObservable, safePluck, isKeyOf } from '@rx-angular/state/selections'; import { queueScheduler, Subscription, Subject, BehaviorSubject, isObservable, EMPTY } from 'rxjs'; import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { toObservableMicrotaskInternal } from '@rx-angular/cdk/internals/core'; import { map, catchError, tap } from 'rxjs/operators'; /** * Injection token for the default accumulator function. * * @example * providers: [ * { * provide: RX_ACCUMULATOR_FN, * useValue: (state, slice) => ({ ...state, ...slice }) * } * ] */ const RX_ACCUMULATOR_FN = new InjectionToken('RX_ACCUMULATOR_FN', { providedIn: 'root', factory: () => defaultAccumulator, }); /** * Provider function to specify a custom `AccumulationFn` for `RxState` to use. * @param fn */ function withAccumulatorFn(fn) { return { kind: 'Accumulator', providers: [{ provide: RX_ACCUMULATOR_FN, useValue: fn }], }; } /** * Injection token for the default state scheduler * * @example * providers: [ * { * provide: RX_STATE_SCHEDULER, * useValue: asapScheduler * } * ] */ const RX_STATE_SCHEDULER = new InjectionToken('RX_STATE_SCHEDULER', { providedIn: 'root', factory: () => queueScheduler, }); /** * Provider function to specify a scheduler for `RxState` to perform state updates & emit new values. * @param scheduler */ function withScheduler(scheduler) { return { kind: 'Scheduler', providers: [{ provide: RX_STATE_SCHEDULER, useValue: scheduler }], }; } /** * Provider function to specify synchronous (no) scheduling for `RxState`. The state computations * will be fully synchronous instead of using the default `queueScheduler` */ function withSyncScheduler() { return { kind: 'Scheduler', providers: [{ provide: RX_STATE_SCHEDULER, useValue: 'sync' }], }; } /** * This function is used to provide the configuration for the rxState function. * * You can provide multiple configurations at once. * * You can use these functions to provide the configuration: * - withAccumulatorFn - to provide a custom accumulator function * - withScheduler - to provide a custom scheduler * */ function provideRxStateConfig(...configs) { return flatten(configs.map((c) => c.providers)); } function flatten(arr) { return arr.reduce((acc, val) => acc.concat(val), []); } function createSignalStateProxy(state$, stateFn) { const destroyRef = inject(DestroyRef); const signalState = {}; return new Proxy(signalState, { get(target, p) { let _signal = target[p]; if (!_signal) { const val = stateFn(p); _signal = signal(val); target[p] = _signal; state$ .pipe(select(p), takeUntilDestroyed(destroyRef)) .subscribe((val) => _signal.set(val)); } return _signal; }, has(target, prop) { return !!target[prop]; }, ownKeys(target) { return [...Reflect.ownKeys(target)]; }, getOwnPropertyDescriptor(target, key) { return { enumerable: true, configurable: true, }; }, set() { return true; }, }); } /** * @description * RxState is a light-weight reactive state management service for managing local state in angular. * * @example * Component({ * selector: 'app-stateful', * template: `<div>{{ state$ | async | json }}</div>`, * providers: [RxState] * }) * export class StatefulComponent { * readonly state$ = this.state.select(); * * constructor(private state: RxState<{ foo: string }>) {} * } * * @docsCategory RxState * @docsPage RxState */ class RxState { /** * @internal */ constructor() { this.subscription = new Subscription(); this.scheduler = inject(RX_STATE_SCHEDULER, { optional: true }); this.accumulator = createAccumulationObservable(new Subject(), new Subject(), new BehaviorSubject(inject(RX_ACCUMULATOR_FN)), this.scheduler === 'sync' ? null : this.scheduler); this.effectObservable = createSideEffectObservable(new Subject(), this.scheduler === 'sync' ? null : this.scheduler); this.injector = inject(Injector); /** * @description * The unmodified state exposed as `Observable<State>`. It is not shared, distinct or gets replayed. * Use the `$` property if you want to read the state without having applied {@link stateful} to it. */ this.$ = this.accumulator.signal$; this.subscription.add(this.subscribe()); inject(DestroyRef).onDestroy(() => this.subscription.unsubscribe()); } /** * @description * * Return RxState in ReadOnly mode exposing only methods for reading state * get(), select(), computed() and signal() methods. * This can be helpful when you don't want others to write in your state. * * @example * ```typescript * const readOnlyState = state.asReadOnly(); * const getNum = state.get('num'); * const selectNum$ = state.select('num'); * ``` * * @return Pick<RxState<State>, ReadOnly> */ asReadOnly() { return { get: this.get.bind(this), select: this.select.bind(this), computed: this.computed.bind(this), signal: this.signal.bind(this), }; } /** * @description * * Allows to customize state accumulation function. * This can be helpful to implement deep updates and tackle other immutability problems in a custom way. * @example * * ```typescript * const myAccumulator = (state: MyState, slice: Partial<MyState>) => deepCopy(state, slice); * * this.state.setAccumulator(myAccumulator); * ``` * * @param {AccumulationFn} accumulatorFn * @return void * * @deprecated * Use `provideRxStateConfig` and provide the accumulator with the `withAccumulator` provider function. * Will be removed in future versions. */ setAccumulator(accumulatorFn) { this.accumulator.nextAccumulator(accumulatorFn); } /** @internal **/ get(...keys) { const hasStateAnyKeys = Object.keys(this.accumulator.state).length > 0; if (!!keys && keys.length) { return safePluck(this.accumulator.state, keys); } else { return hasStateAnyKeys ? this.accumulator.state : undefined; } } /** * @internal */ set(keyOrStateOrProjectState, stateOrSliceProjectFn) { if (typeof keyOrStateOrProjectState === 'object' && stateOrSliceProjectFn === undefined) { this.accumulator.nextSlice(keyOrStateOrProjectState); return; } if (typeof keyOrStateOrProjectState === 'function' && stateOrSliceProjectFn === undefined) { this.accumulator.nextSlice(keyOrStateOrProjectState(this.accumulator.state)); return; } if (isKeyOf(keyOrStateOrProjectState) && typeof stateOrSliceProjectFn === 'function') { const state = {}; state[keyOrStateOrProjectState] = stateOrSliceProjectFn(this.accumulator.state); this.accumulator.nextSlice(state); return; } throw new Error('wrong params passed to set'); } /** * @internal */ connect(keyOrInputOrSlice$, projectOrSlices$, projectValueFn) { /** * From top to bottom the overloads are handled. */ if (isObservable(keyOrInputOrSlice$) && !projectOrSlices$ && !projectValueFn) { this.accumulator.nextSliceObservable(keyOrInputOrSlice$); return; } if (isSignal(keyOrInputOrSlice$) && !projectOrSlices$ && !projectValueFn) { this.accumulator.nextSliceObservable(toObservableMicrotaskInternal(keyOrInputOrSlice$, { injector: this.injector, })); return; } if (isObservable(keyOrInputOrSlice$) && projectOrSlices$ && typeof projectOrSlices$ === 'function' && !projectValueFn) { const projectionStateFn = projectOrSlices$; const slice$ = keyOrInputOrSlice$.pipe(map((v) => projectionStateFn(this.accumulator.state, v))); this.accumulator.nextSliceObservable(slice$); return; } if (isSignal(keyOrInputOrSlice$) && projectOrSlices$ && typeof projectOrSlices$ === 'function' && !projectValueFn) { const projectionStateFn = projectOrSlices$; const slice$ = toObservableMicrotaskInternal(keyOrInputOrSlice$, { injector: this.injector, }).pipe(map((v) => projectionStateFn(this.accumulator.state, v))); this.accumulator.nextSliceObservable(slice$); return; } if (isKeyOf(keyOrInputOrSlice$) && isObservable(projectOrSlices$) && !projectValueFn) { const slice$ = projectOrSlices$.pipe(map((value) => ({ ...{}, [keyOrInputOrSlice$]: value }))); this.accumulator.nextSliceObservable(slice$); return; } if (isKeyOf(keyOrInputOrSlice$) && isSignal(projectOrSlices$) && !projectValueFn) { const slice$ = toObservableMicrotaskInternal(projectOrSlices$, { injector: this.injector, }).pipe(map((value) => ({ ...{}, [keyOrInputOrSlice$]: value }))); this.accumulator.nextSliceObservable(slice$); return; } if (projectValueFn && typeof projectValueFn === 'function' && isKeyOf(keyOrInputOrSlice$) && isObservable(projectOrSlices$)) { const key = keyOrInputOrSlice$; const slice$ = projectOrSlices$.pipe(map((value) => ({ ...{}, [key]: projectValueFn(this.get(), value), }))); this.accumulator.nextSliceObservable(slice$); return; } if (projectValueFn && typeof projectValueFn === 'function' && isKeyOf(keyOrInputOrSlice$) && isSignal(projectOrSlices$)) { const key = keyOrInputOrSlice$; const slice$ = toObservableMicrotaskInternal(projectOrSlices$, { injector: this.injector, }).pipe(map((value) => ({ ...{}, [key]: projectValueFn(this.get(), value), }))); this.accumulator.nextSliceObservable(slice$); return; } throw new Error('wrong params passed to connect'); } /** * @internal */ select(...args) { return this.accumulator.state$.pipe(select(...args)); } /** * @description * Returns a signal of the given key. It's first value is determined by the * current keys value in RxState. Whenever the key gets updated, the signal * will also be updated accordingly. * * @example * const fooSignal = state.signal('foo'); * * @param {Key} key * * @return Signal<State[Key]> */ signal(key) { return this.signalStoreProxy[key]; } /** * @description * Lets you create a computed signal based off multiple keys stored in RxState. * * @example * const computedSignal = state.computed((s) => s.foo + s.bar); * * @param {(slice: SignalStateProxy<Type>) => ComputedType} fn * @return Signal<ComputedType> */ computed(fn) { return computed(() => { return fn(this.signalStoreProxy); }); } /** @internal */ computedFrom(...ops) { return toSignal(this.select(...ops), { injector: this.injector, requireSync: true, }); } /** * @description * Manages side-effects of your state. Provide an `Observable<any>` * **side-effect** and an optional `sideEffectFunction`. * Subscription handling is done automatically. * * @example * // Directly pass an observable side-effect * const localStorageEffect$ = changes$.pipe( * tap(changes => storeChanges(changes)) * ); * state.hold(localStorageEffect$); * * // Pass an additional `sideEffectFunction` * * const localStorageEffectFn = changes => storeChanges(changes); * state.hold(changes$, localStorageEffectFn); * * @param {Observable<SideEffect>} obsOrObsWithSideEffect * @param {function} [sideEffectFn] * * @return void */ hold(obsOrObsWithSideEffect, sideEffectFn) { const sideEffect = obsOrObsWithSideEffect.pipe(catchError((e) => EMPTY)); if (typeof sideEffectFn === 'function') { this.effectObservable.nextEffectObservable(sideEffect.pipe(tap(sideEffectFn))); return; } this.effectObservable.nextEffectObservable(sideEffect); } /** * @internal */ subscribe() { const subscription = new Subscription(); subscription.add(this.accumulator.subscribe()); subscription.add(this.effectObservable.subscribe()); this.signalStoreProxy = createSignalStateProxy(this.$, this.get.bind(this)); return subscription; } /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxState, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } /** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxState }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxState, decorators: [{ type: Injectable }], ctorParameters: () => [] }); /** * @description * Functional way to setup state management with RxState. It's a wrapper around RxState that automatically get * destroyed. * * @example * ```ts * import { rxState } from '@rx-angular/state'; * * Component({}) * export class FooComponent { * readonly state = rxState<{ count: number }>(({ set }) => set({ count: 0 })); * } * ``` * * @param setupFn * @returns RxState instance * * * * @docsCategory RxState * @docsPage RxState * */ function rxState(setupFn) { assertInInjectionContext(rxState); const legacyState = new RxState(); const state = { get: legacyState.get.bind(legacyState), set: legacyState.set.bind(legacyState), connect: legacyState.connect.bind(legacyState), select: legacyState.select.bind(legacyState), signal: legacyState.signal.bind(legacyState), computed: legacyState.computed.bind(legacyState), computedFrom: legacyState.computedFrom.bind(legacyState), $: legacyState.$, setAccumulator: legacyState.setAccumulator.bind(legacyState), asReadOnly: legacyState.asReadOnly.bind(legacyState), }; setupFn?.(state); return state; } /** * Generated bundle index. Do not edit. */ export { RX_ACCUMULATOR_FN, RX_STATE_SCHEDULER, RxState, provideRxStateConfig, rxState, withAccumulatorFn, withScheduler, withSyncScheduler }; //# sourceMappingURL=state.mjs.map