Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(store): allow plain functions in withNgxsPlugin #2255

Merged
merged 1 commit into from
Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading