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