diff --git a/modules/data/src/entity-data-config.ts b/modules/data/src/entity-data-config.ts new file mode 100644 index 0000000000..ebd423363d --- /dev/null +++ b/modules/data/src/entity-data-config.ts @@ -0,0 +1,18 @@ +import { InjectionToken } from '@angular/core'; +import { MetaReducer } from '@ngrx/store'; +import { EntityCache } from './reducers/entity-cache'; +import { EntityAction } from './actions/entity-action'; +import { EntityMetadataMap } from './entity-metadata/entity-metadata'; +import { EntityCollection } from './reducers/entity-collection'; + +export interface EntityDataModuleConfig { + entityMetadata?: EntityMetadataMap; + entityCacheMetaReducers?: ( + | MetaReducer + | InjectionToken> + )[]; + entityCollectionMetaReducers?: MetaReducer[]; + // Initial EntityCache state or a function that returns that state + initialEntityCacheState?: EntityCache | (() => EntityCache); + pluralNames?: { [name: string]: string }; +} diff --git a/modules/data/src/entity-data-without-effects.module.ts b/modules/data/src/entity-data-without-effects.module.ts index 152622613a..e378a5c82e 100644 --- a/modules/data/src/entity-data-without-effects.module.ts +++ b/modules/data/src/entity-data-without-effects.module.ts @@ -1,66 +1,10 @@ +import { ModuleWithProviders, NgModule } from '@angular/core'; +import { EntityDataModuleConfig } from './entity-data-config'; import { - ModuleWithProviders, - NgModule, - Inject, - Injector, - InjectionToken, - Optional, - OnDestroy, -} from '@angular/core'; - -import { - Action, - combineReducers, - MetaReducer, - ReducerManager, - StoreModule, -} from '@ngrx/store'; - -import { CorrelationIdGenerator } from './utils/correlation-id-generator'; -import { EntityDispatcherDefaultOptions } from './dispatchers/entity-dispatcher-default-options'; -import { EntityAction } from './actions/entity-action'; -import { EntityActionFactory } from './actions/entity-action-factory'; -import { EntityCache } from './reducers/entity-cache'; -import { EntityCacheDispatcher } from './dispatchers/entity-cache-dispatcher'; -import { entityCacheSelectorProvider } from './selectors/entity-cache-selector'; -import { EntityCollectionServiceElementsFactory } from './entity-services/entity-collection-service-elements-factory'; -import { EntityCollectionServiceFactory } from './entity-services/entity-collection-service-factory'; -import { EntityServices } from './entity-services/entity-services'; -import { EntityCollection } from './reducers/entity-collection'; -import { EntityCollectionCreator } from './reducers/entity-collection-creator'; -import { EntityCollectionReducerFactory } from './reducers/entity-collection-reducer'; -import { EntityCollectionReducerMethodsFactory } from './reducers/entity-collection-reducer-methods'; -import { EntityCollectionReducerRegistry } from './reducers/entity-collection-reducer-registry'; -import { EntityDispatcherFactory } from './dispatchers/entity-dispatcher-factory'; -import { EntityDefinitionService } from './entity-metadata/entity-definition.service'; -import { EntityMetadataMap } from './entity-metadata/entity-metadata'; -import { EntityCacheReducerFactory } from './reducers/entity-cache-reducer'; -import { - ENTITY_CACHE_NAME, - ENTITY_CACHE_NAME_TOKEN, - ENTITY_CACHE_META_REDUCERS, - ENTITY_COLLECTION_META_REDUCERS, - INITIAL_ENTITY_CACHE_STATE, -} from './reducers/constants'; - -import { DefaultLogger } from './utils/default-logger'; -import { EntitySelectorsFactory } from './selectors/entity-selectors'; -import { EntitySelectors$Factory } from './selectors/entity-selectors$'; -import { EntityServicesBase } from './entity-services/entity-services-base'; -import { EntityServicesElements } from './entity-services/entity-services-elements'; -import { Logger, PLURAL_NAMES_TOKEN } from './utils/interfaces'; - -export interface EntityDataModuleConfig { - entityMetadata?: EntityMetadataMap; - entityCacheMetaReducers?: ( - | MetaReducer - | InjectionToken> - )[]; - entityCollectionMetaReducers?: MetaReducer[]; - // Initial EntityCache state or a function that returns that state - initialEntityCacheState?: EntityCache | (() => EntityCache); - pluralNames?: { [name: string]: string }; -} + provideRootEntityDataWithoutEffects, + ENTITY_DATA_WITHOUT_EFFECTS_PROVIDERS, + initializeEntityDataWithoutEffects, +} from './provide-entity-data'; /** * Module without effects or dataservices which means no HTTP calls @@ -69,105 +13,19 @@ export interface EntityDataModuleConfig { * therefore opt-out of @ngrx/effects for entities */ @NgModule({ - imports: [ - StoreModule, // rely on Store feature providers rather than Store.forFeature() - ], - providers: [ - CorrelationIdGenerator, - EntityDispatcherDefaultOptions, - EntityActionFactory, - EntityCacheDispatcher, - EntityCacheReducerFactory, - entityCacheSelectorProvider, - EntityCollectionCreator, - EntityCollectionReducerFactory, - EntityCollectionReducerMethodsFactory, - EntityCollectionReducerRegistry, - EntityCollectionServiceElementsFactory, - EntityCollectionServiceFactory, - EntityDefinitionService, - EntityDispatcherFactory, - EntitySelectorsFactory, - EntitySelectors$Factory, - EntityServicesElements, - { provide: ENTITY_CACHE_NAME_TOKEN, useValue: ENTITY_CACHE_NAME }, - { provide: EntityServices, useClass: EntityServicesBase }, - { provide: Logger, useClass: DefaultLogger }, - ], + providers: [ENTITY_DATA_WITHOUT_EFFECTS_PROVIDERS], }) -export class EntityDataModuleWithoutEffects implements OnDestroy { - private entityCacheFeature: any; - +export class EntityDataModuleWithoutEffects { static forRoot( config: EntityDataModuleConfig ): ModuleWithProviders { return { ngModule: EntityDataModuleWithoutEffects, - providers: [ - { - provide: ENTITY_CACHE_META_REDUCERS, - useValue: config.entityCacheMetaReducers - ? config.entityCacheMetaReducers - : [], - }, - { - provide: ENTITY_COLLECTION_META_REDUCERS, - useValue: config.entityCollectionMetaReducers - ? config.entityCollectionMetaReducers - : [], - }, - { - provide: PLURAL_NAMES_TOKEN, - multi: true, - useValue: config.pluralNames ? config.pluralNames : {}, - }, - ], - }; - } - - constructor( - private reducerManager: ReducerManager, - entityCacheReducerFactory: EntityCacheReducerFactory, - private injector: Injector, - // optional params - @Optional() - @Inject(ENTITY_CACHE_NAME_TOKEN) - private entityCacheName: string, - @Optional() - @Inject(INITIAL_ENTITY_CACHE_STATE) - private initialState: any, - @Optional() - @Inject(ENTITY_CACHE_META_REDUCERS) - private metaReducers: ( - | MetaReducer - | InjectionToken> - )[] - ) { - // Add the @ngrx/data feature to the Store's features - // as Store.forFeature does for StoreFeatureModule - const key = entityCacheName || ENTITY_CACHE_NAME; - - initialState = - typeof initialState === 'function' ? initialState() : initialState; - - const reducers: MetaReducer[] = ( - metaReducers || [] - ).map((mr) => { - return mr instanceof InjectionToken ? injector.get(mr) : mr; - }); - - this.entityCacheFeature = { - key, - reducers: entityCacheReducerFactory.create(), - reducerFactory: combineReducers, - initialState: initialState || {}, - metaReducers: reducers, + providers: [provideRootEntityDataWithoutEffects(config)], }; - reducerManager.addFeature(this.entityCacheFeature); } - // eslint-disable-next-line @angular-eslint/contextual-lifecycle - ngOnDestroy() { - this.reducerManager.removeFeature(this.entityCacheFeature); + constructor() { + initializeEntityDataWithoutEffects(); } } diff --git a/modules/data/src/entity-data.module.ts b/modules/data/src/entity-data.module.ts index 87aa01dbe6..692d76adca 100644 --- a/modules/data/src/entity-data.module.ts +++ b/modules/data/src/entity-data.module.ts @@ -1,37 +1,13 @@ import { ModuleWithProviders, NgModule } from '@angular/core'; -import { EffectsModule, EffectSources } from '@ngrx/effects'; - -import { DefaultDataServiceFactory } from './dataservices/default-data.service'; - -import { - DefaultPersistenceResultHandler, - PersistenceResultHandler, -} from './dataservices/persistence-result-handler.service'; - -import { - DefaultHttpUrlGenerator, - HttpUrlGenerator, -} from './dataservices/http-url-generator'; - -import { EntityCacheDataService } from './dataservices/entity-cache-data.service'; -import { EntityCacheEffects } from './effects/entity-cache-effects'; -import { EntityDataService } from './dataservices/entity-data.service'; -import { EntityEffects } from './effects/entity-effects'; - -import { ENTITY_METADATA_TOKEN } from './entity-metadata/entity-metadata'; - -import { - ENTITY_CACHE_META_REDUCERS, - ENTITY_COLLECTION_META_REDUCERS, -} from './reducers/constants'; -import { Pluralizer, PLURAL_NAMES_TOKEN } from './utils/interfaces'; -import { DefaultPluralizer } from './utils/default-pluralizer'; - +import { EntityDataModuleConfig } from './entity-data-config'; +import { EntityDataModuleWithoutEffects } from './entity-data-without-effects.module'; import { - EntityDataModuleConfig, - EntityDataModuleWithoutEffects, -} from './entity-data-without-effects.module'; + ENTITY_DATA_PROVIDERS, + initializeEntityData, + provideRootEntityData, + provideRootEntityDataWithoutEffects, +} from './provide-entity-data'; /** * entity-data main module includes effects and HTTP data services @@ -39,23 +15,8 @@ import { * No `forFeature` yet. */ @NgModule({ - imports: [ - EntityDataModuleWithoutEffects, - EffectsModule, // do not supply effects because can't replace later - ], - providers: [ - DefaultDataServiceFactory, - EntityCacheDataService, - EntityDataService, - EntityCacheEffects, - EntityEffects, - { provide: HttpUrlGenerator, useClass: DefaultHttpUrlGenerator }, - { - provide: PersistenceResultHandler, - useClass: DefaultPersistenceResultHandler, - }, - { provide: Pluralizer, useClass: DefaultPluralizer }, - ], + imports: [EntityDataModuleWithoutEffects], + providers: [ENTITY_DATA_PROVIDERS], }) export class EntityDataModule { static forRoot( @@ -64,59 +25,13 @@ export class EntityDataModule { return { ngModule: EntityDataModule, providers: [ - // TODO: Moved these effects classes up to EntityDataModule itself - // Remove this comment if that was a mistake. - // EntityCacheEffects, - // EntityEffects, - { - provide: ENTITY_METADATA_TOKEN, - multi: true, - useValue: config.entityMetadata ? config.entityMetadata : [], - }, - { - provide: ENTITY_CACHE_META_REDUCERS, - useValue: config.entityCacheMetaReducers - ? config.entityCacheMetaReducers - : [], - }, - { - provide: ENTITY_COLLECTION_META_REDUCERS, - useValue: config.entityCollectionMetaReducers - ? config.entityCollectionMetaReducers - : [], - }, - { - provide: PLURAL_NAMES_TOKEN, - multi: true, - useValue: config.pluralNames ? config.pluralNames : {}, - }, + provideRootEntityDataWithoutEffects(config), + provideRootEntityData(config), ], }; } - constructor( - private effectSources: EffectSources, - entityCacheEffects: EntityCacheEffects, - entityEffects: EntityEffects - ) { - // We can't use `forFeature()` because, if we did, the developer could not - // replace the entity-data `EntityEffects` with a custom alternative. - // Replacing that class is an extensibility point we need. - // - // The FEATURE_EFFECTS token is not exposed, so can't use that technique. - // Warning: this alternative approach relies on an undocumented API - // to add effect directly rather than through `forFeature()`. - // The danger is that EffectsModule.forFeature evolves and we no longer perform a crucial step. - this.addEffects(entityCacheEffects); - this.addEffects(entityEffects); - } - - /** - * Add another class instance that contains effects. - * @param effectSourceInstance a class instance that implements effects. - * Warning: undocumented @ngrx/effects API - */ - addEffects(effectSourceInstance: any) { - this.effectSources.addEffects(effectSourceInstance); + constructor() { + initializeEntityData(); } } diff --git a/modules/data/src/index.ts b/modules/data/src/index.ts index 79a1da4bcb..ee05fe17e5 100644 --- a/modules/data/src/index.ts +++ b/modules/data/src/index.ts @@ -190,9 +190,15 @@ export { toUpdateFactory, } from './utils/utilities'; +// // EntityDataConfig +export { EntityDataModuleConfig } from './entity-data-config'; + // // EntityDataModule -export { - EntityDataModuleConfig, - EntityDataModuleWithoutEffects, -} from './entity-data-without-effects.module'; +export { EntityDataModuleWithoutEffects } from './entity-data-without-effects.module'; export { EntityDataModule } from './entity-data.module'; + +// // Standalone APIs +export { + provideEntityData, + provideEntityDataWithoutEffects, +} from './provide-entity-data'; diff --git a/modules/data/src/provide-entity-data.ts b/modules/data/src/provide-entity-data.ts new file mode 100644 index 0000000000..57496d23b5 --- /dev/null +++ b/modules/data/src/provide-entity-data.ts @@ -0,0 +1,214 @@ +import { + ENVIRONMENT_INITIALIZER, + EnvironmentProviders, + inject, + InjectionToken, + makeEnvironmentProviders, + Provider, +} from '@angular/core'; +import { + ActionReducerFactory, + combineReducers, + MetaReducer, + ReducerManager, +} from '@ngrx/store'; +import { EffectSources } from '@ngrx/effects'; +import { EntityDispatcherDefaultOptions } from './dispatchers/entity-dispatcher-default-options'; +import { EntityActionFactory } from './actions/entity-action-factory'; +import { EntityCacheDispatcher } from './dispatchers/entity-cache-dispatcher'; +import { entityCacheSelectorProvider } from './selectors/entity-cache-selector'; +import { EntityCollectionServiceElementsFactory } from './entity-services/entity-collection-service-elements-factory'; +import { EntityCollectionServiceFactory } from './entity-services/entity-collection-service-factory'; +import { EntityServices } from './entity-services/entity-services'; +import { EntityCollectionCreator } from './reducers/entity-collection-creator'; +import { EntityCollectionReducerFactory } from './reducers/entity-collection-reducer'; +import { EntityCollectionReducerMethodsFactory } from './reducers/entity-collection-reducer-methods'; +import { EntityCollectionReducerRegistry } from './reducers/entity-collection-reducer-registry'; +import { EntityDispatcherFactory } from './dispatchers/entity-dispatcher-factory'; +import { EntityDefinitionService } from './entity-metadata/entity-definition.service'; +import { EntityCacheReducerFactory } from './reducers/entity-cache-reducer'; +import { + ENTITY_CACHE_META_REDUCERS, + ENTITY_CACHE_NAME, + ENTITY_CACHE_NAME_TOKEN, + ENTITY_COLLECTION_META_REDUCERS, + INITIAL_ENTITY_CACHE_STATE, +} from './reducers/constants'; +import { EntityCache } from './reducers/entity-cache'; +import { EntitySelectorsFactory } from './selectors/entity-selectors'; +import { EntitySelectors$Factory } from './selectors/entity-selectors$'; +import { EntityServicesBase } from './entity-services/entity-services-base'; +import { EntityServicesElements } from './entity-services/entity-services-elements'; +import { DefaultLogger } from './utils/default-logger'; +import { Logger, PLURAL_NAMES_TOKEN, Pluralizer } from './utils/interfaces'; +import { CorrelationIdGenerator } from './utils/correlation-id-generator'; +import { ENTITY_METADATA_TOKEN } from './entity-metadata/entity-metadata'; +import { DefaultDataServiceFactory } from './dataservices/default-data.service'; +import { + DefaultPersistenceResultHandler, + PersistenceResultHandler, +} from './dataservices/persistence-result-handler.service'; +import { + DefaultHttpUrlGenerator, + HttpUrlGenerator, +} from './dataservices/http-url-generator'; +import { EntityCacheDataService } from './dataservices/entity-cache-data.service'; +import { EntityDataService } from './dataservices/entity-data.service'; +import { EntityCacheEffects } from './effects/entity-cache-effects'; +import { EntityEffects } from './effects/entity-effects'; +import { DefaultPluralizer } from './utils/default-pluralizer'; +import { EntityDataModuleConfig } from './entity-data-config'; + +export const ENTITY_DATA_WITHOUT_EFFECTS_PROVIDERS: Provider[] = [ + CorrelationIdGenerator, + EntityDispatcherDefaultOptions, + EntityActionFactory, + EntityCacheDispatcher, + EntityCacheReducerFactory, + entityCacheSelectorProvider, + EntityCollectionCreator, + EntityCollectionReducerFactory, + EntityCollectionReducerMethodsFactory, + EntityCollectionReducerRegistry, + EntityCollectionServiceElementsFactory, + EntityCollectionServiceFactory, + EntityDefinitionService, + EntityDispatcherFactory, + EntitySelectorsFactory, + EntitySelectors$Factory, + EntityServicesElements, + { provide: ENTITY_CACHE_NAME_TOKEN, useValue: ENTITY_CACHE_NAME }, + { provide: EntityServices, useClass: EntityServicesBase }, + { provide: Logger, useClass: DefaultLogger }, +]; + +const ENTITY_DATA_WITHOUT_EFFECTS_ENV_PROVIDER: Provider = { + provide: ENVIRONMENT_INITIALIZER, + multi: true, + useValue: () => initializeEntityDataWithoutEffects(), +}; + +export function initializeEntityDataWithoutEffects(): void { + const reducerManager = inject(ReducerManager); + const entityCacheReducerFactory = inject(EntityCacheReducerFactory); + const entityCacheName = inject(ENTITY_CACHE_NAME_TOKEN, { + optional: true, + }); + const initialStateOrFn = inject(INITIAL_ENTITY_CACHE_STATE, { + optional: true, + }); + const metaReducersOrTokens = inject< + Array | InjectionToken>> + >(ENTITY_CACHE_META_REDUCERS, { + optional: true, + }); + + // Add the @ngrx/data feature to the Store's features + const key = entityCacheName || ENTITY_CACHE_NAME; + const metaReducers = (metaReducersOrTokens || []).map((mr) => { + return mr instanceof InjectionToken ? inject(mr) : mr; + }); + const initialState = + typeof initialStateOrFn === 'function' + ? initialStateOrFn() + : initialStateOrFn; + + const entityCacheFeature = { + key, + reducers: entityCacheReducerFactory.create(), + reducerFactory: combineReducers as ActionReducerFactory, + initialState: initialState || {}, + metaReducers: metaReducers, + }; + reducerManager.addFeature(entityCacheFeature); +} + +export function provideRootEntityDataWithoutEffects( + config: EntityDataModuleConfig +): Provider[] { + return [ + { + provide: ENTITY_CACHE_META_REDUCERS, + useValue: config.entityCacheMetaReducers + ? config.entityCacheMetaReducers + : [], + }, + { + provide: ENTITY_COLLECTION_META_REDUCERS, + useValue: config.entityCollectionMetaReducers + ? config.entityCollectionMetaReducers + : [], + }, + { + provide: PLURAL_NAMES_TOKEN, + multi: true, + useValue: config.pluralNames ? config.pluralNames : {}, + }, + ]; +} + +export function provideEntityDataWithoutEffects( + config: EntityDataModuleConfig +): EnvironmentProviders { + return makeEnvironmentProviders([ + ENTITY_DATA_WITHOUT_EFFECTS_PROVIDERS, + provideRootEntityDataWithoutEffects(config), + ENTITY_DATA_WITHOUT_EFFECTS_ENV_PROVIDER, + ]); +} + +export const ENTITY_DATA_PROVIDERS: Provider[] = [ + DefaultDataServiceFactory, + EntityCacheDataService, + EntityDataService, + EntityCacheEffects, + EntityEffects, + { provide: HttpUrlGenerator, useClass: DefaultHttpUrlGenerator }, + { + provide: PersistenceResultHandler, + useClass: DefaultPersistenceResultHandler, + }, + { provide: Pluralizer, useClass: DefaultPluralizer }, +]; + +const ENTITY_DATA_ENV_PROVIDER: Provider = { + provide: ENVIRONMENT_INITIALIZER, + multi: true, + useValue: () => initializeEntityData(), +}; + +export function initializeEntityData(): void { + const effectsSources = inject(EffectSources); + const entityCacheEffects = inject(EntityCacheEffects); + const entityEffects = inject(EntityEffects); + + effectsSources.addEffects(entityCacheEffects); + effectsSources.addEffects(entityEffects); +} + +export function provideRootEntityData( + config: EntityDataModuleConfig +): Provider[] { + return [ + { + provide: ENTITY_METADATA_TOKEN, + multi: true, + useValue: config.entityMetadata ? config.entityMetadata : [], + }, + ]; +} + +export function provideEntityData( + config: EntityDataModuleConfig +): EnvironmentProviders { + return makeEnvironmentProviders([ + // add EntityDataWithoutEffects providers + ENTITY_DATA_WITHOUT_EFFECTS_PROVIDERS, + provideRootEntityDataWithoutEffects(config), + ENTITY_DATA_WITHOUT_EFFECTS_ENV_PROVIDER, + // add EntityData providers + ENTITY_DATA_PROVIDERS, + provideRootEntityData(config), + ENTITY_DATA_ENV_PROVIDER, + ]); +}