From 9eeca10fc682a03fca9dbe4ea012042e0c70e758 Mon Sep 17 00:00:00 2001 From: Santosh Yadav Date: Wed, 14 Sep 2022 19:44:38 +0000 Subject: [PATCH 01/12] feat(data): Introduce Standalone API for NgRx Data --- .../src/entity-data-without-effects.module.ts | 83 +------ modules/data/src/entity-data.module.ts | 70 +----- modules/data/src/index.ts | 8 +- modules/data/src/provide_entity_data.ts | 222 ++++++++++++++++++ .../standalone-app/src/app/app.component.ts | 2 + .../src/app/board/board.component.html | 7 + .../src/app/board/board.component.scss | 0 .../src/app/board/board.component.ts | 44 ++++ .../src/app/board/board.routes.ts | 9 + .../board/fake-backend-interceptor.service.ts | 91 +++++++ .../src/app/board/state/story-data.service.ts | 19 ++ .../src/app/board/state/story.metadata.ts | 13 + .../src/app/board/state/story.selectors.ts | 17 ++ .../src/app/board/state/story.ts | 18 ++ .../src/app/board/ui/board-ui.component.html | 33 +++ .../src/app/board/ui/board-ui.component.scss | 55 +++++ .../src/app/board/ui/board-ui.component.ts | 52 ++++ projects/standalone-app/src/app/story.ts | 18 ++ projects/standalone-app/src/main.ts | 29 ++- 19 files changed, 643 insertions(+), 147 deletions(-) create mode 100644 modules/data/src/provide_entity_data.ts create mode 100644 projects/standalone-app/src/app/board/board.component.html create mode 100644 projects/standalone-app/src/app/board/board.component.scss create mode 100644 projects/standalone-app/src/app/board/board.component.ts create mode 100644 projects/standalone-app/src/app/board/board.routes.ts create mode 100644 projects/standalone-app/src/app/board/fake-backend-interceptor.service.ts create mode 100644 projects/standalone-app/src/app/board/state/story-data.service.ts create mode 100644 projects/standalone-app/src/app/board/state/story.metadata.ts create mode 100644 projects/standalone-app/src/app/board/state/story.selectors.ts create mode 100644 projects/standalone-app/src/app/board/state/story.ts create mode 100644 projects/standalone-app/src/app/board/ui/board-ui.component.html create mode 100644 projects/standalone-app/src/app/board/ui/board-ui.component.scss create mode 100644 projects/standalone-app/src/app/board/ui/board-ui.component.ts create mode 100644 projects/standalone-app/src/app/story.ts diff --git a/modules/data/src/entity-data-without-effects.module.ts b/modules/data/src/entity-data-without-effects.module.ts index 152622613a..cb5c00cc9e 100644 --- a/modules/data/src/entity-data-without-effects.module.ts +++ b/modules/data/src/entity-data-without-effects.module.ts @@ -16,51 +16,19 @@ import { 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 }; -} +import { + EntityDataModuleConfig, + _provideEntityDataWithoutEffects, +} from './provide_entity_data'; /** * Module without effects or dataservices which means no HTTP calls @@ -72,28 +40,7 @@ export interface EntityDataModuleConfig { 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: [], }) export class EntityDataModuleWithoutEffects implements OnDestroy { private entityCacheFeature: any; @@ -103,25 +50,7 @@ export class EntityDataModuleWithoutEffects implements OnDestroy { ): 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 : {}, - }, - ], + providers: [..._provideEntityDataWithoutEffects(config)], }; } diff --git a/modules/data/src/entity-data.module.ts b/modules/data/src/entity-data.module.ts index 87aa01dbe6..62224f0fa7 100644 --- a/modules/data/src/entity-data.module.ts +++ b/modules/data/src/entity-data.module.ts @@ -2,36 +2,14 @@ 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 { EntityDataModuleWithoutEffects } from './entity-data-without-effects.module'; import { EntityDataModuleConfig, - EntityDataModuleWithoutEffects, -} from './entity-data-without-effects.module'; + _provideEntityData, +} from './provide_entity_data'; /** * entity-data main module includes effects and HTTP data services @@ -43,19 +21,6 @@ import { 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 }, - ], }) export class EntityDataModule { static forRoot( @@ -63,34 +28,7 @@ export class EntityDataModule { ): ModuleWithProviders { 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 : {}, - }, - ], + providers: [..._provideEntityData(config)], }; } diff --git a/modules/data/src/index.ts b/modules/data/src/index.ts index 79a1da4bcb..6398cb57c5 100644 --- a/modules/data/src/index.ts +++ b/modules/data/src/index.ts @@ -191,8 +191,10 @@ export { } from './utils/utilities'; // // EntityDataModule +export { EntityDataModuleWithoutEffects } from './entity-data-without-effects.module'; +export { EntityDataModule } from './entity-data.module'; export { + provideEntityData, + provideEntityDataWithoutEffects, EntityDataModuleConfig, - EntityDataModuleWithoutEffects, -} from './entity-data-without-effects.module'; -export { EntityDataModule } from './entity-data.module'; +} 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..91b21ab29b --- /dev/null +++ b/modules/data/src/provide_entity_data.ts @@ -0,0 +1,222 @@ +import { + EnvironmentProviders, + ENVIRONMENT_INITIALIZER, + inject, + InjectionToken, + makeEnvironmentProviders, + Provider, +} from '@angular/core'; +import { Action, MetaReducer } from '@ngrx/store'; +import { DefaultDataServiceFactory } from './dataservices/default-data.service'; +import { EntityCacheDataService } from './dataservices/entity-cache-data.service'; +import { EntityDataService } from './dataservices/entity-data.service'; +import { + DefaultHttpUrlGenerator, + HttpUrlGenerator, +} from './dataservices/http-url-generator'; +import { + DefaultPersistenceResultHandler, + PersistenceResultHandler, +} from './dataservices/persistence-result-handler.service'; +import { EntityCacheEffects } from './effects/entity-cache-effects'; +import { EntityEffects } from './effects/entity-effects'; +import { + EntityMetadataMap, + ENTITY_METADATA_TOKEN, +} from './entity-metadata/entity-metadata'; +import { + ENTITY_CACHE_META_REDUCERS, + ENTITY_CACHE_NAME, + ENTITY_CACHE_NAME_TOKEN, + ENTITY_COLLECTION_META_REDUCERS, +} from './reducers/constants'; +import { DefaultPluralizer } from './utils/default-pluralizer'; +import { Logger, Pluralizer, PLURAL_NAMES_TOKEN } from './utils/interfaces'; +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 { EntityCacheDispatcher } from './dispatchers/entity-cache-dispatcher'; +import { + createEntityCacheSelector, + entityCacheSelectorProvider, + ENTITY_CACHE_SELECTOR_TOKEN, +} 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 { CorrelationIdGenerator } from './utils/correlation-id-generator'; +import { EntityDispatcherDefaultOptions } from './dispatchers/entity-dispatcher-default-options'; +import { EntityActionFactory } from './actions/entity-action-factory'; +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 { EntityCacheReducerFactory } from './reducers/entity-cache-reducer'; +import { EntityCache } from './reducers/entity-cache'; +import { EntityCollection } from './reducers/entity-collection'; +import { EntityAction } from './actions/entity-action'; + +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 }; +} + +export function _provideEntityData(config: EntityDataModuleConfig): Provider[] { + return [ + DefaultDataServiceFactory, + EntityCacheDataService, + EntityDataService, + EntityCacheEffects, + EntityEffects, + EntityCacheReducerFactory, + EntityCollectionCreator, + EntityCollectionReducerRegistry, + EntityCollectionReducerFactory, + EntityCollectionReducerMethodsFactory, + EntityDefinitionService, + EntityCollectionServiceElementsFactory, + EntityDispatcherFactory, + EntityActionFactory, + EntityDispatcherDefaultOptions, + CorrelationIdGenerator, + EntitySelectorsFactory, + EntitySelectors$Factory, + { provide: EntityServices, useClass: EntityServicesBase }, + { provide: HttpUrlGenerator, useClass: DefaultHttpUrlGenerator }, + { + provide: PersistenceResultHandler, + useClass: DefaultPersistenceResultHandler, + }, + { provide: Pluralizer, useClass: DefaultPluralizer }, + { + 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 : {}, + }, + ]; +} + +export function _provideEntityDataWithoutEffects( + config: EntityDataModuleConfig +) { + return [ + 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 }, + { + 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 : {}, + }, + ]; +} + +/** + * + * @usageNotes + * + * ```ts + * bootstrapApplication(AppComponent, { + * providers: [provideEntityData()], + * }); + * ``` + */ +export function provideEntityData( + config: EntityDataModuleConfig +): EnvironmentProviders { + return makeEnvironmentProviders([ + ..._provideEntityData(config), + ENVIRONMENT_STATE_PROVIDER, + ]); +} + +/** + * + * @usageNotes + * + * ```ts + * bootstrapApplication(AppComponent, { + * providers: [provideEntityDataWithoutEffects()], + * }); + * ``` + */ +export function provideEntityDataWithoutEffects( + config: EntityDataModuleConfig +): EnvironmentProviders { + return makeEnvironmentProviders([ + ..._provideEntityDataWithoutEffects(config), + ENVIRONMENT_STATE_PROVIDER + ]); +} + +const ENVIRONMENT_STATE_PROVIDER: Provider[] = [ + { + provide: ENTITY_CACHE_SELECTOR_TOKEN, + useFactory: createEntityCacheSelector, + }, + { + provide: ENVIRONMENT_INITIALIZER, + multi: true, + deps: [], + useFactory() { + return () => inject(ENTITY_CACHE_SELECTOR_TOKEN); + }, + }, +]; diff --git a/projects/standalone-app/src/app/app.component.ts b/projects/standalone-app/src/app/app.component.ts index 15ff68a86f..1e550e444d 100644 --- a/projects/standalone-app/src/app/app.component.ts +++ b/projects/standalone-app/src/app/app.component.ts @@ -9,6 +9,8 @@ import { RouterModule } from '@angular/router';

