Skip to content

Commit

Permalink
fix(store): allow plain functions in withNgxsPlugin (#2255)
Browse files Browse the repository at this point in the history
We allow passing a plain function into `withNgxsPlugin`, as the plugin can be a simple
function (not a class).
  • Loading branch information
arturovt authored Nov 18, 2024
1 parent 0577575 commit 6ec891b
Show file tree
Hide file tree
Showing 13 changed files with 61 additions and 79 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 3 additions & 11 deletions docs/concepts/store/meta-reducer.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))]
};
```

Expand Down
14 changes: 9 additions & 5 deletions docs/plugins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ 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');

@Injectable()
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 => {
Expand All @@ -41,16 +41,20 @@ 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);
});
}
```

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
Expand Down
7 changes: 1 addition & 6 deletions packages/devtools-plugin/src/devtools.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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
Expand Down
18 changes: 3 additions & 15 deletions packages/form-plugin/src/form.module.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -18,17 +12,11 @@ export class NgxsFormPluginModule {
static forRoot(): ModuleWithProviders<NgxsFormPluginModule> {
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);
}
7 changes: 1 addition & 6 deletions packages/logger-plugin/src/logger.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down
7 changes: 1 addition & 6 deletions packages/storage-plugin/src/storage.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down
8 changes: 7 additions & 1 deletion packages/store/plugins/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
31 changes: 22 additions & 9 deletions packages/store/plugins/src/symbols.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<NgxsPlugin[]>(
typeof ngDevMode !== 'undefined' && ngDevMode ? 'NGXS_PLUGINS' : ''
);

export function ɵisPluginClass(
plugin: Type<NgxsPlugin> | NgxsPluginFn
): plugin is Type<NgxsPlugin> {
// 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;
}
2 changes: 1 addition & 1 deletion packages/store/src/plugin-manager.ts
Original file line number Diff line number Diff line change
@@ -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[] = [];

Expand Down
14 changes: 10 additions & 4 deletions packages/store/src/standalone-features/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -23,12 +23,18 @@ import { PluginManager } from '../plugin-manager';
* });
* ```
*/
export function withNgxsPlugin(plugin: Type<NgxsPlugin>): EnvironmentProviders {
export function withNgxsPlugin(plugin: Type<NgxsPlugin> | 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
}
]);
}
2 changes: 0 additions & 2 deletions packages/store/src/standalone-features/root-providers.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -16,7 +15,6 @@ export function getRootProviders(
): Provider[] {
return [
StateFactory,
PluginManager,
...states,
{
provide: ROOT_STATE_TOKEN,
Expand Down
15 changes: 2 additions & 13 deletions packages/store/tests/plugins.spec.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 6ec891b

Please sign in to comment.