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

273 lines (266 loc) 10 kB
import * as i0 from '@angular/core'; import { inject, DestroyRef, Injectable, Optional, assertInInjectionContext, ErrorHandler } from '@angular/core'; import { Subject, merge } from 'rxjs'; /** * @internal * Internal helper to create the proxy object * It lives as standalone function because we don't need to carrie it in memory for every ActionHandler instance * @param subjects * @param transforms */ function actionProxyHandler({ subjectMap, transformsMap, effectMap, errorHandler = null, }) { function getEventEmitter(prop) { if (!subjectMap[prop]) { subjectMap[prop] = new Subject(); } return subjectMap[prop]; } function dispatch(value, prop) { subjectMap[prop] = subjectMap[prop] || new Subject(); try { const val = transformsMap && transformsMap[prop] ? transformsMap[prop](value) : value; subjectMap[prop].next(val); } catch (err) { errorHandler?.handleError(err); } } return { // shorthand setter for multiple EventEmitter e.g. actions({propA: 1, propB: 2}) apply(_, __, props) { props.forEach((slice) => Object.entries(slice).forEach(([k, v]) => dispatch(v, k))); }, get(_, property) { const prop = property; // the user wants to get multiple or one single EventEmitter as observable `eventEmitter.prop$` if (prop.toString().split('').pop() === '$') { // the user wants to get multiple EventEmitter as observable `eventEmitter.$(['prop1', 'prop2'])` if (prop.toString().length === 1) { return (props) => merge(...props.map((k) => { return getEventEmitter(k); })); } // the user wants to get a single EventEmitter as observable `eventEmitter.prop$` const propName = prop.toString().slice(0, -1); return getEventEmitter(propName); } // the user wants to get a single EventEmitter and trigger a side effect on event emission if (prop.toString().startsWith('on')) { // we need to first remove the 'on' from the the prop name const slicedPropName = prop.toString().slice(2); // now convert the slicedPropName to camelcase const propName = (slicedPropName.charAt(0).toLowerCase() + slicedPropName.slice(1)); return (behaviour, sf) => { const sub = getEventEmitter(propName).pipe(behaviour).subscribe(sf); effectMap[propName] = sub; return () => sub.unsubscribe(); }; } // the user wants to get a dispatcher function to imperatively dispatch the EventEmitter return (args) => { dispatch(args, prop); }; }, set() { throw new Error('No setters available. To emit call the property name.'); }, }; } /** * @deprecated - use rxActions instead * * This class creates RxActions bound to Angular's DI life-cycles. This prevents memory leaks and optionally makes the instance reusable across the app. * The function has to be used inside an injection context. * If the consumer gets destroyed also the actions get destroyed automatically. * * @example * @Component({ * standalone: true, * template: `...`, * }) * export class AnyComponent { * ui = rxActions<{search: string, refresh: void}>(); * } */ class RxActionFactory { constructor(errorHandler) { this.errorHandler = errorHandler; this.subjects = []; inject(DestroyRef).onDestroy(() => this.destroy()); } /* * Returns a object based off of the provided typing with a separate setter `[prop](value: T[K]): void` and observable stream `[prop]$: Observable<T[K]>`; * * { search: string } => { search$: Observable<string>, search: (value: string) => void;} * * @example * * interface UIActions { * search: string, * submit: void * }; * * const actions = new RxActionFactory<UIActions>().create(); * * actions.search($event.target.value); * actions.search$.subscribe(); * * As it is well typed the following things would not work: * actions.submit('not void'); // not void * actions.search(); // requires an argument * actions.search(42); // not a string * actions.search$.error(new Error('traraaa')); // not possible by typings as well as in code * actions.search = "string"; // not a setter. the proxy will throw an error pointing out that you have to call it * * @param transforms - A map of transform functions to apply on transformations to actions before emitting them. * This is very useful to clean up bloated templates and components. e.g. `[input]="$event?.target?.value"` => `[input]="$event"` * * @example * function coerceSearchActionParams(e: Event | string | number): string { * if(e?.target?.value !== undefined) { * return e?.target?.value + '' * } * return e + ''; * } * const actions = getActions<search: string, submit: void>({search: coerceSearchActionParams, submit: (v: any) => void 0;}); * * actions.search($event); * actions.search('string'); * actions.search(42); * actions.submit('not void'); // does not error anymore * actions.search$.subscribe(); // string Observable * */ create(transforms) { const subjectMap = {}; const effectMap = {}; this.subjects.push(subjectMap); // eslint-disable-next-line @typescript-eslint/no-empty-function function signals() { } return new Proxy(signals, actionProxyHandler({ subjectMap, effectMap, transformsMap: transforms, errorHandler: this.errorHandler ?? null, })); } destroy() { this.subjects.forEach((s) => { Object.values(s).forEach((subject) => subject.complete()); }); } /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxActionFactory, deps: [{ token: i0.ErrorHandler, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); } /** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxActionFactory }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxActionFactory, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: i0.ErrorHandler, decorators: [{ type: Optional }] }] }); /** * Manage events in components and services in a single place * * @example * * interface UI { * search: string, * submit: void * }; * * import { rxActions } from '@rx-angular/state/actions'; * * @Component({...}) * export class Component { * ui = rxActions<{ name: string }>(({transforms}) => transforms({name: v => v})); * * name$ = this.ui.name$; // Observable<string> - listens to name changes * emitName = this.ui.name; // (name: string) => void - emits name change * sub = this.ui.onName(o$ => o$.pipe(), console.log) // () => void - stops side effect * * onInit() { * const name$ = this.ui.name$; // Observable<string> - listens to name changes * const emitName = this.ui.name; // (name: string) => void - emits name change * const stop = this.ui.onName(o$ => o$.pipe(), console.log) // () => void - stops side effect * stop(); * } * * } * */ function rxActions(setupFn) { // Assert rxAction usage assertInInjectionContext(rxActions); const subjectMap = {}; const effectMap = {}; const errorHandler = inject(ErrorHandler); let transformsMap = {}; /** * @internal * Internally used to clean up potential subscriptions to the subjects. (For Actions it is most probably a rare case but still important to care about) */ inject(DestroyRef).onDestroy(() => { Object.values(subjectMap).forEach((subject) => subject.complete()); }); // run setup function if given // eslint-disable-next-line @typescript-eslint/no-unused-expressions setupFn && setupFn({ transforms: (t) => (transformsMap = t), }); // create actions function signals() { } return new Proxy(signals, actionProxyHandler({ subjectMap, transformsMap, effectMap, errorHandler, })); } /** * @description * This transform is a side effecting operation applying `preventDefault` to a passed Event * @param e */ function preventDefault(e) { e.preventDefault(); return e; } /** * @description * This transform is a side effecting operation applying `stopPropagation` to a passed Event * @param e */ function stopPropagation(e) { e.stopPropagation(); return e; } /** * @description * This transform is a side effecting operation applying `preventDefault` and `stopPropagation` to a passed Event * @param e */ function preventDefaultStopPropagation(e) { e.stopPropagation(); e.preventDefault(); return e; } /** * @description * This transform is helps to pluck values from DOM `Event` or forward the value directly. * @param e */ function eventValue(e) { // Consider https://stackoverflow.com/questions/1458894/how-to-determine-if-javascript-object-is-an-event if (e?.target) { return e?.target?.value; } return e; } /** * Generated bundle index. Do not edit. */ export { RxActionFactory, eventValue, preventDefault, preventDefaultStopPropagation, rxActions, stopPropagation }; //# sourceMappingURL=rx-angular-state-actions.mjs.map