Welcome {{ title }}

Load Feature +
+ Boards `, diff --git a/projects/standalone-app/src/app/board/board.component.html b/projects/standalone-app/src/app/board/board.component.html new file mode 100644 index 0000000000..c0c2758426 --- /dev/null +++ b/projects/standalone-app/src/app/board/board.component.html @@ -0,0 +1,7 @@ + + diff --git a/projects/standalone-app/src/app/board/board.component.scss b/projects/standalone-app/src/app/board/board.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/projects/standalone-app/src/app/board/board.component.ts b/projects/standalone-app/src/app/board/board.component.ts new file mode 100644 index 0000000000..4c8de1c1d1 --- /dev/null +++ b/projects/standalone-app/src/app/board/board.component.ts @@ -0,0 +1,44 @@ +import { Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { StoryDataService } from './state/story-data.service'; +import { + CreateStoryDto, + DeleteStoryDto, + Stories, + UpdateStoryDto, +} from './state/story'; +import { CommonModule } from '@angular/common'; +import { BoardUiComponent } from './ui/board-ui.component'; + +@Component({ + selector: 'ngrx-board-component', + templateUrl: './board.component.html', + styleUrls: ['./board.component.scss'], + standalone: true, + imports: [CommonModule, BoardUiComponent], +}) +export class BoardComponent implements OnInit { + stories$: Observable = this.storyDataService.groupedStories$; + + constructor(private storyDataService: StoryDataService) {} + + ngOnInit(): void { + this.storyDataService.getAll(); + } + + add(story: CreateStoryDto): void { + this.storyDataService.add(story, { isOptimistic: false }); + } + + update(story: UpdateStoryDto): void { + this.storyDataService.update(story, { isOptimistic: true }); + } + + loadAll(): void { + this.storyDataService.getAll(); + } + + delete(id: DeleteStoryDto): void { + this.storyDataService.delete(id); + } +} diff --git a/projects/standalone-app/src/app/board/board.routes.ts b/projects/standalone-app/src/app/board/board.routes.ts new file mode 100644 index 0000000000..44919dd6a9 --- /dev/null +++ b/projects/standalone-app/src/app/board/board.routes.ts @@ -0,0 +1,9 @@ +import { Routes } from '@angular/router'; +import { BoardComponent } from './board.component'; + +export const routes: Routes = [ + { + path: '', + component: BoardComponent, + }, +]; diff --git a/projects/standalone-app/src/app/board/fake-backend-interceptor.service.ts b/projects/standalone-app/src/app/board/fake-backend-interceptor.service.ts new file mode 100644 index 0000000000..b142f4a1bd --- /dev/null +++ b/projects/standalone-app/src/app/board/fake-backend-interceptor.service.ts @@ -0,0 +1,91 @@ +import { Injectable } from '@angular/core'; +import { + HttpErrorResponse, + HttpEvent, + HttpHandler, + HttpInterceptor, + HttpRequest, + HttpResponse, +} from '@angular/common/http'; +import { Observable, of, throwError } from 'rxjs'; +import { Story } from './state/story'; + +const data: Record = {}; + +@Injectable({ + providedIn: 'root', +}) +export class FakeBackendInterceptor implements HttpInterceptor { + intercept( + req: HttpRequest, + next: HttpHandler + ): Observable> { + const { url, body } = req; + + let storyId: string | undefined; + + if (req.method === 'GET' && req.url.includes('stories')) { + return of(new HttpResponse({ status: 200, body: Object.values(data) })); + } + + switch (req.method) { + case 'GET': + storyId = url.split('/').pop(); + + if (!storyId) { + return throwError(() => new HttpErrorResponse({ status: 400 })); + } + + const obj = data[storyId]; + if (obj) { + return of(new HttpResponse({ status: 200, body: obj })); + } + + return throwError(() => new HttpErrorResponse({ status: 404 })); + + case 'POST': + storyId = Date.now().toString(); + data[storyId] = { + ...body, + storyId, + createdAt: new Date(), + updatedAt: new Date(), + }; + + return of(new HttpResponse({ status: 201, body: data[storyId] })); + + case 'PUT': + storyId = url.split('/').pop(); + + if (!storyId) { + return throwError(() => new HttpErrorResponse({ status: 400 })); + } + + if (!data[storyId]) { + return throwError(() => new HttpErrorResponse({ status: 404 })); + } + + data[storyId] = { + ...data[storyId], + ...body, + updatedAt: new Date(), + }; + + return of(new HttpResponse({ status: 200, body: data[storyId] })); + + case 'DELETE': + storyId = url.split('/').pop(); + + if (!storyId) { + return throwError(() => new HttpErrorResponse({ status: 400 })); + } + + delete data[storyId]; + + return of(new HttpResponse({ status: 200, body: storyId })); + + default: + return throwError(() => new HttpErrorResponse({ status: 501 })); + } + } +} diff --git a/projects/standalone-app/src/app/board/state/story-data.service.ts b/projects/standalone-app/src/app/board/state/story-data.service.ts new file mode 100644 index 0000000000..b73a1f0a01 --- /dev/null +++ b/projects/standalone-app/src/app/board/state/story-data.service.ts @@ -0,0 +1,19 @@ +import { + EntityCollectionServiceBase, + EntityCollectionServiceElementsFactory, +} from '@ngrx/data'; +import { select } from '@ngrx/store'; +import { Injectable } from '@angular/core'; +import { selectStories } from './story.selectors'; +import { Story } from './story'; + +@Injectable({ + providedIn: 'root', +}) +export class StoryDataService extends EntityCollectionServiceBase { + groupedStories$ = this.entities$.pipe(select(selectStories)); + + constructor(serviceElementsFactory: EntityCollectionServiceElementsFactory) { + super('Story', serviceElementsFactory); + } +} diff --git a/projects/standalone-app/src/app/board/state/story.metadata.ts b/projects/standalone-app/src/app/board/state/story.metadata.ts new file mode 100644 index 0000000000..8969f79214 --- /dev/null +++ b/projects/standalone-app/src/app/board/state/story.metadata.ts @@ -0,0 +1,13 @@ +import { EntityMetadata } from '@ngrx/data'; +import { Story } from './story'; + +export const storyEntityMetadata: EntityMetadata = { + entityName: 'Story', + selectId: (entity: Story): string => entity.storyId, + sortComparer: (a, b) => a.createdAt.getTime() - b.createdAt.getTime(), + filterFn: (entities, pattern) => + entities.filter( + (entity) => + entity.title?.includes(pattern) || entity.title?.includes(pattern) + ), +}; diff --git a/projects/standalone-app/src/app/board/state/story.selectors.ts b/projects/standalone-app/src/app/board/state/story.selectors.ts new file mode 100644 index 0000000000..407a1587ba --- /dev/null +++ b/projects/standalone-app/src/app/board/state/story.selectors.ts @@ -0,0 +1,17 @@ +import { createSelector } from '@ngrx/store'; +import { Stories, Story } from './story'; + +export const selectStories = createSelector( + (stories) => stories, + (stories: Stories) => + stories.reduce( + (prev, cur) => { + prev[cur.column].push(cur); + + prev[cur.column].sort((a, b) => a.order - b.order); + + return prev; + }, + [[], [], [], []] + ) +); diff --git a/projects/standalone-app/src/app/board/state/story.ts b/projects/standalone-app/src/app/board/state/story.ts new file mode 100644 index 0000000000..502e51e584 --- /dev/null +++ b/projects/standalone-app/src/app/board/state/story.ts @@ -0,0 +1,18 @@ +export interface Story { + storyId: string; + order: number; + column: number; + title: string; + description: string; + createdAt: Date; + updatedAt: Date; +} + +export type Stories = Story[]; + +export type CreateStoryDto = Partial; + +export type UpdateStoryDto = Required> & + Partial>; + +export type DeleteStoryDto = string; diff --git a/projects/standalone-app/src/app/board/ui/board-ui.component.html b/projects/standalone-app/src/app/board/ui/board-ui.component.html new file mode 100644 index 0000000000..04d7dc2a9a --- /dev/null +++ b/projects/standalone-app/src/app/board/ui/board-ui.component.html @@ -0,0 +1,33 @@ +
+
+

Column {{ i }}

+ + + +
+
+ + - + +
+
+
+
diff --git a/projects/standalone-app/src/app/board/ui/board-ui.component.scss b/projects/standalone-app/src/app/board/ui/board-ui.component.scss new file mode 100644 index 0000000000..2de4cc03ab --- /dev/null +++ b/projects/standalone-app/src/app/board/ui/board-ui.component.scss @@ -0,0 +1,55 @@ +.list-container { + width: 400px; + max-width: 100%; + margin: 0 25px 25px 0; + display: inline-block; + vertical-align: top; + border: black; +} + +.drag-list { + padding: 20px; + border: solid 1px #ccc; + min-height: 60px; + background: white; + border-radius: 4px; + overflow: hidden; + display: block; +} + +.drag-box { + padding: 20px 10px; + border: solid 1px #ccc; + color: rgba(0, 0, 0, 0.87); + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + box-sizing: border-box; + cursor: move; + background: white; + font-size: 14px; +} + +.cdk-drag-preview { + box-sizing: border-box; + border-radius: 4px; + box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), + 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12); +} + +.cdk-drag-placeholder { + opacity: 0; +} + +.cdk-drag-animating { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} + +.drag-box:last-child { + border: none; +} + +.drag.cdk-drop-list-dragging .drag-box:not(.cdk-drag-placeholder) { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} diff --git a/projects/standalone-app/src/app/board/ui/board-ui.component.ts b/projects/standalone-app/src/app/board/ui/board-ui.component.ts new file mode 100644 index 0000000000..b02bcb2cb5 --- /dev/null +++ b/projects/standalone-app/src/app/board/ui/board-ui.component.ts @@ -0,0 +1,52 @@ +import { CdkDragDrop, DragDropModule } from '@angular/cdk/drag-drop'; +import { CommonModule } from '@angular/common'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { + CreateStoryDto, + DeleteStoryDto, + Stories, + Story, + UpdateStoryDto, +} from '../state/story'; + +@Component({ + selector: 'ngrx-board-ui', + templateUrl: './board-ui.component.html', + styleUrls: ['./board-ui.component.scss'], + standalone: true, + imports: [CommonModule, DragDropModule, FormsModule], +}) +export class BoardUiComponent { + @Input() stories: Stories[] = []; + + @Output() add = new EventEmitter(); + + @Output() delete = new EventEmitter(); + + @Output() update = new EventEmitter(); + + addNew(column: number, stories: Stories): void { + this.add.emit({ + order: stories.length, + column, + title: `Order ${stories.length} Column ${column}`, + description: '', + }); + } + + dropStory(event: CdkDragDrop, column: number): void { + this.update.emit({ + column, + order: event.currentIndex, + storyId: event.item.data.storyId, + }); + } + + updateStory(story: Story, title: string): void { + this.update.emit({ + storyId: story.storyId, + title, + }); + } +} diff --git a/projects/standalone-app/src/app/story.ts b/projects/standalone-app/src/app/story.ts new file mode 100644 index 0000000000..502e51e584 --- /dev/null +++ b/projects/standalone-app/src/app/story.ts @@ -0,0 +1,18 @@ +export interface Story { + storyId: string; + order: number; + column: number; + title: string; + description: string; + createdAt: Date; + updatedAt: Date; +} + +export type Stories = Story[]; + +export type CreateStoryDto = Partial; + +export type UpdateStoryDto = Required> & + Partial>; + +export type DeleteStoryDto = string; diff --git a/projects/standalone-app/src/main.ts b/projects/standalone-app/src/main.ts index 68865919a6..23f4540278 100644 --- a/projects/standalone-app/src/main.ts +++ b/projects/standalone-app/src/main.ts @@ -8,6 +8,9 @@ import { provideStore } from '@ngrx/store'; import { provideEffects } from '@ngrx/effects'; import { provideRouterStore, routerReducer } from '@ngrx/router-store'; import { provideStoreDevtools } from '@ngrx/store-devtools'; +import { provideEntityData } from '@ngrx/data'; +import { EntityMetadata } from '@ngrx/data'; +import { Story } from './app/story'; import { AppComponent } from './app/app.component'; @@ -18,6 +21,17 @@ if (environment.production) { enableProdMode(); } +export const storyEntityMetadata: EntityMetadata = { + entityName: 'Story', + selectId: (entity: Story): string => entity.storyId, + sortComparer: (a, b) => a.createdAt.getTime() - b.createdAt.getTime(), + filterFn: (entities, pattern) => + entities.filter( + (entity) => + entity.title?.includes(pattern) || entity.title?.includes(pattern) + ), +}; + bootstrapApplication(AppComponent, { providers: [ provideStore({ router: routerReducer }), @@ -28,6 +42,11 @@ bootstrapApplication(AppComponent, { loadChildren: () => import('./app/lazy/feature.routes').then((m) => m.routes), }, + { + path: 'board', + loadChildren: () => + import('./app/board/board.routes').then((m) => m.routes), + }, ], withEnabledBlockingInitialNavigation() ), @@ -38,5 +57,13 @@ bootstrapApplication(AppComponent, { }), provideRouterStore(), provideEffects(AppEffects), - ], + provideEntityData({ + entityMetadata: { + Story: storyEntityMetadata, + }, + pluralNames: { + Story: 'stories', + }, + }), + ] }); From c8843754a615709bf3f00f8c70baee92bec94a02 Mon Sep 17 00:00:00 2001 From: Santosh Yadav Date: Tue, 1 Nov 2022 22:34:58 +0100 Subject: [PATCH 02/12] feat(data): Introduce Standalone API for NgRx Data --- modules/data/src/provide_entity_data.ts | 10 ++++------ projects/standalone-app/src/app/app.component.ts | 2 ++ projects/standalone-app/src/app/board/board.route.ts | 9 +++++++++ 3 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 projects/standalone-app/src/app/board/board.route.ts diff --git a/modules/data/src/provide_entity_data.ts b/modules/data/src/provide_entity_data.ts index 91b21ab29b..81faf0a885 100644 --- a/modules/data/src/provide_entity_data.ts +++ b/modules/data/src/provide_entity_data.ts @@ -39,11 +39,6 @@ import { EntityCollectionReducerRegistry } from './reducers/entity-collection-re import { EntityDispatcherFactory } from './dispatchers/entity-dispatcher-factory'; import { EntityDefinitionService } from './entity-metadata/entity-definition.service'; import { EntityCacheDispatcher } from './dispatchers/entity-cache-dispatcher'; -import { - createEntityCacheSelector, - entityCacheSelectorProvider, - ENTITY_CACHE_SELECTOR_TOKEN, -} 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'; @@ -59,6 +54,10 @@ import { EntityCacheReducerFactory } from './reducers/entity-cache-reducer'; import { EntityCache } from './reducers/entity-cache'; import { EntityCollection } from './reducers/entity-collection'; import { EntityAction } from './actions/entity-action'; +import { + createEntityCacheSelector, + ENTITY_CACHE_SELECTOR_TOKEN, +} from './selectors/entity-cache-selector'; export interface EntityDataModuleConfig { entityMetadata?: EntityMetadataMap; @@ -133,7 +132,6 @@ export function _provideEntityDataWithoutEffects( EntityActionFactory, EntityCacheDispatcher, EntityCacheReducerFactory, - entityCacheSelectorProvider, EntityCollectionCreator, EntityCollectionReducerFactory, EntityCollectionReducerMethodsFactory, diff --git a/projects/standalone-app/src/app/app.component.ts b/projects/standalone-app/src/app/app.component.ts index 1e550e444d..12d0d471c8 100644 --- a/projects/standalone-app/src/app/app.component.ts +++ b/projects/standalone-app/src/app/app.component.ts @@ -12,6 +12,8 @@ import { RouterModule } from '@angular/router';
Boards + Boards + `, }) diff --git a/projects/standalone-app/src/app/board/board.route.ts b/projects/standalone-app/src/app/board/board.route.ts new file mode 100644 index 0000000000..44919dd6a9 --- /dev/null +++ b/projects/standalone-app/src/app/board/board.route.ts @@ -0,0 +1,9 @@ +import { Routes } from '@angular/router'; +import { BoardComponent } from './board.component'; + +export const routes: Routes = [ + { + path: '', + component: BoardComponent, + }, +]; From 39cd7c541b0bb520d972c721e33d859d964ba362 Mon Sep 17 00:00:00 2001 From: Santosh Yadav Date: Tue, 1 Nov 2022 23:26:25 +0100 Subject: [PATCH 03/12] feat(data): Introduce Standalone API for NgRx Data --- projects/standalone-app-e2e/src/support/app.po.ts | 2 +- projects/standalone-app/src/app/app.component.ts | 2 -- .../src/app/board/fake-backend-interceptor.service.ts | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/projects/standalone-app-e2e/src/support/app.po.ts b/projects/standalone-app-e2e/src/support/app.po.ts index 19f735f532..3038b9c569 100644 --- a/projects/standalone-app-e2e/src/support/app.po.ts +++ b/projects/standalone-app-e2e/src/support/app.po.ts @@ -1,2 +1,2 @@ export const getGreeting = () => cy.get('h1'); -export const loadFeature = () => cy.get('a').click(); +export const loadFeature = () => cy.get('a').contains('Load Feature').click(); diff --git a/projects/standalone-app/src/app/app.component.ts b/projects/standalone-app/src/app/app.component.ts index 12d0d471c8..1e550e444d 100644 --- a/projects/standalone-app/src/app/app.component.ts +++ b/projects/standalone-app/src/app/app.component.ts @@ -12,8 +12,6 @@ import { RouterModule } from '@angular/router';
Boards - Boards - `, }) diff --git a/projects/standalone-app/src/app/board/fake-backend-interceptor.service.ts b/projects/standalone-app/src/app/board/fake-backend-interceptor.service.ts index b142f4a1bd..baf57774d2 100644 --- a/projects/standalone-app/src/app/board/fake-backend-interceptor.service.ts +++ b/projects/standalone-app/src/app/board/fake-backend-interceptor.service.ts @@ -35,7 +35,7 @@ export class FakeBackendInterceptor implements HttpInterceptor { if (!storyId) { return throwError(() => new HttpErrorResponse({ status: 400 })); } - + // eslint-disable-next-line no-case-declarations const obj = data[storyId]; if (obj) { return of(new HttpResponse({ status: 200, body: obj })); From 6a3f81fba99d7a8494d1d1a6ed3b1f02e910c811 Mon Sep 17 00:00:00 2001 From: Santosh Yadav Date: Thu, 3 Nov 2022 21:05:30 +0100 Subject: [PATCH 04/12] feat(data): Introduce Standalone API for NgRx Data --- modules/data/src/entity-data-config.ts | 18 ++ .../src/entity-data-without-effects.module.ts | 93 +------- modules/data/src/entity-data.module.ts | 47 ++-- modules/data/src/index.ts | 8 +- modules/data/src/provide-entity-data.ts | 214 +++++++++++++++++ modules/data/src/provide_entity_data.ts | 220 ------------------ 6 files changed, 263 insertions(+), 337 deletions(-) create mode 100644 modules/data/src/entity-data-config.ts create mode 100644 modules/data/src/provide-entity-data.ts delete mode 100644 modules/data/src/provide_entity_data.ts 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 cb5c00cc9e..e378a5c82e 100644 --- a/modules/data/src/entity-data-without-effects.module.ts +++ b/modules/data/src/entity-data-without-effects.module.ts @@ -1,34 +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 { EntityCache } from './reducers/entity-cache'; -import { EntityCacheReducerFactory } from './reducers/entity-cache-reducer'; -import { - ENTITY_CACHE_NAME, - ENTITY_CACHE_NAME_TOKEN, - ENTITY_CACHE_META_REDUCERS, - INITIAL_ENTITY_CACHE_STATE, -} from './reducers/constants'; - -import { - EntityDataModuleConfig, - _provideEntityDataWithoutEffects, -} from './provide_entity_data'; + provideRootEntityDataWithoutEffects, + ENTITY_DATA_WITHOUT_EFFECTS_PROVIDERS, + initializeEntityDataWithoutEffects, +} from './provide-entity-data'; /** * Module without effects or dataservices which means no HTTP calls @@ -37,66 +13,19 @@ import { * therefore opt-out of @ngrx/effects for entities */ @NgModule({ - imports: [ - StoreModule, // rely on Store feature providers rather than Store.forFeature() - ], - providers: [], + 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: [..._provideEntityDataWithoutEffects(config)], - }; - } - - 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 62224f0fa7..fd040ac3a7 100644 --- a/modules/data/src/entity-data.module.ts +++ b/modules/data/src/entity-data.module.ts @@ -5,11 +5,14 @@ import { EffectsModule, EffectSources } from '@ngrx/effects'; import { EntityCacheEffects } from './effects/entity-cache-effects'; import { EntityEffects } from './effects/entity-effects'; -import { EntityDataModuleWithoutEffects } from './entity-data-without-effects.module'; +import { EntityDataModuleConfig } from './entity-data-config'; import { - EntityDataModuleConfig, - _provideEntityData, -} from './provide_entity_data'; + ENTITY_DATA_PROVIDERS, + ENTITY_DATA_WITHOUT_EFFECTS_PROVIDERS, + initializeEntityData, + provideRootEntityData, + provideRootEntityDataWithoutEffects, +} from './provide-entity-data'; /** * entity-data main module includes effects and HTTP data services @@ -17,10 +20,7 @@ import { * No `forFeature` yet. */ @NgModule({ - imports: [ - EntityDataModuleWithoutEffects, - EffectsModule, // do not supply effects because can't replace later - ], + providers: [ENTITY_DATA_WITHOUT_EFFECTS_PROVIDERS, ENTITY_DATA_PROVIDERS], }) export class EntityDataModule { static forRoot( @@ -28,33 +28,14 @@ export class EntityDataModule { ): ModuleWithProviders { return { ngModule: EntityDataModule, - providers: [..._provideEntityData(config)], + providers: [ + 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 6398cb57c5..ee05fe17e5 100644 --- a/modules/data/src/index.ts +++ b/modules/data/src/index.ts @@ -190,11 +190,15 @@ export { toUpdateFactory, } from './utils/utilities'; +// // EntityDataConfig +export { EntityDataModuleConfig } from './entity-data-config'; + // // EntityDataModule export { EntityDataModuleWithoutEffects } from './entity-data-without-effects.module'; export { EntityDataModule } from './entity-data.module'; + +// // Standalone APIs export { provideEntityData, provideEntityDataWithoutEffects, - EntityDataModuleConfig, -} from './provide_entity_data'; +} 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, + ]); +} diff --git a/modules/data/src/provide_entity_data.ts b/modules/data/src/provide_entity_data.ts deleted file mode 100644 index 81faf0a885..0000000000 --- a/modules/data/src/provide_entity_data.ts +++ /dev/null @@ -1,220 +0,0 @@ -import { - EnvironmentProviders, - ENVIRONMENT_INITIALIZER, - inject, - InjectionToken, - makeEnvironmentProviders, - Provider, -} from '@angular/core'; -import { Action, MetaReducer } from '@ngrx/store'; -import { DefaultDataServiceFactory } from './dataservices/default-data.service'; -import { EntityCacheDataService } from './dataservices/entity-cache-data.service'; -import { EntityDataService } from './dataservices/entity-data.service'; -import { - DefaultHttpUrlGenerator, - HttpUrlGenerator, -} from './dataservices/http-url-generator'; -import { - DefaultPersistenceResultHandler, - PersistenceResultHandler, -} from './dataservices/persistence-result-handler.service'; -import { EntityCacheEffects } from './effects/entity-cache-effects'; -import { EntityEffects } from './effects/entity-effects'; -import { - EntityMetadataMap, - ENTITY_METADATA_TOKEN, -} from './entity-metadata/entity-metadata'; -import { - ENTITY_CACHE_META_REDUCERS, - ENTITY_CACHE_NAME, - ENTITY_CACHE_NAME_TOKEN, - ENTITY_COLLECTION_META_REDUCERS, -} from './reducers/constants'; -import { DefaultPluralizer } from './utils/default-pluralizer'; -import { Logger, Pluralizer, PLURAL_NAMES_TOKEN } from './utils/interfaces'; -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 { EntityCacheDispatcher } from './dispatchers/entity-cache-dispatcher'; -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 { CorrelationIdGenerator } from './utils/correlation-id-generator'; -import { EntityDispatcherDefaultOptions } from './dispatchers/entity-dispatcher-default-options'; -import { EntityActionFactory } from './actions/entity-action-factory'; -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 { EntityCacheReducerFactory } from './reducers/entity-cache-reducer'; -import { EntityCache } from './reducers/entity-cache'; -import { EntityCollection } from './reducers/entity-collection'; -import { EntityAction } from './actions/entity-action'; -import { - createEntityCacheSelector, - ENTITY_CACHE_SELECTOR_TOKEN, -} from './selectors/entity-cache-selector'; - -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 }; -} - -export function _provideEntityData(config: EntityDataModuleConfig): Provider[] { - return [ - DefaultDataServiceFactory, - EntityCacheDataService, - EntityDataService, - EntityCacheEffects, - EntityEffects, - EntityCacheReducerFactory, - EntityCollectionCreator, - EntityCollectionReducerRegistry, - EntityCollectionReducerFactory, - EntityCollectionReducerMethodsFactory, - EntityDefinitionService, - EntityCollectionServiceElementsFactory, - EntityDispatcherFactory, - EntityActionFactory, - EntityDispatcherDefaultOptions, - CorrelationIdGenerator, - EntitySelectorsFactory, - EntitySelectors$Factory, - { provide: EntityServices, useClass: EntityServicesBase }, - { provide: HttpUrlGenerator, useClass: DefaultHttpUrlGenerator }, - { - provide: PersistenceResultHandler, - useClass: DefaultPersistenceResultHandler, - }, - { provide: Pluralizer, useClass: DefaultPluralizer }, - { - 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 : {}, - }, - ]; -} - -export function _provideEntityDataWithoutEffects( - config: EntityDataModuleConfig -) { - return [ - CorrelationIdGenerator, - EntityDispatcherDefaultOptions, - EntityActionFactory, - EntityCacheDispatcher, - EntityCacheReducerFactory, - 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 }, - { - 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 : {}, - }, - ]; -} - -/** - * - * @usageNotes - * - * ```ts - * bootstrapApplication(AppComponent, { - * providers: [provideEntityData()], - * }); - * ``` - */ -export function provideEntityData( - config: EntityDataModuleConfig -): EnvironmentProviders { - return makeEnvironmentProviders([ - ..._provideEntityData(config), - ENVIRONMENT_STATE_PROVIDER, - ]); -} - -/** - * - * @usageNotes - * - * ```ts - * bootstrapApplication(AppComponent, { - * providers: [provideEntityDataWithoutEffects()], - * }); - * ``` - */ -export function provideEntityDataWithoutEffects( - config: EntityDataModuleConfig -): EnvironmentProviders { - return makeEnvironmentProviders([ - ..._provideEntityDataWithoutEffects(config), - ENVIRONMENT_STATE_PROVIDER - ]); -} - -const ENVIRONMENT_STATE_PROVIDER: Provider[] = [ - { - provide: ENTITY_CACHE_SELECTOR_TOKEN, - useFactory: createEntityCacheSelector, - }, - { - provide: ENVIRONMENT_INITIALIZER, - multi: true, - deps: [], - useFactory() { - return () => inject(ENTITY_CACHE_SELECTOR_TOKEN); - }, - }, -]; From 071a95643f1f92d7ef7ea3ecc289e93eea35f3b5 Mon Sep 17 00:00:00 2001 From: Santosh Yadav Date: Thu, 3 Nov 2022 21:42:46 +0100 Subject: [PATCH 05/12] feat(data): fix sample app --- .../board/fake-backend-interceptor.service.ts | 125 +++++++++--------- projects/standalone-app/src/main.ts | 17 ++- 2 files changed, 73 insertions(+), 69 deletions(-) diff --git a/projects/standalone-app/src/app/board/fake-backend-interceptor.service.ts b/projects/standalone-app/src/app/board/fake-backend-interceptor.service.ts index baf57774d2..6793019749 100644 --- a/projects/standalone-app/src/app/board/fake-backend-interceptor.service.ts +++ b/projects/standalone-app/src/app/board/fake-backend-interceptor.service.ts @@ -3,7 +3,9 @@ import { HttpErrorResponse, HttpEvent, HttpHandler, + HttpHandlerFn, HttpInterceptor, + HttpInterceptorFn, HttpRequest, HttpResponse, } from '@angular/common/http'; @@ -12,80 +14,79 @@ import { Story } from './state/story'; const data: Record = {}; -@Injectable({ - providedIn: 'root', -}) -export class FakeBackendInterceptor implements HttpInterceptor { - intercept( - req: HttpRequest, - next: HttpHandler - ): Observable> { - const { url, body } = req; - - let storyId: string | undefined; - - if (req.method === 'GET' && req.url.includes('stories')) { - return of(new HttpResponse({ status: 200, body: Object.values(data) })); - } - - switch (req.method) { - case 'GET': - storyId = url.split('/').pop(); - - if (!storyId) { - return throwError(() => new HttpErrorResponse({ status: 400 })); - } - // eslint-disable-next-line no-case-declarations - const obj = data[storyId]; - if (obj) { - return of(new HttpResponse({ status: 200, body: obj })); - } +function fakeBackendInterceptor( + req: HttpRequest, + next: HttpHandlerFn +): Observable> { + const { url, body } = req; - return throwError(() => new HttpErrorResponse({ status: 404 })); + let storyId: string | undefined; + + if (req.method === 'GET' && req.url.includes('stories')) { + return of(new HttpResponse({ status: 200, body: Object.values(data) })); + } - case 'POST': - storyId = Date.now().toString(); - data[storyId] = { - ...body, - storyId, - createdAt: new Date(), - updatedAt: new Date(), - }; + switch (req.method) { + case 'GET': + storyId = url.split('/').pop(); - return of(new HttpResponse({ status: 201, body: data[storyId] })); + if (!storyId) { + return throwError(() => new HttpErrorResponse({ status: 400 })); + } + // eslint-disable-next-line no-case-declarations + const obj = data[storyId]; + if (obj) { + return of(new HttpResponse({ status: 200, body: obj })); + } - case 'PUT': - storyId = url.split('/').pop(); + return throwError(() => new HttpErrorResponse({ status: 404 })); - if (!storyId) { - return throwError(() => new HttpErrorResponse({ status: 400 })); - } + case 'POST': + storyId = Date.now().toString(); + data[storyId] = { + ...body, + storyId, + createdAt: new Date(), + updatedAt: new Date(), + }; - if (!data[storyId]) { - return throwError(() => new HttpErrorResponse({ status: 404 })); - } + return of(new HttpResponse({ status: 201, body: data[storyId] })); - data[storyId] = { - ...data[storyId], - ...body, - updatedAt: new Date(), - }; + case 'PUT': + storyId = url.split('/').pop(); - return of(new HttpResponse({ status: 200, body: data[storyId] })); + if (!storyId) { + return throwError(() => new HttpErrorResponse({ status: 400 })); + } + + if (!data[storyId]) { + return throwError(() => new HttpErrorResponse({ status: 404 })); + } - case 'DELETE': - storyId = url.split('/').pop(); + data[storyId] = { + ...data[storyId], + ...body, + updatedAt: new Date(), + }; - if (!storyId) { - return throwError(() => new HttpErrorResponse({ status: 400 })); - } + return of(new HttpResponse({ status: 200, body: data[storyId] })); - delete data[storyId]; + case 'DELETE': + storyId = url.split('/').pop(); - return of(new HttpResponse({ status: 200, body: storyId })); + if (!storyId) { + return throwError(() => new HttpErrorResponse({ status: 400 })); + } - default: - return throwError(() => new HttpErrorResponse({ status: 501 })); - } + delete data[storyId]; + + return of(new HttpResponse({ status: 200, body: storyId })); + + default: + return throwError(() => new HttpErrorResponse({ status: 501 })); } } + +export function fakeBackendInterceptorFn(): HttpInterceptorFn { + return (req, next) => fakeBackendInterceptor(req, next); +} diff --git a/projects/standalone-app/src/main.ts b/projects/standalone-app/src/main.ts index 23f4540278..7ea166b1b6 100644 --- a/projects/standalone-app/src/main.ts +++ b/projects/standalone-app/src/main.ts @@ -1,4 +1,4 @@ -import { enableProdMode } from '@angular/core'; +import { enableProdMode, importProvidersFrom } from '@angular/core'; import { bootstrapApplication } from '@angular/platform-browser'; import { provideRouter, @@ -16,6 +16,8 @@ import { AppComponent } from './app/app.component'; import { environment } from './environments/environment'; import { AppEffects } from './app/app.effects'; +import { provideHttpClient, withInterceptors } from '@angular/common/http'; +import { fakeBackendInterceptorFn } from './app/board/fake-backend-interceptor.service'; if (environment.production) { enableProdMode(); @@ -34,6 +36,7 @@ export const storyEntityMetadata: EntityMetadata = { bootstrapApplication(AppComponent, { providers: [ + provideHttpClient(withInterceptors([fakeBackendInterceptorFn()])), provideStore({ router: routerReducer }), provideRouter( [ @@ -42,11 +45,11 @@ bootstrapApplication(AppComponent, { loadChildren: () => import('./app/lazy/feature.routes').then((m) => m.routes), }, - { - path: 'board', - loadChildren: () => - import('./app/board/board.routes').then((m) => m.routes), - }, + { + path: 'board', + loadChildren: () => + import('./app/board/board.routes').then((m) => m.routes), + }, ], withEnabledBlockingInitialNavigation() ), @@ -65,5 +68,5 @@ bootstrapApplication(AppComponent, { Story: 'stories', }, }), - ] + ], }); From 21a20f1d4b7ccbfdd641c4f65cb2b8a6d29636e5 Mon Sep 17 00:00:00 2001 From: Santosh Yadav Date: Thu, 3 Nov 2022 21:43:39 +0100 Subject: [PATCH 06/12] feat(data): fix sample app --- .../src/app/board/fake-backend-interceptor.service.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/projects/standalone-app/src/app/board/fake-backend-interceptor.service.ts b/projects/standalone-app/src/app/board/fake-backend-interceptor.service.ts index 6793019749..da79e2ade8 100644 --- a/projects/standalone-app/src/app/board/fake-backend-interceptor.service.ts +++ b/projects/standalone-app/src/app/board/fake-backend-interceptor.service.ts @@ -1,10 +1,7 @@ -import { Injectable } from '@angular/core'; import { HttpErrorResponse, HttpEvent, - HttpHandler, HttpHandlerFn, - HttpInterceptor, HttpInterceptorFn, HttpRequest, HttpResponse, From 416b2ee268f8a46a809ef4ef184ad5e728c79785 Mon Sep 17 00:00:00 2001 From: Santosh Yadav Date: Fri, 4 Nov 2022 21:34:57 +0100 Subject: [PATCH 07/12] feat(data): Introduce Standalone API for NgRx Data --- .../entity-collection-service.spec.ts | 16 +++- .../standalone-app/src/app/app.component.ts | 2 - .../src/app/board/board.component.html | 7 -- .../src/app/board/board.component.scss | 0 .../src/app/board/board.component.ts | 44 --------- .../src/app/board/board.route.ts | 9 -- .../src/app/board/board.routes.ts | 9 -- .../board/fake-backend-interceptor.service.ts | 89 ------------------- .../src/app/board/state/story-data.service.ts | 19 ---- .../src/app/board/state/story.metadata.ts | 13 --- .../src/app/board/state/story.selectors.ts | 17 ---- .../src/app/board/state/story.ts | 18 ---- .../src/app/board/ui/board-ui.component.html | 33 ------- .../src/app/board/ui/board-ui.component.scss | 55 ------------ .../src/app/board/ui/board-ui.component.ts | 52 ----------- projects/standalone-app/src/main.ts | 8 -- 16 files changed, 12 insertions(+), 379 deletions(-) delete mode 100644 projects/standalone-app/src/app/board/board.component.html delete mode 100644 projects/standalone-app/src/app/board/board.component.scss delete mode 100644 projects/standalone-app/src/app/board/board.component.ts delete mode 100644 projects/standalone-app/src/app/board/board.route.ts delete mode 100644 projects/standalone-app/src/app/board/board.routes.ts delete mode 100644 projects/standalone-app/src/app/board/fake-backend-interceptor.service.ts delete mode 100644 projects/standalone-app/src/app/board/state/story-data.service.ts delete mode 100644 projects/standalone-app/src/app/board/state/story.metadata.ts delete mode 100644 projects/standalone-app/src/app/board/state/story.selectors.ts delete mode 100644 projects/standalone-app/src/app/board/state/story.ts delete mode 100644 projects/standalone-app/src/app/board/ui/board-ui.component.html delete mode 100644 projects/standalone-app/src/app/board/ui/board-ui.component.scss delete mode 100644 projects/standalone-app/src/app/board/ui/board-ui.component.ts diff --git a/modules/data/spec/entity-services/entity-collection-service.spec.ts b/modules/data/spec/entity-services/entity-collection-service.spec.ts index 1ff954ead2..4105c4a79f 100644 --- a/modules/data/spec/entity-services/entity-collection-service.spec.ts +++ b/modules/data/spec/entity-services/entity-collection-service.spec.ts @@ -29,7 +29,8 @@ import { Logger, } from '../..'; -describe('EntityCollectionService', () => { +// TODO fix these tests +xdescribe('EntityCollectionService', () => { describe('Command dispatching', () => { // Borrowing the dispatcher tests from entity-dispatcher.spec. // The critical difference: those test didn't invoke the reducers; they do when run here. @@ -133,9 +134,16 @@ describe('EntityCollectionService', () => { const httpError = { error: new Error('Test Failure'), status: 501 }; const error = makeDataServiceError('GET', httpError); dataService.setErrorResponse('getWithQuery', error); - heroCollectionService - .getWithQuery({ name: 'foo' }) - .subscribe(expectErrorToBe(error, done)); + heroCollectionService.getWithQuery({ name: 'foo' }).subscribe( + () => { + console.log('expected error', error); + expectErrorToBe(error, done); + }, + (err) => { + console.log('actual error', err); + expectErrorToBe(error, done); + } + ); }); it('load observable should emit heroes on success', (done: any) => { diff --git a/projects/standalone-app/src/app/app.component.ts b/projects/standalone-app/src/app/app.component.ts index 1e550e444d..15ff68a86f 100644 --- a/projects/standalone-app/src/app/app.component.ts +++ b/projects/standalone-app/src/app/app.component.ts @@ -9,8 +9,6 @@ import { RouterModule } from '@angular/router';

Welcome {{ title }}

Load Feature -
- Boards `, diff --git a/projects/standalone-app/src/app/board/board.component.html b/projects/standalone-app/src/app/board/board.component.html deleted file mode 100644 index c0c2758426..0000000000 --- a/projects/standalone-app/src/app/board/board.component.html +++ /dev/null @@ -1,7 +0,0 @@ - - diff --git a/projects/standalone-app/src/app/board/board.component.scss b/projects/standalone-app/src/app/board/board.component.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/projects/standalone-app/src/app/board/board.component.ts b/projects/standalone-app/src/app/board/board.component.ts deleted file mode 100644 index 4c8de1c1d1..0000000000 --- a/projects/standalone-app/src/app/board/board.component.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { Observable } from 'rxjs'; -import { StoryDataService } from './state/story-data.service'; -import { - CreateStoryDto, - DeleteStoryDto, - Stories, - UpdateStoryDto, -} from './state/story'; -import { CommonModule } from '@angular/common'; -import { BoardUiComponent } from './ui/board-ui.component'; - -@Component({ - selector: 'ngrx-board-component', - templateUrl: './board.component.html', - styleUrls: ['./board.component.scss'], - standalone: true, - imports: [CommonModule, BoardUiComponent], -}) -export class BoardComponent implements OnInit { - stories$: Observable = this.storyDataService.groupedStories$; - - constructor(private storyDataService: StoryDataService) {} - - ngOnInit(): void { - this.storyDataService.getAll(); - } - - add(story: CreateStoryDto): void { - this.storyDataService.add(story, { isOptimistic: false }); - } - - update(story: UpdateStoryDto): void { - this.storyDataService.update(story, { isOptimistic: true }); - } - - loadAll(): void { - this.storyDataService.getAll(); - } - - delete(id: DeleteStoryDto): void { - this.storyDataService.delete(id); - } -} diff --git a/projects/standalone-app/src/app/board/board.route.ts b/projects/standalone-app/src/app/board/board.route.ts deleted file mode 100644 index 44919dd6a9..0000000000 --- a/projects/standalone-app/src/app/board/board.route.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Routes } from '@angular/router'; -import { BoardComponent } from './board.component'; - -export const routes: Routes = [ - { - path: '', - component: BoardComponent, - }, -]; diff --git a/projects/standalone-app/src/app/board/board.routes.ts b/projects/standalone-app/src/app/board/board.routes.ts deleted file mode 100644 index 44919dd6a9..0000000000 --- a/projects/standalone-app/src/app/board/board.routes.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Routes } from '@angular/router'; -import { BoardComponent } from './board.component'; - -export const routes: Routes = [ - { - path: '', - component: BoardComponent, - }, -]; diff --git a/projects/standalone-app/src/app/board/fake-backend-interceptor.service.ts b/projects/standalone-app/src/app/board/fake-backend-interceptor.service.ts deleted file mode 100644 index da79e2ade8..0000000000 --- a/projects/standalone-app/src/app/board/fake-backend-interceptor.service.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { - HttpErrorResponse, - HttpEvent, - HttpHandlerFn, - HttpInterceptorFn, - HttpRequest, - HttpResponse, -} from '@angular/common/http'; -import { Observable, of, throwError } from 'rxjs'; -import { Story } from './state/story'; - -const data: Record = {}; - -function fakeBackendInterceptor( - req: HttpRequest, - next: HttpHandlerFn -): Observable> { - const { url, body } = req; - - let storyId: string | undefined; - - if (req.method === 'GET' && req.url.includes('stories')) { - return of(new HttpResponse({ status: 200, body: Object.values(data) })); - } - - switch (req.method) { - case 'GET': - storyId = url.split('/').pop(); - - if (!storyId) { - return throwError(() => new HttpErrorResponse({ status: 400 })); - } - // eslint-disable-next-line no-case-declarations - const obj = data[storyId]; - if (obj) { - return of(new HttpResponse({ status: 200, body: obj })); - } - - return throwError(() => new HttpErrorResponse({ status: 404 })); - - case 'POST': - storyId = Date.now().toString(); - data[storyId] = { - ...body, - storyId, - createdAt: new Date(), - updatedAt: new Date(), - }; - - return of(new HttpResponse({ status: 201, body: data[storyId] })); - - case 'PUT': - storyId = url.split('/').pop(); - - if (!storyId) { - return throwError(() => new HttpErrorResponse({ status: 400 })); - } - - if (!data[storyId]) { - return throwError(() => new HttpErrorResponse({ status: 404 })); - } - - data[storyId] = { - ...data[storyId], - ...body, - updatedAt: new Date(), - }; - - return of(new HttpResponse({ status: 200, body: data[storyId] })); - - case 'DELETE': - storyId = url.split('/').pop(); - - if (!storyId) { - return throwError(() => new HttpErrorResponse({ status: 400 })); - } - - delete data[storyId]; - - return of(new HttpResponse({ status: 200, body: storyId })); - - default: - return throwError(() => new HttpErrorResponse({ status: 501 })); - } -} - -export function fakeBackendInterceptorFn(): HttpInterceptorFn { - return (req, next) => fakeBackendInterceptor(req, next); -} diff --git a/projects/standalone-app/src/app/board/state/story-data.service.ts b/projects/standalone-app/src/app/board/state/story-data.service.ts deleted file mode 100644 index b73a1f0a01..0000000000 --- a/projects/standalone-app/src/app/board/state/story-data.service.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { - EntityCollectionServiceBase, - EntityCollectionServiceElementsFactory, -} from '@ngrx/data'; -import { select } from '@ngrx/store'; -import { Injectable } from '@angular/core'; -import { selectStories } from './story.selectors'; -import { Story } from './story'; - -@Injectable({ - providedIn: 'root', -}) -export class StoryDataService extends EntityCollectionServiceBase { - groupedStories$ = this.entities$.pipe(select(selectStories)); - - constructor(serviceElementsFactory: EntityCollectionServiceElementsFactory) { - super('Story', serviceElementsFactory); - } -} diff --git a/projects/standalone-app/src/app/board/state/story.metadata.ts b/projects/standalone-app/src/app/board/state/story.metadata.ts deleted file mode 100644 index 8969f79214..0000000000 --- a/projects/standalone-app/src/app/board/state/story.metadata.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { EntityMetadata } from '@ngrx/data'; -import { Story } from './story'; - -export const storyEntityMetadata: EntityMetadata = { - entityName: 'Story', - selectId: (entity: Story): string => entity.storyId, - sortComparer: (a, b) => a.createdAt.getTime() - b.createdAt.getTime(), - filterFn: (entities, pattern) => - entities.filter( - (entity) => - entity.title?.includes(pattern) || entity.title?.includes(pattern) - ), -}; diff --git a/projects/standalone-app/src/app/board/state/story.selectors.ts b/projects/standalone-app/src/app/board/state/story.selectors.ts deleted file mode 100644 index 407a1587ba..0000000000 --- a/projects/standalone-app/src/app/board/state/story.selectors.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { createSelector } from '@ngrx/store'; -import { Stories, Story } from './story'; - -export const selectStories = createSelector( - (stories) => stories, - (stories: Stories) => - stories.reduce( - (prev, cur) => { - prev[cur.column].push(cur); - - prev[cur.column].sort((a, b) => a.order - b.order); - - return prev; - }, - [[], [], [], []] - ) -); diff --git a/projects/standalone-app/src/app/board/state/story.ts b/projects/standalone-app/src/app/board/state/story.ts deleted file mode 100644 index 502e51e584..0000000000 --- a/projects/standalone-app/src/app/board/state/story.ts +++ /dev/null @@ -1,18 +0,0 @@ -export interface Story { - storyId: string; - order: number; - column: number; - title: string; - description: string; - createdAt: Date; - updatedAt: Date; -} - -export type Stories = Story[]; - -export type CreateStoryDto = Partial; - -export type UpdateStoryDto = Required> & - Partial>; - -export type DeleteStoryDto = string; diff --git a/projects/standalone-app/src/app/board/ui/board-ui.component.html b/projects/standalone-app/src/app/board/ui/board-ui.component.html deleted file mode 100644 index 04d7dc2a9a..0000000000 --- a/projects/standalone-app/src/app/board/ui/board-ui.component.html +++ /dev/null @@ -1,33 +0,0 @@ -
-
-

Column {{ i }}

- - - -
-
- - - - -
-
-
-
diff --git a/projects/standalone-app/src/app/board/ui/board-ui.component.scss b/projects/standalone-app/src/app/board/ui/board-ui.component.scss deleted file mode 100644 index 2de4cc03ab..0000000000 --- a/projects/standalone-app/src/app/board/ui/board-ui.component.scss +++ /dev/null @@ -1,55 +0,0 @@ -.list-container { - width: 400px; - max-width: 100%; - margin: 0 25px 25px 0; - display: inline-block; - vertical-align: top; - border: black; -} - -.drag-list { - padding: 20px; - border: solid 1px #ccc; - min-height: 60px; - background: white; - border-radius: 4px; - overflow: hidden; - display: block; -} - -.drag-box { - padding: 20px 10px; - border: solid 1px #ccc; - color: rgba(0, 0, 0, 0.87); - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - box-sizing: border-box; - cursor: move; - background: white; - font-size: 14px; -} - -.cdk-drag-preview { - box-sizing: border-box; - border-radius: 4px; - box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), - 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12); -} - -.cdk-drag-placeholder { - opacity: 0; -} - -.cdk-drag-animating { - transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); -} - -.drag-box:last-child { - border: none; -} - -.drag.cdk-drop-list-dragging .drag-box:not(.cdk-drag-placeholder) { - transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); -} diff --git a/projects/standalone-app/src/app/board/ui/board-ui.component.ts b/projects/standalone-app/src/app/board/ui/board-ui.component.ts deleted file mode 100644 index b02bcb2cb5..0000000000 --- a/projects/standalone-app/src/app/board/ui/board-ui.component.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { CdkDragDrop, DragDropModule } from '@angular/cdk/drag-drop'; -import { CommonModule } from '@angular/common'; -import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { - CreateStoryDto, - DeleteStoryDto, - Stories, - Story, - UpdateStoryDto, -} from '../state/story'; - -@Component({ - selector: 'ngrx-board-ui', - templateUrl: './board-ui.component.html', - styleUrls: ['./board-ui.component.scss'], - standalone: true, - imports: [CommonModule, DragDropModule, FormsModule], -}) -export class BoardUiComponent { - @Input() stories: Stories[] = []; - - @Output() add = new EventEmitter(); - - @Output() delete = new EventEmitter(); - - @Output() update = new EventEmitter(); - - addNew(column: number, stories: Stories): void { - this.add.emit({ - order: stories.length, - column, - title: `Order ${stories.length} Column ${column}`, - description: '', - }); - } - - dropStory(event: CdkDragDrop, column: number): void { - this.update.emit({ - column, - order: event.currentIndex, - storyId: event.item.data.storyId, - }); - } - - updateStory(story: Story, title: string): void { - this.update.emit({ - storyId: story.storyId, - title, - }); - } -} diff --git a/projects/standalone-app/src/main.ts b/projects/standalone-app/src/main.ts index 7ea166b1b6..fe5ea94838 100644 --- a/projects/standalone-app/src/main.ts +++ b/projects/standalone-app/src/main.ts @@ -16,8 +16,6 @@ import { AppComponent } from './app/app.component'; import { environment } from './environments/environment'; import { AppEffects } from './app/app.effects'; -import { provideHttpClient, withInterceptors } from '@angular/common/http'; -import { fakeBackendInterceptorFn } from './app/board/fake-backend-interceptor.service'; if (environment.production) { enableProdMode(); @@ -36,7 +34,6 @@ export const storyEntityMetadata: EntityMetadata = { bootstrapApplication(AppComponent, { providers: [ - provideHttpClient(withInterceptors([fakeBackendInterceptorFn()])), provideStore({ router: routerReducer }), provideRouter( [ @@ -45,11 +42,6 @@ bootstrapApplication(AppComponent, { loadChildren: () => import('./app/lazy/feature.routes').then((m) => m.routes), }, - { - path: 'board', - loadChildren: () => - import('./app/board/board.routes').then((m) => m.routes), - }, ], withEnabledBlockingInitialNavigation() ), From 53ad789fa1fe065518a7ccad332f3a38dc6240f0 Mon Sep 17 00:00:00 2001 From: Santosh Yadav Date: Fri, 4 Nov 2022 21:45:51 +0100 Subject: [PATCH 08/12] feat(data): Introduce Standalone API for NgRx Data --- projects/standalone-app/src/main.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/projects/standalone-app/src/main.ts b/projects/standalone-app/src/main.ts index fe5ea94838..ce47046a83 100644 --- a/projects/standalone-app/src/main.ts +++ b/projects/standalone-app/src/main.ts @@ -1,4 +1,4 @@ -import { enableProdMode, importProvidersFrom } from '@angular/core'; +import { enableProdMode } from '@angular/core'; import { bootstrapApplication } from '@angular/platform-browser'; import { provideRouter, @@ -8,7 +8,6 @@ import { provideStore } from '@ngrx/store'; import { provideEffects } from '@ngrx/effects'; import { provideRouterStore, routerReducer } from '@ngrx/router-store'; import { provideStoreDevtools } from '@ngrx/store-devtools'; -import { provideEntityData } from '@ngrx/data'; import { EntityMetadata } from '@ngrx/data'; import { Story } from './app/story'; @@ -52,13 +51,5 @@ bootstrapApplication(AppComponent, { }), provideRouterStore(), provideEffects(AppEffects), - provideEntityData({ - entityMetadata: { - Story: storyEntityMetadata, - }, - pluralNames: { - Story: 'stories', - }, - }), ], }); From 9037a07235543905b649912061acb52477a87a24 Mon Sep 17 00:00:00 2001 From: Santosh Yadav Date: Fri, 4 Nov 2022 22:10:04 +0100 Subject: [PATCH 09/12] feat(data): Introduce Standalone API for NgRx Data --- modules/data/spec/entity-data.module.spec.ts | 2 +- modules/data/spec/entity-services/entity-services.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/data/spec/entity-data.module.spec.ts b/modules/data/spec/entity-data.module.spec.ts index 873f8856fb..66a84e696d 100644 --- a/modules/data/spec/entity-data.module.spec.ts +++ b/modules/data/spec/entity-data.module.spec.ts @@ -72,7 +72,7 @@ const entityMetadata = { //////// Tests begin //////// -describe('EntityDataModule', () => { +xdescribe('EntityDataModule', () => { describe('with replaced EntityEffects', () => { // factory never changes in these tests const entityActionFactory = new EntityActionFactory(); diff --git a/modules/data/spec/entity-services/entity-services.spec.ts b/modules/data/spec/entity-services/entity-services.spec.ts index 251826cc71..d2ba94074b 100644 --- a/modules/data/spec/entity-services/entity-services.spec.ts +++ b/modules/data/spec/entity-services/entity-services.spec.ts @@ -22,7 +22,7 @@ import { Logger, } from '../..'; -describe('EntityServices', () => { +xdescribe('EntityServices', () => { describe('entityActionErrors$', () => { it('should emit EntityAction errors for multiple entity types', () => { const errors: EntityAction[] = []; From 6f6413ecc64e7fa85602200a9f2a4a6a07b5e172 Mon Sep 17 00:00:00 2001 From: Santosh Yadav Date: Fri, 4 Nov 2022 22:14:19 +0100 Subject: [PATCH 10/12] feat(data): Introduce Standalone API for NgRx Data --- modules/data/src/entity-data.module.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/modules/data/src/entity-data.module.ts b/modules/data/src/entity-data.module.ts index fd040ac3a7..692d76adca 100644 --- a/modules/data/src/entity-data.module.ts +++ b/modules/data/src/entity-data.module.ts @@ -1,14 +1,9 @@ import { ModuleWithProviders, NgModule } from '@angular/core'; -import { EffectsModule, EffectSources } from '@ngrx/effects'; - -import { EntityCacheEffects } from './effects/entity-cache-effects'; -import { EntityEffects } from './effects/entity-effects'; - import { EntityDataModuleConfig } from './entity-data-config'; +import { EntityDataModuleWithoutEffects } from './entity-data-without-effects.module'; import { ENTITY_DATA_PROVIDERS, - ENTITY_DATA_WITHOUT_EFFECTS_PROVIDERS, initializeEntityData, provideRootEntityData, provideRootEntityDataWithoutEffects, @@ -20,7 +15,8 @@ import { * No `forFeature` yet. */ @NgModule({ - providers: [ENTITY_DATA_WITHOUT_EFFECTS_PROVIDERS, ENTITY_DATA_PROVIDERS], + imports: [EntityDataModuleWithoutEffects], + providers: [ENTITY_DATA_PROVIDERS], }) export class EntityDataModule { static forRoot( From 9ab57c6dd1a5b1016b14892c5522f7c137a14889 Mon Sep 17 00:00:00 2001 From: Santosh Yadav Date: Fri, 4 Nov 2022 22:24:23 +0100 Subject: [PATCH 11/12] feat(data): Introduce Standalone API for NgRx Data --- modules/data/spec/entity-data.module.spec.ts | 2 +- .../entity-collection-service.spec.ts | 2 +- .../entity-services/entity-services.spec.ts | 2 +- projects/standalone-app/src/app/story.ts | 18 ------------------ projects/standalone-app/src/main.ts | 13 ------------- 5 files changed, 3 insertions(+), 34 deletions(-) delete mode 100644 projects/standalone-app/src/app/story.ts diff --git a/modules/data/spec/entity-data.module.spec.ts b/modules/data/spec/entity-data.module.spec.ts index 66a84e696d..873f8856fb 100644 --- a/modules/data/spec/entity-data.module.spec.ts +++ b/modules/data/spec/entity-data.module.spec.ts @@ -72,7 +72,7 @@ const entityMetadata = { //////// Tests begin //////// -xdescribe('EntityDataModule', () => { +describe('EntityDataModule', () => { describe('with replaced EntityEffects', () => { // factory never changes in these tests const entityActionFactory = new EntityActionFactory(); diff --git a/modules/data/spec/entity-services/entity-collection-service.spec.ts b/modules/data/spec/entity-services/entity-collection-service.spec.ts index 4105c4a79f..b573580521 100644 --- a/modules/data/spec/entity-services/entity-collection-service.spec.ts +++ b/modules/data/spec/entity-services/entity-collection-service.spec.ts @@ -30,7 +30,7 @@ import { } from '../..'; // TODO fix these tests -xdescribe('EntityCollectionService', () => { +describe('EntityCollectionService', () => { describe('Command dispatching', () => { // Borrowing the dispatcher tests from entity-dispatcher.spec. // The critical difference: those test didn't invoke the reducers; they do when run here. diff --git a/modules/data/spec/entity-services/entity-services.spec.ts b/modules/data/spec/entity-services/entity-services.spec.ts index d2ba94074b..251826cc71 100644 --- a/modules/data/spec/entity-services/entity-services.spec.ts +++ b/modules/data/spec/entity-services/entity-services.spec.ts @@ -22,7 +22,7 @@ import { Logger, } from '../..'; -xdescribe('EntityServices', () => { +describe('EntityServices', () => { describe('entityActionErrors$', () => { it('should emit EntityAction errors for multiple entity types', () => { const errors: EntityAction[] = []; diff --git a/projects/standalone-app/src/app/story.ts b/projects/standalone-app/src/app/story.ts deleted file mode 100644 index 502e51e584..0000000000 --- a/projects/standalone-app/src/app/story.ts +++ /dev/null @@ -1,18 +0,0 @@ -export interface Story { - storyId: string; - order: number; - column: number; - title: string; - description: string; - createdAt: Date; - updatedAt: Date; -} - -export type Stories = Story[]; - -export type CreateStoryDto = Partial; - -export type UpdateStoryDto = Required> & - Partial>; - -export type DeleteStoryDto = string; diff --git a/projects/standalone-app/src/main.ts b/projects/standalone-app/src/main.ts index ce47046a83..68865919a6 100644 --- a/projects/standalone-app/src/main.ts +++ b/projects/standalone-app/src/main.ts @@ -8,8 +8,6 @@ import { provideStore } from '@ngrx/store'; import { provideEffects } from '@ngrx/effects'; import { provideRouterStore, routerReducer } from '@ngrx/router-store'; import { provideStoreDevtools } from '@ngrx/store-devtools'; -import { EntityMetadata } from '@ngrx/data'; -import { Story } from './app/story'; import { AppComponent } from './app/app.component'; @@ -20,17 +18,6 @@ if (environment.production) { enableProdMode(); } -export const storyEntityMetadata: EntityMetadata = { - entityName: 'Story', - selectId: (entity: Story): string => entity.storyId, - sortComparer: (a, b) => a.createdAt.getTime() - b.createdAt.getTime(), - filterFn: (entities, pattern) => - entities.filter( - (entity) => - entity.title?.includes(pattern) || entity.title?.includes(pattern) - ), -}; - bootstrapApplication(AppComponent, { providers: [ provideStore({ router: routerReducer }), From 6076e41c5b9d238e931b256e47c033db1a546b99 Mon Sep 17 00:00:00 2001 From: Santosh Yadav Date: Fri, 4 Nov 2022 22:52:57 +0100 Subject: [PATCH 12/12] feat(data): Introduce Standalone API for NgRx Data --- .../entity-collection-service.spec.ts | 14 +++----------- projects/standalone-app-e2e/src/support/app.po.ts | 2 +- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/modules/data/spec/entity-services/entity-collection-service.spec.ts b/modules/data/spec/entity-services/entity-collection-service.spec.ts index b573580521..1ff954ead2 100644 --- a/modules/data/spec/entity-services/entity-collection-service.spec.ts +++ b/modules/data/spec/entity-services/entity-collection-service.spec.ts @@ -29,7 +29,6 @@ import { Logger, } from '../..'; -// TODO fix these tests describe('EntityCollectionService', () => { describe('Command dispatching', () => { // Borrowing the dispatcher tests from entity-dispatcher.spec. @@ -134,16 +133,9 @@ describe('EntityCollectionService', () => { const httpError = { error: new Error('Test Failure'), status: 501 }; const error = makeDataServiceError('GET', httpError); dataService.setErrorResponse('getWithQuery', error); - heroCollectionService.getWithQuery({ name: 'foo' }).subscribe( - () => { - console.log('expected error', error); - expectErrorToBe(error, done); - }, - (err) => { - console.log('actual error', err); - expectErrorToBe(error, done); - } - ); + heroCollectionService + .getWithQuery({ name: 'foo' }) + .subscribe(expectErrorToBe(error, done)); }); it('load observable should emit heroes on success', (done: any) => { diff --git a/projects/standalone-app-e2e/src/support/app.po.ts b/projects/standalone-app-e2e/src/support/app.po.ts index 3038b9c569..19f735f532 100644 --- a/projects/standalone-app-e2e/src/support/app.po.ts +++ b/projects/standalone-app-e2e/src/support/app.po.ts @@ -1,2 +1,2 @@ export const getGreeting = () => cy.get('h1'); -export const loadFeature = () => cy.get('a').contains('Load Feature').click(); +export const loadFeature = () => cy.get('a').click();