UNPKG

@rx-angular/cdk

Version:

@rx-angular/cdk is a Component Development Kit for ergonomic and highly performant angular applications. It helps to to build Large scale applications, UI libs, state management, rendering systems and much more. Furthermore the unique way of mixing reacti

440 lines (431 loc) 15.8 kB
import { coalescingManager, coalesceWith } from '@rx-angular/cdk/coalescing'; import { forceFrameRate, scheduleCallback, cancelCallback } from '@rx-angular/cdk/internals/scheduler'; import { Observable, throwError, ReplaySubject, BehaviorSubject, fromEvent } from 'rxjs'; import { filter, switchMap, mapTo, tap, catchError, map, take, switchAll, startWith, share, shareReplay, takeUntil } from 'rxjs/operators'; import * as i0 from '@angular/core'; import { NgZone, InjectionToken, Injectable, Optional, Inject } from '@angular/core'; import { getZoneUnPatchedApi } from '@rx-angular/cdk/internals/core'; import { coerceAllFactory } from '@rx-angular/cdk/coercing'; // set default to 60fps forceFrameRate(60); const immediateStrategy = { name: 'immediate', work: (cdRef) => cdRef.detectChanges(), behavior: ({ work, scope, ngZone }) => { return (o$) => o$.pipe(scheduleOnQueue(work, { ngZone, priority: 1 /* PriorityLevel.ImmediatePriority */, scope, })); }, }; const userBlockingStrategy = { name: 'userBlocking', work: (cdRef) => cdRef.detectChanges(), behavior: ({ work, scope, ngZone }) => { return (o$) => o$.pipe(scheduleOnQueue(work, { ngZone, priority: 2 /* PriorityLevel.UserBlockingPriority */, scope, })); }, }; const normalStrategy = { name: 'normal', work: (cdRef) => cdRef.detectChanges(), behavior: ({ work, scope, ngZone }) => { return (o$) => o$.pipe(scheduleOnQueue(work, { ngZone, priority: 3 /* PriorityLevel.NormalPriority */, scope, })); }, }; const lowStrategy = { name: 'low', work: (cdRef) => cdRef.detectChanges(), behavior: ({ work, scope, ngZone }) => { return (o$) => o$.pipe(scheduleOnQueue(work, { ngZone, priority: 4 /* PriorityLevel.LowPriority */, scope, })); }, }; const idleStrategy = { name: 'idle', work: (cdRef) => cdRef.detectChanges(), behavior: ({ work, scope, ngZone }) => { return (o$) => o$.pipe(scheduleOnQueue(work, { ngZone, priority: 5 /* PriorityLevel.IdlePriority */, scope, })); }, }; function scheduleOnQueue(work, options) { const scope = options.scope || {}; return (o$) => o$.pipe(filter(() => !coalescingManager.isCoalescing(scope)), switchMap((v) => new Observable((subscriber) => { coalescingManager.add(scope); const task = scheduleCallback(options.priority, () => { work(); coalescingManager.remove(scope); subscriber.next(v); }, { delay: options.delay, ngZone: options.ngZone }); return () => { coalescingManager.remove(scope); cancelCallback(task); }; }).pipe(mapTo(v)))); } const RX_CONCURRENT_STRATEGIES = { immediate: immediateStrategy, userBlocking: userBlockingStrategy, normal: normalStrategy, low: lowStrategy, idle: idleStrategy, }; const animationFrameTick = () => new Observable((subscriber) => { // use the unpatched API no avoid zone interference const id = getZoneUnPatchedApi('requestAnimationFrame')(() => { subscriber.next(0); subscriber.complete(); }); return () => { // use the unpatched API no avoid zone interference getZoneUnPatchedApi('cancelAnimationFrame')(id); }; }); const localCredentials = { name: 'local', work: (cdRef, _, notification) => { cdRef.detectChanges(); }, behavior: ({ work, scope, ngZone }) => (o$) => o$.pipe(coalesceWith(animationFrameTick(), scope), tap(() => (ngZone ? ngZone.run(() => work()) : work()))), }; const noopCredentials = { name: 'noop', work: () => void 0, behavior: () => (o$) => o$, }; const nativeCredentials = { name: 'native', work: (cdRef) => cdRef.markForCheck(), behavior: ({ work, ngZone }) => (o$) => o$.pipe(tap(() => ngZone && !NgZone.isInAngularZone() ? ngZone.run(() => work()) : work())), }; const RX_NATIVE_STRATEGIES = { native: nativeCredentials, noop: noopCredentials, local: localCredentials, }; const RX_RENDER_STRATEGIES_CONFIG = new InjectionToken('rxa-render-strategies-config'); const RX_RENDER_STRATEGIES_DEFAULTS = { primaryStrategy: 'normal', customStrategies: { ...RX_NATIVE_STRATEGIES, ...RX_CONCURRENT_STRATEGIES, }, patchZone: true, parent: false, }; function mergeDefaultConfig(cfg) { const custom = cfg ? cfg : { customStrategies: {} }; return { ...RX_RENDER_STRATEGIES_DEFAULTS, ...custom, customStrategies: { ...custom.customStrategies, ...RX_RENDER_STRATEGIES_DEFAULTS.customStrategies, }, }; } /** * @description * Can be used to set the default render strategy or create custom render strategies. * * With this function you can customize the behavior of: * - `rxLet` directive * - `rxFor` directive * - `rxIf` directive * - `rxVirtualFor` directive * - `rxVirtualView` directive * - `RxStrategyProvider` service. * * @example * import { provideRxRenderStrategies } from '@rx-angular/cdk/render-strategies'; * * const appConfig: ApplicationConfig = { * providers: [ * provideRxRenderStrategies({ * primaryStrategy: 'sync', * customStrategies: { * sync: { * name: 'sync', * work: (cdRef) => { cdRef.detectChanges(); }, * behavior: ({ work }) => (o$) => o$.pipe(tap(() => work())) * }, * asap: { * name: 'asap', * work: (cdRef) => { cdRef.detectChanges(); }, * behavior: ({ work }) => (o$) => o$.pipe(delay(0, asapScheduler), tap(() => work())) * }, * }), * ], * }; * * @param config - The configuration object. * @returns A provider that can be used to set the default render strategy or create custom render strategies. */ function provideRxRenderStrategies(config) { return { provide: RX_RENDER_STRATEGIES_CONFIG, useValue: mergeDefaultConfig(config), }; } /** * @internal * * @param value * @param strategy * @param workFactory * @param options */ function onStrategy(value, strategy, workFactory, options = {}) { return new Observable((subscriber) => { subscriber.next(value); }).pipe(strategy.behavior({ work: () => workFactory(value, strategy.work, options), scope: options.scope || {}, ngZone: options.ngZone, }), catchError((error) => throwError(() => [error, value])), map(() => value), take(1)); } /** * @internal * * A factory function returning an object to handle the process of turning strategy names into `RxStrategyCredentials` * You can next a strategy name as Observable or string and get an Observable of `RxStrategyCredentials` * * @param defaultStrategyName * @param strategies */ function strategyHandling(defaultStrategyName, strategies) { const hotFlattened = coerceAllFactory(() => new ReplaySubject(1), switchAll()); return { strategy$: hotFlattened.values$.pipe(startWith(defaultStrategyName), nameToStrategyCredentials(strategies, defaultStrategyName), share()), next(name) { hotFlattened.next(name); }, }; } /** * @internal */ function nameToStrategyCredentials(strategies, defaultStrategyName) { return (o$) => o$.pipe(map((name) => name && Object.keys(strategies).includes(name) ? strategies[name] : strategies[defaultStrategyName])); } /** * @description * RxStrategyProvider is a wrapper service that you can use to consume strategies and schedule your code execution. * * @example * Component({ * selector: 'app-service-communicator', * template: `` * }); * export class ServiceCommunicationComponent { * private currentUserSettings; * * constructor( * private strategyProvider: RxStrategyProvider, * private userService: UserService, * private backgroundSync: BackgroundSyncService * ) { * this.userService.fetchCurrentUserSettings * .pipe( * tap(settings => (this.currentUserSettings = settings)), * this.strategyProvider.scheduleWith( * settings => this.backgroundSync.openConnection(settings), * { strategy: 'idle' } * ) * ) * .subscribe(); * } * } * * @docsCategory RxStrategyProvider * @docsPage RxStrategyProvider */ class RxStrategyProvider { /** * @description * Returns current `RxAngularConfig` used in the service. * Config includes: * - strategy that currently in use - `primaryStrategy` * - array of custom user defined strategies - `customStrategies` * - setting that is responsible for running in our outside of the zone.js - `patchZone` */ get config() { return this._cfg; } /** * @description * Returns object that contains key-value pairs of strategy names and their credentials (settings) that are available in the service. */ get strategies() { return this._strategies$.getValue(); } /** * @description * Returns an array of strategy names available in the service. */ get strategyNames() { return Object.values(this.strategies).map((s) => s.name); } /** * @description * Returns current strategy of the service. */ get primaryStrategy() { return this._primaryStrategy$.getValue().name; } /** * @description * Set's the strategy that will be used by the service. */ set primaryStrategy(strategyName) { this._primaryStrategy$.next(this.strategies[strategyName]); } /** * @internal */ constructor(cfg) { this._strategies$ = new BehaviorSubject(undefined); this._primaryStrategy$ = new BehaviorSubject(undefined); /** * @description * Current strategy of the service as an observable. */ this.primaryStrategy$ = this._primaryStrategy$.asObservable(); /** * @description * Returns observable of an object that contains key-value pairs of strategy names and their credentials (settings) that are available in the service. */ this.strategies$ = this._strategies$.asObservable(); /** * @description * Returns an observable of an array of strategy names available in the service. */ this.strategyNames$ = this.strategies$.pipe(map((strategies) => Object.values(strategies).map((s) => s.name)), shareReplay({ bufferSize: 1, refCount: true })); this._cfg = mergeDefaultConfig(cfg); this._strategies$.next(this._cfg.customStrategies); this.primaryStrategy = this.config.primaryStrategy; } /** * @description * Allows to schedule a work inside rxjs `pipe`. Accepts the work and configuration options object. * - work is any function that should be executed * - (optional) options includes strategy, patchZone and scope * * Scope is by default a subscription but you can also pass `this` and then the scope will be current component. * Scope setup is useful if your work is some of the methods of `ChangeDetectorRef`. Only one change detection will be triggered if you have multiple schedules of change detection methods and scope is set to `this`. * * @example * myObservable$.pipe( * this.strategyProvider.scheduleWith(() => myWork(), {strategy: 'idle', patchZone: false}) * ).subscribe(); * * @return MonoTypeOperatorFunction<R> */ scheduleWith(work, options) { const strategy = this.strategies[options?.strategy || this.primaryStrategy]; const scope = options?.scope || {}; const _work = getWork(work, options?.patchZone); const ngZone = options?.patchZone || undefined; return (o$) => o$.pipe(switchMap((v) => onStrategy(v, strategy, (_v) => { _work(_v); }, { scope, ngZone }))); } /** * @description * Allows to schedule a work as an observable. Accepts the work and configuration options object. * - work is any function that should be executed * - (optional) options includes strategy, patchZone and scope * * Scope is by default a subscription but you can also pass `this` and then the scope will be current component. * Scope setup is especially useful if you provide work that will trigger a change detection. * * @example * this.strategyProvider.schedule(() => myWork(), {strategy: 'idle', patchZone: false}).subscribe(); * * @return Observable<R> */ schedule(work, options) { const strategy = this.strategies[options?.strategy || this.primaryStrategy]; const scope = options?.scope || {}; const _work = getWork(work, options?.patchZone); const ngZone = options?.patchZone || undefined; let returnVal; return onStrategy(null, strategy, () => { returnVal = _work(); }, { scope, ngZone }).pipe(map(() => returnVal)); } /** * @description * Allows to schedule a change detection cycle. Accepts the ChangeDetectorRef and configuration options object. * Options include: * - afterCD which is the work that should be executed after change detection cycle. * - abortCtrl is an AbortController that you can use to cancel the scheduled cycle. * * @example * this.strategyProvider.scheduleCd(this.changeDetectorRef, {afterCD: myWork()}); * * @return AbortController */ scheduleCD(cdRef, options) { const strategy = this.strategies[options?.strategy || this.primaryStrategy]; const scope = options?.scope || cdRef; const abC = options?.abortCtrl || new AbortController(); const ngZone = options?.patchZone || undefined; const work = getWork(() => { strategy.work(cdRef, scope); if (options?.afterCD) { options.afterCD(); } }, options.patchZone); onStrategy(null, strategy, () => { work(); }, { scope, ngZone }) .pipe(takeUntil(fromEvent(abC.signal, 'abort'))) .subscribe(); return abC; } /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxStrategyProvider, deps: [{ token: RX_RENDER_STRATEGIES_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); } /** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxStrategyProvider, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxStrategyProvider, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [{ type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [RX_RENDER_STRATEGIES_CONFIG] }] }] }); function getWork(work, patchZone) { let _work = work; if (patchZone) { _work = (args) => patchZone.run(() => work(args)); } return _work; } /** * Generated bundle index. Do not edit. */ export { RX_CONCURRENT_STRATEGIES, RX_NATIVE_STRATEGIES, RX_RENDER_STRATEGIES_CONFIG, RxStrategyProvider, onStrategy, provideRxRenderStrategies, strategyHandling }; //# sourceMappingURL=cdk-render-strategies.mjs.map