diff --git a/integration/app/app-routing.module.ts b/integration/app/app-routing.module.ts deleted file mode 100644 index d98b3e477..000000000 --- a/integration/app/app-routing.module.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { RouterModule } from '@angular/router'; -import { NgModule } from '@angular/core'; - -@NgModule({ - imports: [ - RouterModule.forRoot([ - { path: '', pathMatch: 'full', redirectTo: '/list' }, - { - path: 'list', - loadChildren: () => import('@integration/list/list.module').then(m => m.ListModule) - }, - { - path: 'detail', - loadChildren: () => - import('@integration/detail/detail.module').then(m => m.DetailModule) - }, - { - path: 'counter', - loadChildren: () => - import('@integration/counter/counter.module').then(m => m.CounterModule) - } - ]) - ], - exports: [RouterModule] -}) -export class AppRoutingModule {} diff --git a/integration/app/app.component.spec.ts b/integration/app/app.component.spec.ts index b62df8536..faff5f08d 100644 --- a/integration/app/app.component.spec.ts +++ b/integration/app/app.component.spec.ts @@ -1,13 +1,13 @@ import { TestBed, ComponentFixture, fakeAsync, tick } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; -import { ComponentFixtureAutoDetect, discardPeriodicTasks } from '@angular/core/testing'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { take } from 'rxjs/operators'; -import { Store } from '@ngxs/store'; +import { Store, provideStore } from '@ngxs/store'; +import { withNgxsFormPlugin } from '@ngxs/form-plugin'; -import { AppComponent } from '@integration/app.component'; -import { AppModule } from '@integration/app.module'; -import { Pizza, Todo } from '@integration/store/todos/todos.model'; +import { AppComponent } from './app.component'; +import { Pizza, Todo } from './store/todos/todos.model'; +import { TodosState } from './store/todos/todos.state'; +import { TodoState } from './store/todos/todo/todo.state'; describe('AppComponent', () => { let fixture: ComponentFixture; @@ -22,8 +22,8 @@ describe('AppComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [AppModule, RouterTestingModule, FormsModule, ReactiveFormsModule], - providers: [{ provide: ComponentFixtureAutoDetect, useValue: true }] + imports: [RouterTestingModule], + providers: [provideStore([TodosState, TodoState], withNgxsFormPlugin())] }); fixture = TestBed.createComponent(AppComponent); @@ -54,7 +54,10 @@ describe('AppComponent', () => { }); }); - it('should set toppings using form control', fakeAsync(() => { + it('should set toppings using form control', fakeAsync(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + component.pizzaForm.patchValue({ toppings: 'oli' }); tick(200); let flag = false; @@ -80,12 +83,15 @@ describe('AppComponent', () => { expect(flag).toBe(true); })); - it('should set toppings prefix', fakeAsync(() => { + it('should set toppings prefix', fakeAsync(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + component.pizzaForm.patchValue({ toppings: 'cheese' }); - tick(200); + tick(100); component.onPrefix(); let flag = false; - tick(200); + tick(100); component.pizza$.pipe(take(1)).subscribe((pizza: Pizza) => { flag = true; @@ -95,7 +101,6 @@ describe('AppComponent', () => { }); expect(flag).toBe(true); - discardPeriodicTasks(); })); it('should load data in pizza form', () => { diff --git a/integration/app/app.component.ts b/integration/app/app.component.ts index e15f23c23..a33fd0213 100644 --- a/integration/app/app.component.ts +++ b/integration/app/app.component.ts @@ -1,6 +1,15 @@ -import { FormArray, FormBuilder, FormControl } from '@angular/forms'; -import { Component, OnInit } from '@angular/core'; +import { + FormArray, + FormBuilder, + FormControl, + FormsModule, + ReactiveFormsModule +} from '@angular/forms'; +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule } from '@angular/router'; import { Store } from '@ngxs/store'; +import { NgxsFormDirective } from '@ngxs/form-plugin'; import { Observable } from 'rxjs'; import { TodoState } from '@integration/store/todos/todo/todo.state'; @@ -11,7 +20,10 @@ import { Extras, Pizza, Todo } from '@integration/store/todos/todos.model'; @Component({ selector: 'app-root', - templateUrl: './app.component.html' + templateUrl: './app.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [CommonModule, RouterModule, FormsModule, ReactiveFormsModule, NgxsFormDirective] }) export class AppComponent implements OnInit { allExtras: Extras[] = [ diff --git a/integration/app/app.module.ts b/integration/app/app.module.ts deleted file mode 100644 index 4c456f531..000000000 --- a/integration/app/app.module.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { BrowserModule } from '@angular/platform-browser'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { NgModule } from '@angular/core'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; - -import { AppRoutingModule } from '@integration/app-routing.module'; -import { AppComponent } from '@integration/app.component'; -import { NgxsStoreModule } from '@integration/store/store.module'; - -@NgModule({ - imports: [ - BrowserModule.withServerTransition({ appId: 'my-app' }), - BrowserAnimationsModule, - FormsModule, - ReactiveFormsModule, - - NgxsStoreModule, - AppRoutingModule - ], - declarations: [AppComponent], - bootstrap: [AppComponent] -}) -export class AppModule {} diff --git a/integration/main.server.ts b/integration/main.server.ts index 6e6383344..2ac68a282 100644 --- a/integration/main.server.ts +++ b/integration/main.server.ts @@ -6,5 +6,5 @@ if (environment.production) { enableProdMode(); } -export { AppServerModule } from './app/app.server.module'; -export { renderModuleFactory } from '@angular/platform-server'; +export { AppComponent } from './app/app.component'; +export { APP_ID_VALUE, SHARED_PLATFORM_PROVIDERS } from './shared-platform-providers'; diff --git a/integration/main.ts b/integration/main.ts index a9ca1caf8..644fa8589 100644 --- a/integration/main.ts +++ b/integration/main.ts @@ -1,11 +1,15 @@ import { enableProdMode } from '@angular/core'; -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { bootstrapApplication } from '@angular/platform-browser'; +import { provideAnimations } from '@angular/platform-browser/animations'; -import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; +import { AppComponent } from './app/app.component'; +import { SHARED_PLATFORM_PROVIDERS } from './shared-platform-providers'; if (environment.production) { enableProdMode(); } -platformBrowserDynamic().bootstrapModule(AppModule); +bootstrapApplication(AppComponent, { + providers: [provideAnimations(), ...SHARED_PLATFORM_PROVIDERS] +}); diff --git a/integration/server.ts b/integration/server.ts index 8ebc55510..d5432fa80 100644 --- a/integration/server.ts +++ b/integration/server.ts @@ -1,28 +1,46 @@ import 'zone.js/node'; +import { readFile } from 'fs/promises'; +import { join } from 'path'; +import * as express from 'express'; +import { Provider } from '@angular/core'; import { APP_BASE_HREF } from '@angular/common'; -import { ngExpressEngine } from '@nguniversal/express-engine'; +import { renderApplication } from '@angular/platform-server'; import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens'; -import * as express from 'express'; -import { existsSync } from 'fs'; -import { join } from 'path'; -import { AppServerModule } from './main.server'; +import { AppComponent, APP_ID_VALUE, SHARED_PLATFORM_PROVIDERS } from './main.server'; + +interface RenderOptions { + req: express.Request; + providers: Provider[]; +} // The Express app is exported so that it can be used by serverless Functions. export function app() { const server = express(); const distFolder = join(process.cwd(), 'dist-integration'); - const indexHtml = existsSync(join(distFolder, 'index.original.html')) - ? 'index.original.html' - : 'index'; + let indexHtmlContent: string | null = null; - // Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine) server.engine( 'html', - ngExpressEngine({ - bootstrap: AppServerModule - }) as any + async ( + path: string, + options: object, + callback: (error: Error | null, content: string) => void + ) => { + const { req, providers } = options as RenderOptions; + + indexHtmlContent ||= await readFile(path, { encoding: 'utf-8' }); + + const html = await renderApplication(AppComponent, { + providers, + appId: APP_ID_VALUE, + document: indexHtmlContent, + url: `${req.baseUrl}${req.url}` + }); + + callback(null, html); + } ); server.set('view engine', 'html'); @@ -40,9 +58,10 @@ export function app() { // All regular routes use the Universal engine server.get('*', (req, res) => { - res.render(indexHtml, { + res.render('index', { req, providers: [ + ...SHARED_PLATFORM_PROVIDERS, { provide: APP_BASE_HREF, useValue: req.baseUrl }, { provide: REQUEST, useValue: req }, { provide: RESPONSE, useValue: res } diff --git a/integration/shared-platform-providers.ts b/integration/shared-platform-providers.ts new file mode 100644 index 000000000..17b2b6cd5 --- /dev/null +++ b/integration/shared-platform-providers.ts @@ -0,0 +1,50 @@ +import { APP_ID } from '@angular/core'; +import { provideRouter } from '@angular/router'; +import { provideStore, withNgxsOptions } from '@ngxs/store'; +import { withNgxsFormPlugin } from '@ngxs/form-plugin'; +import { withNgxsLoggerPlugin } from '@ngxs/logger-plugin'; +import { withNgxsReduxDevtoolsPlugin } from '@ngxs/devtools-plugin'; +import { withNgxsRouterPlugin } from '@ngxs/router-plugin'; +import { withNgxsStoragePlugin } from '@ngxs/storage-plugin'; + +import { TodosState } from '@integration/store/todos/todos.state'; +import { TodoState } from '@integration/store/todos/todo/todo.state'; +import { TODOS_STORAGE_KEY } from '@integration/store/todos/todos.model'; + +import { environment } from './environments/environment'; + +export const APP_ID_VALUE = 'integration-app'; + +export const SHARED_PLATFORM_PROVIDERS = [ + { provide: APP_ID, useValue: APP_ID_VALUE }, + + provideRouter([ + { path: '', pathMatch: 'full', redirectTo: '/list' }, + { + path: 'list', + loadChildren: () => import('@integration/list/list.module').then(m => m.ListModule) + }, + { + path: 'detail', + loadChildren: () => import('@integration/detail/detail.module').then(m => m.DetailModule) + }, + { + path: 'counter', + loadChildren: () => + import('@integration/counter/counter.module').then(m => m.CounterModule) + } + ]), + + provideStore( + [TodosState, TodoState], + withNgxsOptions({ + developmentMode: !environment.production, + selectorOptions: {} + }), + withNgxsFormPlugin(), + withNgxsLoggerPlugin({ logger: console, collapsed: false, disabled: true }), + withNgxsReduxDevtoolsPlugin({ disabled: environment.production }), + withNgxsRouterPlugin(), + withNgxsStoragePlugin({ key: [TODOS_STORAGE_KEY] }) + ) +]; diff --git a/integration/tsconfig.editor.json b/integration/tsconfig.editor.json index 20c4afdbf..87778f901 100644 --- a/integration/tsconfig.editor.json +++ b/integration/tsconfig.editor.json @@ -2,6 +2,7 @@ "extends": "./tsconfig.json", "include": ["**/*.ts"], "compilerOptions": { + "module": "esnext", "types": ["jest", "node"] } } diff --git a/integration/tsconfig.json b/integration/tsconfig.json index 9437d409f..be4c6f620 100644 --- a/integration/tsconfig.json +++ b/integration/tsconfig.json @@ -20,7 +20,8 @@ "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": false }, "angularCompilerOptions": { "strictInjectionParameters": true, diff --git a/packages/devtools-plugin/src/devtools.module.ts b/packages/devtools-plugin/src/devtools.module.ts index 98342c46d..1209c65d8 100644 --- a/packages/devtools-plugin/src/devtools.module.ts +++ b/packages/devtools-plugin/src/devtools.module.ts @@ -1,5 +1,5 @@ import { NgModule, ModuleWithProviders, InjectionToken } from '@angular/core'; -import { NGXS_PLUGINS } from '@ngxs/store'; +import { NGXS_PLUGINS, NgxsStoreFeature, withNgxsPlugin } from '@ngxs/store'; import { NgxsDevtoolsOptions, NGXS_DEVTOOLS_OPTIONS } from './symbols'; import { NgxsReduxDevtoolsPlugin } from './devtools.plugin'; @@ -39,3 +39,18 @@ export class NgxsReduxDevtoolsPluginModule { }; } } + +export function withNgxsReduxDevtoolsPlugin(options?: NgxsDevtoolsOptions): NgxsStoreFeature { + return [ + withNgxsPlugin(NgxsReduxDevtoolsPlugin), + { + provide: USER_OPTIONS, + useValue: options + }, + { + provide: NGXS_DEVTOOLS_OPTIONS, + useFactory: devtoolsOptionsFactory, + deps: [USER_OPTIONS] + } + ]; +} diff --git a/packages/devtools-plugin/src/public_api.ts b/packages/devtools-plugin/src/public_api.ts index c813e6e30..9a6a376d8 100644 --- a/packages/devtools-plugin/src/public_api.ts +++ b/packages/devtools-plugin/src/public_api.ts @@ -1,3 +1,3 @@ -export { NgxsReduxDevtoolsPluginModule } from './devtools.module'; +export { NgxsReduxDevtoolsPluginModule, withNgxsReduxDevtoolsPlugin } from './devtools.module'; export { NgxsReduxDevtoolsPlugin } from './devtools.plugin'; export * from './symbols'; diff --git a/packages/form-plugin/src/directive.ts b/packages/form-plugin/src/directive.ts index 2297f3af2..6d8f31739 100644 --- a/packages/form-plugin/src/directive.ts +++ b/packages/form-plugin/src/directive.ts @@ -12,8 +12,8 @@ import { UpdateFormValue } from './actions'; -@Directive({ selector: '[ngxsForm]' }) -export class FormDirective implements OnInit, OnDestroy { +@Directive({ selector: '[ngxsForm]', standalone: true }) +export class NgxsFormDirective implements OnInit, OnDestroy { @Input('ngxsForm') path: string = null!; diff --git a/packages/form-plugin/src/form.module.ts b/packages/form-plugin/src/form.module.ts index 8804ef84c..742665939 100644 --- a/packages/form-plugin/src/form.module.ts +++ b/packages/form-plugin/src/form.module.ts @@ -1,13 +1,12 @@ import { NgModule, ModuleWithProviders } from '@angular/core'; -import { NGXS_PLUGINS } from '@ngxs/store'; +import { NGXS_PLUGINS, NgxsStoreFeature, withNgxsPlugin } from '@ngxs/store'; + import { NgxsFormPlugin } from './form.plugin'; -import { ReactiveFormsModule } from '@angular/forms'; -import { FormDirective } from './directive'; +import { NgxsFormDirective } from './directive'; @NgModule({ - imports: [ReactiveFormsModule], - declarations: [FormDirective], - exports: [FormDirective] + imports: [NgxsFormDirective], + exports: [NgxsFormDirective] }) export class NgxsFormPluginModule { static forRoot(): ModuleWithProviders { @@ -23,3 +22,7 @@ export class NgxsFormPluginModule { }; } } + +export function withNgxsFormPlugin(): NgxsStoreFeature { + return [withNgxsPlugin(NgxsFormPlugin)]; +} diff --git a/packages/form-plugin/src/public_api.ts b/packages/form-plugin/src/public_api.ts index 492a81803..1a257991f 100644 --- a/packages/form-plugin/src/public_api.ts +++ b/packages/form-plugin/src/public_api.ts @@ -1,4 +1,4 @@ -export { NgxsFormPluginModule } from './form.module'; +export { NgxsFormPluginModule, withNgxsFormPlugin } from './form.module'; export { NgxsFormPlugin } from './form.plugin'; -export { FormDirective as ɵFormDirective } from './directive'; +export { NgxsFormDirective } from './directive'; export * from './actions'; diff --git a/packages/logger-plugin/src/logger.module.ts b/packages/logger-plugin/src/logger.module.ts index c3009e5d0..4556816e8 100644 --- a/packages/logger-plugin/src/logger.module.ts +++ b/packages/logger-plugin/src/logger.module.ts @@ -1,5 +1,5 @@ import { InjectionToken, ModuleWithProviders, NgModule } from '@angular/core'; -import { NGXS_PLUGINS } from '@ngxs/store'; +import { NGXS_PLUGINS, withNgxsPlugin, NgxsStoreFeature } from '@ngxs/store'; import { NgxsLoggerPlugin } from './logger.plugin'; import { NgxsLoggerPluginOptions, NGXS_LOGGER_PLUGIN_OPTIONS } from './symbols'; @@ -45,3 +45,15 @@ export class NgxsLoggerPluginModule { }; } } + +export function withNgxsLoggerPlugin(options?: NgxsLoggerPluginOptions): NgxsStoreFeature { + return [ + withNgxsPlugin(NgxsLoggerPlugin), + { provide: USER_OPTIONS, useValue: options }, + { + provide: NGXS_LOGGER_PLUGIN_OPTIONS, + useFactory: loggerOptionsFactory, + deps: [USER_OPTIONS] + } + ]; +} diff --git a/packages/logger-plugin/src/public_api.ts b/packages/logger-plugin/src/public_api.ts index 3f22c4357..33a536e83 100644 --- a/packages/logger-plugin/src/public_api.ts +++ b/packages/logger-plugin/src/public_api.ts @@ -1,3 +1,3 @@ -export { NgxsLoggerPluginModule } from './logger.module'; +export { NgxsLoggerPluginModule, withNgxsLoggerPlugin } from './logger.module'; export { NgxsLoggerPlugin } from './logger.plugin'; export * from './symbols'; diff --git a/packages/logger-plugin/tests/helpers/setup-with-logger.ts b/packages/logger-plugin/tests/helpers/setup-with-logger.ts index b650dfd9c..ae15338db 100644 --- a/packages/logger-plugin/tests/helpers/setup-with-logger.ts +++ b/packages/logger-plugin/tests/helpers/setup-with-logger.ts @@ -1,24 +1,20 @@ import { ErrorHandler } from '@angular/core'; import { TestBed } from '@angular/core/testing'; -import { NgxsModule, Store } from '@ngxs/store'; +import { Store, provideStore } from '@ngxs/store'; import { StateClass } from '@ngxs/store/internals'; +import { NgxsLoggerPluginOptions, withNgxsLoggerPlugin } from '@ngxs/logger-plugin'; import { LoggerSpy } from './logger-spy'; import { NoopErrorHandler } from '../../../store/tests/helpers/utils'; -import { NgxsLoggerPluginModule, NgxsLoggerPluginOptions } from '../../'; export function setupWithLogger(states: StateClass[], opts?: NgxsLoggerPluginOptions) { const logger = new LoggerSpy(); TestBed.configureTestingModule({ - imports: [ - NgxsModule.forRoot(states), - NgxsLoggerPluginModule.forRoot({ - ...opts, - logger - }) - ], - providers: [{ provide: ErrorHandler, useClass: NoopErrorHandler }] + providers: [ + provideStore(states, withNgxsLoggerPlugin({ ...opts, logger })), + { provide: ErrorHandler, useClass: NoopErrorHandler } + ] }); const store: Store = TestBed.inject(Store); diff --git a/packages/router-plugin/src/public_api.ts b/packages/router-plugin/src/public_api.ts index 1317fb36e..0abc27134 100644 --- a/packages/router-plugin/src/public_api.ts +++ b/packages/router-plugin/src/public_api.ts @@ -1,4 +1,4 @@ -export { NgxsRouterPluginModule } from './router.module'; +export { NgxsRouterPluginModule, withNgxsRouterPlugin } from './router.module'; export { RouterState, RouterStateModel } from './router.state'; export { RouterStateSerializer, diff --git a/packages/router-plugin/src/router.module.ts b/packages/router-plugin/src/router.module.ts index 53f239307..11da366be 100644 --- a/packages/router-plugin/src/router.module.ts +++ b/packages/router-plugin/src/router.module.ts @@ -1,5 +1,5 @@ import { ModuleWithProviders, NgModule } from '@angular/core'; -import { NgxsModule } from '@ngxs/store'; +import { NgxsModule, NgxsStoreFeature, provideFeatureStore } from '@ngxs/store'; import { RouterState } from './router.state'; import { DefaultRouterStateSerializer, RouterStateSerializer } from './serializer'; @@ -31,3 +31,16 @@ export class NgxsRouterPluginModule { }; } } + +export function withNgxsRouterPlugin(options?: NgxsRouterPluginOptions): NgxsStoreFeature { + return [ + provideFeatureStore([RouterState]), + { provide: USER_OPTIONS, useValue: options }, + { + provide: NGXS_ROUTER_PLUGIN_OPTIONS, + useFactory: createRouterPluginOptions, + deps: [USER_OPTIONS] + }, + { provide: RouterStateSerializer, useClass: DefaultRouterStateSerializer } + ]; +} diff --git a/packages/storage-plugin/src/public_api.ts b/packages/storage-plugin/src/public_api.ts index 8c5847676..9ac6621fd 100644 --- a/packages/storage-plugin/src/public_api.ts +++ b/packages/storage-plugin/src/public_api.ts @@ -1,4 +1,4 @@ -export { NgxsStoragePluginModule } from './storage.module'; +export { NgxsStoragePluginModule, withNgxsStoragePlugin } from './storage.module'; export { NgxsStoragePlugin } from './storage.plugin'; export * from './symbols'; export * from './engines'; diff --git a/packages/storage-plugin/src/storage.module.ts b/packages/storage-plugin/src/storage.module.ts index ed5321850..0de133c66 100644 --- a/packages/storage-plugin/src/storage.module.ts +++ b/packages/storage-plugin/src/storage.module.ts @@ -5,7 +5,7 @@ import { InjectionToken, Injector } from '@angular/core'; -import { NGXS_PLUGINS } from '@ngxs/store'; +import { NGXS_PLUGINS, NgxsStoreFeature, withNgxsPlugin } from '@ngxs/store'; import { NgxsStoragePluginOptions, @@ -57,3 +57,28 @@ export class NgxsStoragePluginModule { }; } } + +export function withNgxsStoragePlugin(options?: NgxsStoragePluginOptions): NgxsStoreFeature { + return [ + withNgxsPlugin(NgxsStoragePlugin), + { + provide: USER_OPTIONS, + useValue: options + }, + { + provide: NGXS_STORAGE_PLUGIN_OPTIONS, + useFactory: storageOptionsFactory, + deps: [USER_OPTIONS] + }, + { + provide: STORAGE_ENGINE, + useFactory: engineFactory, + deps: [NGXS_STORAGE_PLUGIN_OPTIONS, PLATFORM_ID] + }, + { + provide: FINAL_NGXS_STORAGE_PLUGIN_OPTIONS, + useFactory: createFinalStoragePluginOptions, + deps: [Injector, NGXS_STORAGE_PLUGIN_OPTIONS] + } + ]; +} diff --git a/packages/store/src/execution/symbols.ts b/packages/store/src/execution/symbols.ts index 0d85c592d..068c25456 100644 --- a/packages/store/src/execution/symbols.ts +++ b/packages/store/src/execution/symbols.ts @@ -6,9 +6,9 @@ import { DispatchOutsideZoneNgxsExecutionStrategy } from './dispatch-outside-zon /** * The strategy that might be provided by users through `options.executionStrategy`. */ -export const USER_PROVIDED_NGXS_EXECUTION_STRATEGY = new InjectionToken< +export const CUSTOM_NGXS_EXECUTION_STRATEGY = new InjectionToken< Type | undefined ->('USER_PROVIDED_NGXS_EXECUTION_STRATEGY'); +>('CUSTOM_NGXS_EXECUTION_STRATEGY'); /* * Internal execution strategy injection token @@ -19,7 +19,7 @@ export const NGXS_EXECUTION_STRATEGY = new InjectionToken providedIn: 'root', factory: () => { const injector = inject(INJECTOR); - const executionStrategy = injector.get(USER_PROVIDED_NGXS_EXECUTION_STRATEGY); + const executionStrategy = injector.get(CUSTOM_NGXS_EXECUTION_STRATEGY); return executionStrategy ? injector.get(executionStrategy) : injector.get( diff --git a/packages/store/src/module.ts b/packages/store/src/module.ts index ba29f75b7..09ca5d33f 100644 --- a/packages/store/src/module.ts +++ b/packages/store/src/module.ts @@ -1,149 +1,28 @@ -import { - APP_BOOTSTRAP_LISTENER, - InjectionToken, - ModuleWithProviders, - NgModule, - Provider -} from '@angular/core'; -import { - INITIAL_STATE_TOKEN, - InitialState, - ɵNGXS_STATE_FACTORY, - ɵNGXS_STATE_CONTEXT_FACTORY, - NgxsBootstrapper, - StateClass -} from '@ngxs/store/internals'; +import { ModuleWithProviders, NgModule } from '@angular/core'; +import { StateClass } from '@ngxs/store/internals'; -import { - FEATURE_STATE_TOKEN, - NgxsConfig, - NgxsModuleOptions, - ROOT_STATE_TOKEN -} from './symbols'; -import { USER_PROVIDED_NGXS_EXECUTION_STRATEGY } from './execution/symbols'; -import { StateFactory } from './internal/state-factory'; -import { StateContextFactory } from './internal/state-context-factory'; -import { Actions, InternalActions } from './actions-stream'; -import { LifecycleStateManager } from './internal/lifecycle-state-manager'; -import { InternalDispatchedActionResults, InternalDispatcher } from './internal/dispatcher'; -import { InternalStateOperations } from './internal/state-operations'; -import { Store } from './store'; -import { SelectFactory } from './decorators/select/select-factory'; -import { StateStream } from './internal/state-stream'; -import { PluginManager } from './plugin-manager'; +import { NgxsModuleOptions } from './symbols'; import { NgxsRootModule } from './modules/ngxs-root.module'; import { NgxsFeatureModule } from './modules/ngxs-feature.module'; -import { InternalNgxsExecutionStrategy } from './execution/internal-ngxs-execution-strategy'; -import { mergeDeep } from './utils/utils'; +import { getNgxsRootProviders } from './standalone-features/root-providers'; +import { getNgxsFeatureProviders } from './standalone-features/feature-providers'; -/** - * Ngxs Module - */ @NgModule() export class NgxsModule { - private static readonly ROOT_OPTIONS = new InjectionToken('ROOT_OPTIONS'); - - /** - * Root module factory - */ - public static forRoot( + static forRoot( states: StateClass[] = [], options: NgxsModuleOptions = {} ): ModuleWithProviders { return { ngModule: NgxsRootModule, - providers: [ - StateFactory, - StateContextFactory, - Actions, - InternalActions, - NgxsBootstrapper, - LifecycleStateManager, - InternalDispatcher, - InternalDispatchedActionResults, - InternalStateOperations, - InternalNgxsExecutionStrategy, - Store, - StateStream, - SelectFactory, - PluginManager, - ...states, - ...NgxsModule.ngxsTokenProviders(states, options) - ] + providers: getNgxsRootProviders(states, options) }; } - /** - * Feature module factory - */ - public static forFeature(states: StateClass[] = []): ModuleWithProviders { + static forFeature(states: StateClass[] = []): ModuleWithProviders { return { ngModule: NgxsFeatureModule, - providers: [ - StateFactory, - PluginManager, - ...states, - { - provide: FEATURE_STATE_TOKEN, - multi: true, - useValue: states - } - ] + providers: getNgxsFeatureProviders(states) }; } - - private static ngxsTokenProviders( - states: StateClass[], - options: NgxsModuleOptions - ): Provider[] { - return [ - { - provide: USER_PROVIDED_NGXS_EXECUTION_STRATEGY, - useValue: options.executionStrategy - }, - { - provide: ROOT_STATE_TOKEN, - useValue: states - }, - { - provide: NgxsModule.ROOT_OPTIONS, - useValue: options - }, - { - provide: NgxsConfig, - useFactory: NgxsModule.ngxsConfigFactory, - deps: [NgxsModule.ROOT_OPTIONS] - }, - { - provide: APP_BOOTSTRAP_LISTENER, - useFactory: NgxsModule.appBootstrapListenerFactory, - multi: true, - deps: [NgxsBootstrapper] - }, - { - provide: INITIAL_STATE_TOKEN, - useFactory: NgxsModule.getInitialState - }, - { - provide: ɵNGXS_STATE_CONTEXT_FACTORY, - useExisting: StateContextFactory - }, - { - provide: ɵNGXS_STATE_FACTORY, - useExisting: StateFactory - } - ]; - } - - private static ngxsConfigFactory(options: NgxsModuleOptions): NgxsConfig { - return mergeDeep(new NgxsConfig(), options); - } - - private static appBootstrapListenerFactory(bootstrapper: NgxsBootstrapper): Function { - return () => bootstrapper.bootstrap(); - } - - private static getInitialState() { - return InitialState.pop(); - } } diff --git a/packages/store/src/modules/ngxs-feature.module.ts b/packages/store/src/modules/ngxs-feature.module.ts index 48ea3e68c..affc8bbbb 100644 --- a/packages/store/src/modules/ngxs-feature.module.ts +++ b/packages/store/src/modules/ngxs-feature.module.ts @@ -1,47 +1,13 @@ -import { Inject, NgModule, Optional } from '@angular/core'; +import { NgModule } from '@angular/core'; -import { Store } from '../store'; -import { InternalStateOperations } from '../internal/state-operations'; -import { StateFactory } from '../internal/state-factory'; -import { FEATURE_STATE_TOKEN } from '../symbols'; -import { LifecycleStateManager } from '../internal/lifecycle-state-manager'; -import { StateClassInternal, StatesAndDefaults } from '../internal/internals'; -import { UpdateState } from '../actions/actions'; +import { featureStoreInitializer } from '../standalone-features/initializers'; /** - * Feature module * @ignore */ @NgModule() export class NgxsFeatureModule { - constructor( - _store: Store, - internalStateOperations: InternalStateOperations, - factory: StateFactory, - @Optional() - @Inject(FEATURE_STATE_TOKEN) - states: StateClassInternal[][] = [], - lifecycleStateManager: LifecycleStateManager - ) { - // Since FEATURE_STATE_TOKEN is a multi token, we need to - // flatten it [[Feature1State, Feature2State], [Feature3State]] - const flattenedStates: StateClassInternal[] = NgxsFeatureModule.flattenStates(states); - - // add stores to the state graph and return their defaults - const results: StatesAndDefaults = factory.addAndReturnDefaults(flattenedStates); - - if (results.states.length) { - internalStateOperations.setStateToTheCurrentWithNew(results); - - // dispatch the update action and invoke init and bootstrap functions after - lifecycleStateManager.ngxsBootstrap(new UpdateState(results.defaults), results); - } - } - - private static flattenStates(states: StateClassInternal[][] = []): StateClassInternal[] { - return states.reduce( - (total: StateClassInternal[], values: StateClassInternal[]) => total.concat(values), - [] - ); + constructor() { + featureStoreInitializer(); } } diff --git a/packages/store/src/modules/ngxs-root.module.ts b/packages/store/src/modules/ngxs-root.module.ts index cbe02c261..5542bbf4a 100644 --- a/packages/store/src/modules/ngxs-root.module.ts +++ b/packages/store/src/modules/ngxs-root.module.ts @@ -1,39 +1,13 @@ -import { Inject, NgModule, Optional } from '@angular/core'; +import { NgModule } from '@angular/core'; -import { StateFactory } from '../internal/state-factory'; -import { InternalStateOperations } from '../internal/state-operations'; -import { Store } from '../store'; -import { SelectFactory } from '../decorators/select/select-factory'; -import { ROOT_STATE_TOKEN } from '../symbols'; -import { StateClassInternal, StatesAndDefaults } from '../internal/internals'; -import { LifecycleStateManager } from '../internal/lifecycle-state-manager'; -import { InitState } from '../actions/actions'; +import { rootStoreInitializer } from '../standalone-features/initializers'; /** - * Root module * @ignore */ @NgModule() export class NgxsRootModule { - constructor( - factory: StateFactory, - internalStateOperations: InternalStateOperations, - _store: Store, - _select: SelectFactory, - @Optional() - @Inject(ROOT_STATE_TOKEN) - states: StateClassInternal[] = [], - lifecycleStateManager: LifecycleStateManager - ) { - // Add stores to the state graph and return their defaults - const results: StatesAndDefaults = factory.addAndReturnDefaults(states); - - internalStateOperations.setStateToTheCurrentWithNew(results); - - // Connect our actions stream - factory.connectActionHandlers(); - - // Dispatch the init action and invoke init and bootstrap functions after - lifecycleStateManager.ngxsBootstrap(new InitState(), results); + constructor() { + rootStoreInitializer(); } } diff --git a/packages/store/src/public_api.ts b/packages/store/src/public_api.ts index 1173eaed6..27bdf166e 100644 --- a/packages/store/src/public_api.ts +++ b/packages/store/src/public_api.ts @@ -19,7 +19,7 @@ export { getSelectorMetadata, getStoreMetadata, ensureStoreMetadata, - ensureSelectorMetadata, + ensureSelectorMetadata } from './public_to_deprecate'; export { ofAction, @@ -28,7 +28,7 @@ export { ofActionCanceled, ofActionErrored, ofActionCompleted, - ActionCompletion, + ActionCompletion } from './operators/of-action'; export { StateContext, @@ -37,7 +37,7 @@ export { NgxsAfterBootstrap, NgxsOnChanges, NgxsModuleOptions, - NgxsSimpleChange, + NgxsSimpleChange } from './symbols'; export { Selector } from './decorators/selector/selector'; export { getActionTypeFromInstance, actionMatcher } from './utils/utils'; @@ -56,5 +56,7 @@ export { createPropertySelectors, createSelector, PropertySelectors, - TypedSelector, + TypedSelector } from './selectors'; + +export * from './standalone-features'; diff --git a/packages/store/src/standalone-features/feature-providers.ts b/packages/store/src/standalone-features/feature-providers.ts new file mode 100644 index 000000000..e0d213b84 --- /dev/null +++ b/packages/store/src/standalone-features/feature-providers.ts @@ -0,0 +1,23 @@ +import { Provider } from '@angular/core'; +import { StateClass } from '@ngxs/store/internals'; + +import { FEATURE_STATE_TOKEN } from '../symbols'; +import { PluginManager } from '../plugin-manager'; +import { StateFactory } from '../internal/state-factory'; + +/** + * This function returns providers required when calling `NgxsModule.forFeature` + * or `provideFeatureStore`. This is shared between NgModule and standalone APIs. + */ +export function getNgxsFeatureProviders(states: StateClass[]): Provider[] { + return [ + StateFactory, + PluginManager, + ...states, + { + provide: FEATURE_STATE_TOKEN, + multi: true, + useValue: states + } + ]; +} diff --git a/packages/store/src/standalone-features/index.ts b/packages/store/src/standalone-features/index.ts new file mode 100644 index 000000000..57ead5d14 --- /dev/null +++ b/packages/store/src/standalone-features/index.ts @@ -0,0 +1,3 @@ +export { provideStore } from './provide-store'; +export { provideFeatureStore } from './provide-feature-store'; +export { withNgxsPlugin, withNgxsOptions, NgxsStoreFeature } from './providers-features'; diff --git a/packages/store/src/standalone-features/initializers.ts b/packages/store/src/standalone-features/initializers.ts new file mode 100644 index 000000000..c95c015d6 --- /dev/null +++ b/packages/store/src/standalone-features/initializers.ts @@ -0,0 +1,104 @@ +import { ENVIRONMENT_INITIALIZER, InjectionToken, Provider, inject } from '@angular/core'; + +import { Store } from '../store'; +import { InitState, UpdateState } from '../plugin_api'; +import { FEATURE_STATE_TOKEN, ROOT_STATE_TOKEN } from '../symbols'; +import { StateFactory } from '../internal/state-factory'; +import { StateClassInternal, StatesAndDefaults } from '../internal/internals'; +import { SelectFactory } from '../decorators/select/select-factory'; +import { InternalStateOperations } from '../internal/state-operations'; +import { LifecycleStateManager } from '../internal/lifecycle-state-manager'; + +/** + * This function is shared between NgModule and standalone features. + * We can rely on the same initialization functionality when using + * `NgxsModule.forRoot` and `provideStore`. + */ +export function rootStoreInitializer() { + const factory = inject(StateFactory); + const internalStateOperations = inject(InternalStateOperations); + + inject(Store); + inject(SelectFactory); + + const states = inject(ROOT_STATE_TOKEN, { optional: true }) || []; + const lifecycleStateManager = inject(LifecycleStateManager); + + // Add stores to the state graph and return their defaults + const results: StatesAndDefaults = factory.addAndReturnDefaults(states); + + internalStateOperations.setStateToTheCurrentWithNew(results); + + // Connect our actions stream + factory.connectActionHandlers(); + + // Dispatch the init action and invoke init and bootstrap functions after + lifecycleStateManager.ngxsBootstrap(new InitState(), results); +} + +/** + * This function is shared between NgModule and standalone features. + * We can rely on the same initialization functionality when using + * `NgxsModule.forFeature` and `provideFeatureStore`. + */ +export function featureStoreInitializer() { + inject(Store); + + const internalStateOperations = inject(InternalStateOperations); + const factory = inject(StateFactory); + const states: StateClassInternal[][] = inject(FEATURE_STATE_TOKEN, { optional: true }) || []; + const lifecycleStateManager = inject(LifecycleStateManager); + + // Since FEATURE_STATE_TOKEN is a multi token, we need to + // flatten it [[Feature1State, Feature2State], [Feature3State]] + const flattenedStates: StateClassInternal[] = states.reduce( + (total: StateClassInternal[], values: StateClassInternal[]) => total.concat(values), + [] + ); + + // add stores to the state graph and return their defaults + const results: StatesAndDefaults = factory.addAndReturnDefaults(flattenedStates); + + if (results.states.length) { + internalStateOperations.setStateToTheCurrentWithNew(results); + + // dispatch the update action and invoke init and bootstrap functions after + lifecycleStateManager.ngxsBootstrap(new UpdateState(results.defaults), results); + } +} + +/** + * InjectionToken that registers the global Store. + */ +export const NGXS_ROOT_STORE_INITIALIZER = new InjectionToken( + 'NGXS_ROOT_STORE_INITIALIZER' +); + +/** + * InjectionToken that registers feature states. + */ +export const NGXS_FEATURE_STORE_INITIALIZER = new InjectionToken( + 'NGXS_FEATURE_STORE_INITIALIZER' +); + +export const NGXS_ROOT_ENVIRONMENT_INITIALIZER: Provider[] = [ + { provide: NGXS_ROOT_STORE_INITIALIZER, useFactory: rootStoreInitializer }, + { + provide: ENVIRONMENT_INITIALIZER, + multi: true, + useFactory() { + return () => inject(NGXS_ROOT_STORE_INITIALIZER); + } + } +]; + +export const NGXS_FEATURE_ENVIRONMENT_INITIALIZER: Provider[] = [ + { provide: NGXS_FEATURE_STORE_INITIALIZER, useFactory: featureStoreInitializer }, + { + provide: ENVIRONMENT_INITIALIZER, + multi: true, + useFactory() { + return () => inject(NGXS_FEATURE_STORE_INITIALIZER); + } + } +]; diff --git a/packages/store/src/standalone-features/provide-feature-store.ts b/packages/store/src/standalone-features/provide-feature-store.ts new file mode 100644 index 000000000..742553a79 --- /dev/null +++ b/packages/store/src/standalone-features/provide-feature-store.ts @@ -0,0 +1,31 @@ +import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core'; +import { StateClass } from '@ngxs/store/internals'; + +import { NgxsStoreFeature } from './providers-features'; +import { getNgxsFeatureProviders } from './feature-providers'; +import { NGXS_FEATURE_ENVIRONMENT_INITIALIZER } from './initializers'; + +/** + * This is a standalone version of `NgxsModule.forFeature`. This might be used in the + * same way to register feature states, but on the `Route` providers level: + * + * ```ts + * const routes: Routes = [ + * { + * path: 'products', + * loadComponent: async () => {...}, + * providers: [provideFeatureStore([ProductsState])] + * } + * ]; + * ``` + */ +export function provideFeatureStore( + states: StateClass[], + ...features: NgxsStoreFeature[] +): EnvironmentProviders { + return makeEnvironmentProviders([ + ...getNgxsFeatureProviders(states), + features, + NGXS_FEATURE_ENVIRONMENT_INITIALIZER + ]); +} diff --git a/packages/store/src/standalone-features/provide-store.ts b/packages/store/src/standalone-features/provide-store.ts new file mode 100644 index 000000000..594751bbc --- /dev/null +++ b/packages/store/src/standalone-features/provide-store.ts @@ -0,0 +1,26 @@ +import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core'; +import { StateClass } from '@ngxs/store/internals'; + +import { getNgxsRootProviders } from './root-providers'; +import { NgxsStoreFeature } from './providers-features'; +import { NGXS_ROOT_ENVIRONMENT_INITIALIZER } from './initializers'; + +/** + * Provides the global Store providers and initializes the Store. + * + * ```ts + * bootstrapApplication(AppComponent, { + * providers: [provideStore([CountriesState])] + * }); + * ``` + */ +export function provideStore( + states: StateClass[] = [], + ...features: NgxsStoreFeature[] +): EnvironmentProviders { + return makeEnvironmentProviders([ + ...getNgxsRootProviders(states, /* options */ {}), + features, + NGXS_ROOT_ENVIRONMENT_INITIALIZER + ]); +} diff --git a/packages/store/src/standalone-features/providers-features.ts b/packages/store/src/standalone-features/providers-features.ts new file mode 100644 index 000000000..dab4a61db --- /dev/null +++ b/packages/store/src/standalone-features/providers-features.ts @@ -0,0 +1,50 @@ +import { EnvironmentProviders, Provider, Type } from '@angular/core'; + +import { CUSTOM_NGXS_EXECUTION_STRATEGY } from '../execution/symbols'; +import { NGXS_OPTIONS, NGXS_PLUGINS, NgxsModuleOptions, NgxsPlugin } from '../symbols'; + +/** + * Registers NGXS options when calling `provideStore`. + * + * ```ts + * bootstrapApplication(AppComponent, { + * providers: [ + * provideStore( + * [CountriesState], + * withNgxsOptions({ developmentMode: !environment.production }) + * ) + * ] + * }); + * ``` + */ +export function withNgxsOptions(options: NgxsModuleOptions = {}): NgxsStoreFeature { + return [ + { provide: NGXS_OPTIONS, useValue: options }, + { provide: CUSTOM_NGXS_EXECUTION_STRATEGY, useValue: options.executionStrategy } + ]; +} + +/** + * Registers custom global plugin for the state. + * + * ```ts + * bootstrapApplication(AppComponent, { + * providers: [ + * provideStore( + * [CountriesState], + * withNgxsOptions({ developmentMode: !environment.production }), + * withNgxsPlugin(logoutPlugin) + * ) + * ] + * }); + * ``` + */ +export function withNgxsPlugin(plugin: Type): NgxsStoreFeature { + return [{ provide: NGXS_PLUGINS, useClass: plugin, multi: true }]; +} + +/** + * A type alias that represents providers that act as NGXS Store features + * available for use with `provideStore`. + */ +export type NgxsStoreFeature = EnvironmentProviders | Provider; diff --git a/packages/store/src/standalone-features/root-providers.ts b/packages/store/src/standalone-features/root-providers.ts new file mode 100644 index 000000000..cbfece9ed --- /dev/null +++ b/packages/store/src/standalone-features/root-providers.ts @@ -0,0 +1,86 @@ +import { APP_BOOTSTRAP_LISTENER, Provider } from '@angular/core'; +import { + INITIAL_STATE_TOKEN, + InitialState, + NgxsBootstrapper, + StateClass, + ɵNGXS_STATE_CONTEXT_FACTORY, + ɵNGXS_STATE_FACTORY +} from '@ngxs/store/internals'; + +import { Store } from '../store'; +import { mergeDeep } from '../utils/utils'; +import { StateStream } from '../plugin_api'; +import { PluginManager } from '../plugin-manager'; +import { StateFactory } from '../internal/state-factory'; +import { Actions, InternalActions } from '../actions-stream'; +import { SelectFactory } from '../decorators/select/select-factory'; +import { CUSTOM_NGXS_EXECUTION_STRATEGY } from '../execution/symbols'; +import { InternalStateOperations } from '../internal/state-operations'; +import { StateContextFactory } from '../internal/state-context-factory'; +import { LifecycleStateManager } from '../internal/lifecycle-state-manager'; +import { InternalDispatchedActionResults, InternalDispatcher } from '../internal/dispatcher'; +import { InternalNgxsExecutionStrategy } from '../execution/internal-ngxs-execution-strategy'; +import { NgxsConfig, NgxsModuleOptions, ROOT_STATE_TOKEN, NGXS_OPTIONS } from '../symbols'; + +/** + * This function returns providers required when calling `NgxsModule.forRoot` + * or `provideStore`. This is shared between NgModule and standalone APIs. + */ +export function getNgxsRootProviders( + states: StateClass[], + options: NgxsModuleOptions +): Provider[] { + return [ + StateFactory, + StateContextFactory, + Actions, + InternalActions, + NgxsBootstrapper, + LifecycleStateManager, + InternalDispatcher, + InternalDispatchedActionResults, + InternalStateOperations, + InternalNgxsExecutionStrategy, + Store, + StateStream, + SelectFactory, + PluginManager, + ...states, + { + provide: ROOT_STATE_TOKEN, + useValue: states + }, + { + provide: APP_BOOTSTRAP_LISTENER, + useFactory: (bootstrapper: NgxsBootstrapper) => () => bootstrapper.bootstrap(), + multi: true, + deps: [NgxsBootstrapper] + }, + { + provide: INITIAL_STATE_TOKEN, + useFactory: () => InitialState.pop() + }, + { + provide: NGXS_OPTIONS, + useValue: options + }, + { + provide: CUSTOM_NGXS_EXECUTION_STRATEGY, + useValue: options.executionStrategy + }, + { + provide: NgxsConfig, + useFactory: (options: NgxsModuleOptions) => mergeDeep(new NgxsConfig(), options), + deps: [NGXS_OPTIONS] + }, + { + provide: ɵNGXS_STATE_CONTEXT_FACTORY, + useExisting: StateContextFactory + }, + { + provide: ɵNGXS_STATE_FACTORY, + useExisting: StateFactory + } + ]; +} diff --git a/packages/store/src/symbols.ts b/packages/store/src/symbols.ts index e35b42113..f61a255f0 100644 --- a/packages/store/src/symbols.ts +++ b/packages/store/src/symbols.ts @@ -9,9 +9,14 @@ import { NgxsExecutionStrategy } from './execution/symbols'; import { SharedSelectorOptions } from './internal/internals'; import { StateToken } from './state-token/state-token'; +// A token used to resolve to states provided in the root context (`forRoot`). export const ROOT_STATE_TOKEN = new InjectionToken('ROOT_STATE_TOKEN'); +// A token used to resolve to states provided on the feature level (`forFeature`). export const FEATURE_STATE_TOKEN = new InjectionToken('FEATURE_STATE_TOKEN'); +// A token used to resolve to custom NGXS plugins. export const NGXS_PLUGINS = new InjectionToken('NGXS_PLUGINS'); +// A token used to resolve to options provided in the root context (`forRoot`). +export const NGXS_OPTIONS = new InjectionToken('NGXS_OPTIONS'); export const META_KEY = 'NGXS_META'; export const META_OPTIONS_KEY = 'NGXS_OPTIONS_META'; diff --git a/packages/store/tests/standalone-features/standalone-feature.spec.ts b/packages/store/tests/standalone-features/standalone-feature.spec.ts new file mode 100644 index 000000000..4363a7deb --- /dev/null +++ b/packages/store/tests/standalone-features/standalone-feature.spec.ts @@ -0,0 +1,195 @@ +import { Component, Injectable, NgZone } from '@angular/core'; +import { bootstrapApplication } from '@angular/platform-browser'; +import { freshPlatform, skipConsoleLogging } from '@ngxs/store/internals/testing'; +import { + Action, + NoopNgxsExecutionStrategy, + State, + StateContext, + Store, + provideFeatureStore, + provideStore, + withNgxsOptions +} from '@ngxs/store'; +import { Router, provideRouter } from '@angular/router'; + +import { NGXS_EXECUTION_STRATEGY } from '../../src/execution/symbols'; +import { DispatchOutsideZoneNgxsExecutionStrategy } from '../../src/execution/dispatch-outside-zone-ngxs-execution-strategy'; + +describe('Standalone features', () => { + class AddCountry { + static readonly type = 'Add country'; + constructor(readonly country: string) {} + } + + @State({ + name: 'countries', + defaults: [] + }) + @Injectable() + class CountriesState { + @Action(AddCountry) + addCountry(ctx: StateContext, action: AddCountry): void { + ctx.setState(state => [...state, action.country]); + } + } + + it( + 'should bootstrap with no explicit options', + freshPlatform(async () => { + // Arrange + @Component({ + selector: 'app-root', + template: '', + standalone: true + }) + class TestComponent {} + + // Act + const appRef = await skipConsoleLogging(() => + bootstrapApplication(TestComponent, { + providers: [provideStore([])] + }) + ); + + // Assert + expect(appRef.injector.get(NGXS_EXECUTION_STRATEGY)).toBeInstanceOf( + DispatchOutsideZoneNgxsExecutionStrategy + ); + + appRef.destroy(); + }) + ); + + it( + 'should overwrite default values when `withNgxsConfig` is provided', + freshPlatform(async () => { + // Arrange + @Component({ + selector: 'app-root', + template: '', + standalone: true + }) + class TestComponent {} + + // Act + const appRef = await skipConsoleLogging(() => + bootstrapApplication(TestComponent, { + providers: [ + provideStore( + [], + withNgxsOptions({ + executionStrategy: NoopNgxsExecutionStrategy + }) + ) + ] + }) + ); + + // Assert + expect(appRef.injector.get(NGXS_EXECUTION_STRATEGY)).toBeInstanceOf( + NoopNgxsExecutionStrategy + ); + + appRef.destroy(); + }) + ); + + it( + 'should bootstrap an app with `provideStore`', + freshPlatform(async () => { + // Arrange + @Component({ + selector: 'app-root', + template: '', + standalone: true + }) + class TestComponent {} + + const appRef = await skipConsoleLogging(() => + bootstrapApplication(TestComponent, { + providers: [ + provideStore( + [CountriesState], + withNgxsOptions({ + developmentMode: false + }) + ) + ] + }) + ); + const store = appRef.injector.get(Store); + + // Assert + expect(store.snapshot()).toEqual({ countries: [] }); + + // Act + store.dispatch(new AddCountry('Canada')); + + // Assert + expect(store.snapshot()).toEqual({ countries: ['Canada'] }); + + appRef.destroy(); + }) + ); + + it( + 'should navigate and provide feature store', + freshPlatform(async () => { + // Arrange + @Component({ + selector: 'app-root', + template: '', + standalone: true + }) + class TestComponent {} + + @Component({ + template: '', + standalone: true + }) + class ProductsComponent {} + + @State({ + name: 'products', + defaults: [] + }) + @Injectable() + class ProductsState {} + + const appRef = await skipConsoleLogging(() => + bootstrapApplication(TestComponent, { + providers: [ + provideRouter([ + { + path: 'products', + loadComponent: async () => ProductsComponent, + providers: [provideFeatureStore([ProductsState])] + } + ]), + provideStore( + [CountriesState], + withNgxsOptions({ + developmentMode: false + }) + ) + ] + }) + ); + const store = appRef.injector.get(Store); + const ngZone = appRef.injector.get(NgZone); + const router = appRef.injector.get(Router); + + // Assert + expect(store.snapshot()).toEqual({ countries: [] }); + + // Act + await ngZone.run(() => router.navigateByUrl('/products')); + + // Assert + expect(store.snapshot()).toEqual({ countries: [], products: [] }); + + appRef.destroy(); + }) + ); +}); diff --git a/packages/websocket-plugin/src/public_api.ts b/packages/websocket-plugin/src/public_api.ts index 847d658d6..7f4906d00 100644 --- a/packages/websocket-plugin/src/public_api.ts +++ b/packages/websocket-plugin/src/public_api.ts @@ -1,4 +1,4 @@ -export { NgxsWebsocketPluginModule } from './websocket.module'; +export { NgxsWebsocketPluginModule, withNgxsWebSocketPlugin } from './websocket.module'; export { NGXS_WEBSOCKET_OPTIONS, NgxsWebsocketPluginOptions, diff --git a/packages/websocket-plugin/src/websocket.module.ts b/packages/websocket-plugin/src/websocket.module.ts index 0e251c0c1..4aea836e1 100644 --- a/packages/websocket-plugin/src/websocket.module.ts +++ b/packages/websocket-plugin/src/websocket.module.ts @@ -1,4 +1,5 @@ import { NgModule, ModuleWithProviders, APP_INITIALIZER, InjectionToken } from '@angular/core'; +import { NgxsStoreFeature } from '@ngxs/store'; import { WebSocketHandler } from './websocket-handler'; import { NgxsWebsocketPluginOptions, NGXS_WEBSOCKET_OPTIONS, noop } from './symbols'; @@ -48,3 +49,22 @@ export class NgxsWebsocketPluginModule { }; } } + +export function withNgxsWebSocketPlugin( + options?: NgxsWebsocketPluginOptions +): NgxsStoreFeature { + return [ + { provide: USER_OPTIONS, useValue: options }, + { + provide: NGXS_WEBSOCKET_OPTIONS, + useFactory: websocketOptionsFactory, + deps: [USER_OPTIONS] + }, + { + provide: APP_INITIALIZER, + useFactory: noop, + deps: [WebSocketHandler], + multi: true + } + ]; +}