From 6ec891bf4f1cfb0a69dbc623c5f75c2cbc44ce95 Mon Sep 17 00:00:00 2001 From: Artur Date: Mon, 18 Nov 2024 19:52:10 +0000 Subject: [PATCH] fix(store): allow plain functions in `withNgxsPlugin` (#2255) We allow passing a plain function into `withNgxsPlugin`, as the plugin can be a simple function (not a class). --- CHANGELOG.md | 1 + docs/concepts/store/meta-reducer.md | 14 ++------- docs/plugins/README.md | 14 ++++++--- .../devtools-plugin/src/devtools.module.ts | 7 +---- packages/form-plugin/src/form.module.ts | 18 ++--------- packages/logger-plugin/src/logger.module.ts | 7 +---- packages/storage-plugin/src/storage.module.ts | 7 +---- packages/store/plugins/src/index.ts | 8 ++++- packages/store/plugins/src/symbols.ts | 31 +++++++++++++------ packages/store/src/plugin-manager.ts | 2 +- .../store/src/standalone-features/plugin.ts | 14 ++++++--- .../src/standalone-features/root-providers.ts | 2 -- packages/store/tests/plugins.spec.ts | 15 ++------- 13 files changed, 61 insertions(+), 79 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b01e5bc59..abbc39b9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ $ npm install @ngxs/store@dev - Refactor: Use field initializers for injectees [#2258](https://github.com/ngxs/store/pull/2258) - Refactor: Allow tree-shaking of dev-only code [#2259](https://github.com/ngxs/store/pull/2259) +- Fix(store): Allow plain functions in `withNgxsPlugin` [#2255](https://github.com/ngxs/store/pull/2255) - Fix(store): Run plugins in injection context [#2256](https://github.com/ngxs/store/pull/2256) - Fix(websocket-plugin): Do not dispatch action when root injector is destroyed [#2257](https://github.com/ngxs/store/pull/2257) - Refactor(store): Replace `exhaustMap` [#2254](https://github.com/ngxs/store/pull/2254) diff --git a/docs/concepts/store/meta-reducer.md b/docs/concepts/store/meta-reducer.md index ee420ba88..e0c5b4ee8 100644 --- a/docs/concepts/store/meta-reducer.md +++ b/docs/concepts/store/meta-reducer.md @@ -19,21 +19,13 @@ export function logoutPlugin(state, action, next) { } ``` -Then add it to `providers`: +Then add it to `provideStore` features: ```ts -import { NGXS_PLUGINS } from '@ngxs/store/plugins'; +import { provideStore, withNgxsPlugin } from '@ngxs/store'; export const appConfig: ApplicationConfig = { - providers: [ - provideStore([]), - - { - provide: NGXS_PLUGINS, - useValue: logoutPlugin, - multi: true - } - ] + providers: [provideStore([], withNgxsPlugin(logoutPlugin))] }; ``` diff --git a/docs/plugins/README.md b/docs/plugins/README.md index 6026e6428..e39b42de8 100644 --- a/docs/plugins/README.md +++ b/docs/plugins/README.md @@ -9,7 +9,7 @@ Let's take a look at a basic example of a logger: ```ts import { makeEnvironmentProviders, InjectionToken, Injectable, Inject } from '@angular/core'; import { withNgxsPlugin } from '@ngxs/store'; -import { NgxsPlugin } from '@ngxs/store/plugins'; +import { NgxsPlugin, NgxsNextPluginFn } from '@ngxs/store/plugins'; export const NGXS_LOGGER_PLUGIN_OPTIONS = new InjectionToken('NGXS_LOGGER_PLUGIN_OPTIONS'); @@ -17,7 +17,7 @@ export const NGXS_LOGGER_PLUGIN_OPTIONS = new InjectionToken('NGXS_LOGGER_PLUGIN export class LoggerPlugin implements NgxsPlugin { constructor(@Inject(NGXS_LOGGER_PLUGIN_OPTIONS) private options: any) {} - handle(state, action, next) { + handle(state: any, action: any, next: NgxsNextPluginFn) { console.log('Action started!', state); return next(state, action).pipe( tap(result => { @@ -41,7 +41,13 @@ export function withNgxsLoggerPlugin(options?: any) { You can also use pure functions for plugins. The above example in a pure function would look like this: ```ts -export function logPlugin(state, action, next) { +import { NgxsNextPluginFn } from '@ngxs/store/plugins'; + +export function logPlugin(state: any, action: any, next: NgxsNextPluginFn) { + // Note that plugin functions are called within an injection context, + // allowing you to inject dependencies. + const options = inject(NGXS_LOGGER_PLUGIN_OPTIONS); + console.log('Action started!', state); return next(state, action).pipe(tap(result) => { console.log('Action happened!', result); @@ -49,8 +55,6 @@ export function logPlugin(state, action, next) { } ``` -NOTE: When providing a pure function make sure to use `useValue` instead of `useClass`. - To register a plugin with NGXS, add the plugin to your `provideStore` as an NGXS feature and optionally pass in the plugin options like this: ```ts diff --git a/packages/devtools-plugin/src/devtools.module.ts b/packages/devtools-plugin/src/devtools.module.ts index 4239299a8..929a2b6fa 100644 --- a/packages/devtools-plugin/src/devtools.module.ts +++ b/packages/devtools-plugin/src/devtools.module.ts @@ -6,7 +6,6 @@ import { makeEnvironmentProviders } from '@angular/core'; import { withNgxsPlugin } from '@ngxs/store'; -import { NGXS_PLUGINS } from '@ngxs/store/plugins'; import { NgxsDevtoolsOptions, NGXS_DEVTOOLS_OPTIONS } from './symbols'; import { NgxsReduxDevtoolsPlugin } from './devtools.plugin'; @@ -28,11 +27,7 @@ export class NgxsReduxDevtoolsPluginModule { return { ngModule: NgxsReduxDevtoolsPluginModule, providers: [ - { - provide: NGXS_PLUGINS, - useClass: NgxsReduxDevtoolsPlugin, - multi: true - }, + withNgxsPlugin(NgxsReduxDevtoolsPlugin), { provide: USER_OPTIONS, useValue: options diff --git a/packages/form-plugin/src/form.module.ts b/packages/form-plugin/src/form.module.ts index eb0fcae1f..a06f9c035 100644 --- a/packages/form-plugin/src/form.module.ts +++ b/packages/form-plugin/src/form.module.ts @@ -1,11 +1,5 @@ -import { - NgModule, - ModuleWithProviders, - EnvironmentProviders, - makeEnvironmentProviders -} from '@angular/core'; +import { NgModule, ModuleWithProviders, EnvironmentProviders } from '@angular/core'; import { withNgxsPlugin } from '@ngxs/store'; -import { NGXS_PLUGINS } from '@ngxs/store/plugins'; import { NgxsFormPlugin } from './form.plugin'; import { NgxsFormDirective } from './directive'; @@ -18,17 +12,11 @@ export class NgxsFormPluginModule { static forRoot(): ModuleWithProviders { return { ngModule: NgxsFormPluginModule, - providers: [ - { - provide: NGXS_PLUGINS, - useClass: NgxsFormPlugin, - multi: true - } - ] + providers: [withNgxsPlugin(NgxsFormPlugin)] }; } } export function withNgxsFormPlugin(): EnvironmentProviders { - return makeEnvironmentProviders([withNgxsPlugin(NgxsFormPlugin)]); + return withNgxsPlugin(NgxsFormPlugin); } diff --git a/packages/logger-plugin/src/logger.module.ts b/packages/logger-plugin/src/logger.module.ts index bb4be3b1e..8432d1d0a 100644 --- a/packages/logger-plugin/src/logger.module.ts +++ b/packages/logger-plugin/src/logger.module.ts @@ -6,7 +6,6 @@ import { makeEnvironmentProviders } from '@angular/core'; import { withNgxsPlugin } from '@ngxs/store'; -import { NGXS_PLUGINS } from '@ngxs/store/plugins'; import { NgxsLoggerPlugin } from './logger.plugin'; import { NgxsLoggerPluginOptions, NGXS_LOGGER_PLUGIN_OPTIONS } from './symbols'; @@ -35,11 +34,7 @@ export class NgxsLoggerPluginModule { return { ngModule: NgxsLoggerPluginModule, providers: [ - { - provide: NGXS_PLUGINS, - useClass: NgxsLoggerPlugin, - multi: true - }, + withNgxsPlugin(NgxsLoggerPlugin), { provide: USER_OPTIONS, useValue: options diff --git a/packages/storage-plugin/src/storage.module.ts b/packages/storage-plugin/src/storage.module.ts index 1b7da5ab2..98fe4b06e 100644 --- a/packages/storage-plugin/src/storage.module.ts +++ b/packages/storage-plugin/src/storage.module.ts @@ -6,7 +6,6 @@ import { makeEnvironmentProviders } from '@angular/core'; import { withNgxsPlugin } from '@ngxs/store'; -import { NGXS_PLUGINS } from '@ngxs/store/plugins'; import { ɵUSER_OPTIONS, STORAGE_ENGINE, @@ -25,11 +24,7 @@ export class NgxsStoragePluginModule { return { ngModule: NgxsStoragePluginModule, providers: [ - { - provide: NGXS_PLUGINS, - useClass: NgxsStoragePlugin, - multi: true - }, + withNgxsPlugin(NgxsStoragePlugin), { provide: ɵUSER_OPTIONS, useValue: options diff --git a/packages/store/plugins/src/index.ts b/packages/store/plugins/src/index.ts index 1596c2996..869731d05 100644 --- a/packages/store/plugins/src/index.ts +++ b/packages/store/plugins/src/index.ts @@ -1,3 +1,9 @@ export { InitState, UpdateState } from './actions'; -export { NGXS_PLUGINS, NgxsPlugin, NgxsPluginFn, NgxsNextPluginFn } from './symbols'; +export { + NGXS_PLUGINS, + NgxsPlugin, + NgxsPluginFn, + NgxsNextPluginFn, + ɵisPluginClass +} from './symbols'; export { getActionTypeFromInstance, actionMatcher, setValue, getValue } from './utils'; diff --git a/packages/store/plugins/src/symbols.ts b/packages/store/plugins/src/symbols.ts index 76c8e4b1d..3601ce7f8 100644 --- a/packages/store/plugins/src/symbols.ts +++ b/packages/store/plugins/src/symbols.ts @@ -1,16 +1,10 @@ -import { InjectionToken } from '@angular/core'; +import { InjectionToken, Type } from '@angular/core'; declare const ngDevMode: boolean; -// The injection token is used to resolve to custom NGXS plugins provided -// at the root level through either `{provide}` scheme or `withNgxsPlugin`. -export const NGXS_PLUGINS = new InjectionToken( - typeof ngDevMode !== 'undefined' && ngDevMode ? 'NGXS_PLUGINS' : '' -); - -export type NgxsPluginFn = (state: any, mutation: any, next: NgxsNextPluginFn) => any; +export type NgxsNextPluginFn = (state: any, action: any) => any; -export type NgxsNextPluginFn = (state: any, mutation: any) => any; +export type NgxsPluginFn = (state: any, action: any, next: NgxsNextPluginFn) => any; /** * Plugin interface @@ -21,3 +15,22 @@ export interface NgxsPlugin { */ handle(state: any, action: any, next: NgxsNextPluginFn): any; } + +/** + * A multi-provider token used to resolve to custom NGXS plugins provided + * at the root and feature levels through the `{provide}` scheme. + * + * @deprecated from v18.0.0, use `withNgxsPlugin` instead. + */ +export const NGXS_PLUGINS = /* @__PURE__ */ new InjectionToken( + typeof ngDevMode !== 'undefined' && ngDevMode ? 'NGXS_PLUGINS' : '' +); + +export function ɵisPluginClass( + plugin: Type | NgxsPluginFn +): plugin is Type { + // Determines whether the provided value is a class rather than a function. + // If it’s a class, its handle method should be defined on its prototype, + // as plugins can be either classes or functions. + return !!plugin.prototype.handle; +} diff --git a/packages/store/src/plugin-manager.ts b/packages/store/src/plugin-manager.ts index 20e40dd3b..f6e0356ef 100644 --- a/packages/store/src/plugin-manager.ts +++ b/packages/store/src/plugin-manager.ts @@ -1,7 +1,7 @@ import { inject, Injectable } from '@angular/core'; import { NGXS_PLUGINS, NgxsPlugin, NgxsPluginFn } from '@ngxs/store/plugins'; -@Injectable() +@Injectable({ providedIn: 'root' }) export class PluginManager { readonly plugins: NgxsPluginFn[] = []; diff --git a/packages/store/src/standalone-features/plugin.ts b/packages/store/src/standalone-features/plugin.ts index fe1672758..4aa4268ad 100644 --- a/packages/store/src/standalone-features/plugin.ts +++ b/packages/store/src/standalone-features/plugin.ts @@ -5,7 +5,7 @@ import { inject, makeEnvironmentProviders } from '@angular/core'; -import { NGXS_PLUGINS, NgxsPlugin } from '@ngxs/store/plugins'; +import { NGXS_PLUGINS, NgxsPlugin, NgxsPluginFn, ɵisPluginClass } from '@ngxs/store/plugins'; import { PluginManager } from '../plugin-manager'; @@ -23,12 +23,18 @@ import { PluginManager } from '../plugin-manager'; * }); * ``` */ -export function withNgxsPlugin(plugin: Type): EnvironmentProviders { +export function withNgxsPlugin(plugin: Type | NgxsPluginFn): EnvironmentProviders { return makeEnvironmentProviders([ - { provide: NGXS_PLUGINS, useClass: plugin, multi: true }, + ɵisPluginClass(plugin) + ? { provide: NGXS_PLUGINS, useClass: plugin, multi: true } + : { provide: NGXS_PLUGINS, useValue: plugin, multi: true }, // We should inject the `PluginManager` to retrieve `NGXS_PLUGINS` and // register those plugins. The plugin can be added from inside the child // route, so the plugin manager should be re-injected. - { provide: ENVIRONMENT_INITIALIZER, useValue: () => inject(PluginManager), multi: true } + { + provide: ENVIRONMENT_INITIALIZER, + useValue: () => inject(PluginManager), + multi: true + } ]); } diff --git a/packages/store/src/standalone-features/root-providers.ts b/packages/store/src/standalone-features/root-providers.ts index a734dd81e..73c50e530 100644 --- a/packages/store/src/standalone-features/root-providers.ts +++ b/packages/store/src/standalone-features/root-providers.ts @@ -1,7 +1,6 @@ import { APP_BOOTSTRAP_LISTENER, Provider, inject } from '@angular/core'; import { ɵStateClass, ɵNgxsAppBootstrappedState } from '@ngxs/store/internals'; -import { PluginManager } from '../plugin-manager'; import { StateFactory } from '../internal/state-factory'; import { CUSTOM_NGXS_EXECUTION_STRATEGY } from '../execution/symbols'; import { NgxsModuleOptions, ROOT_STATE_TOKEN, NGXS_OPTIONS } from '../symbols'; @@ -16,7 +15,6 @@ export function getRootProviders( ): Provider[] { return [ StateFactory, - PluginManager, ...states, { provide: ROOT_STATE_TOKEN, diff --git a/packages/store/tests/plugins.spec.ts b/packages/store/tests/plugins.spec.ts index b288da8f9..03520ccfb 100644 --- a/packages/store/tests/plugins.spec.ts +++ b/packages/store/tests/plugins.spec.ts @@ -1,6 +1,6 @@ import { assertInInjectionContext } from '@angular/core'; import { TestBed } from '@angular/core/testing'; -import { NgxsModule, NGXS_PLUGINS, Store, NgxsNextPluginFn, InitState } from '@ngxs/store'; +import { NgxsModule, withNgxsPlugin, Store, NgxsNextPluginFn, InitState } from '@ngxs/store'; import { debounceTime, firstValueFrom, tap } from 'rxjs'; describe('Plugins', () => { @@ -37,18 +37,7 @@ describe('Plugins', () => { TestBed.configureTestingModule({ imports: [NgxsModule.forRoot()], - providers: [ - { - provide: NGXS_PLUGINS, - useValue: asyncLogPlugin, - multi: true - }, - { - provide: NGXS_PLUGINS, - useValue: otherPlugin, - multi: true - } - ] + providers: [withNgxsPlugin(asyncLogPlugin), withNgxsPlugin(otherPlugin)] }); // Act