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