diff --git a/modules/schematics/src/entity/schema.json b/modules/schematics/src/entity/schema.json index 6e840060c0..5ceb701617 100644 --- a/modules/schematics/src/entity/schema.json +++ b/modules/schematics/src/entity/schema.json @@ -52,6 +52,12 @@ "default": false, "description": "Group actions, reducers and effects within relative subfolders", "aliases": ["g"] + }, + "feature": { + "type": "boolean", + "default": false, + "description": "Flag to indicate if part of a feature schematic.", + "visible": false } }, "required": [] diff --git a/modules/schematics/src/entity/schema.ts b/modules/schematics/src/entity/schema.ts index 1d5db1236f..5f469eb7c6 100644 --- a/modules/schematics/src/entity/schema.ts +++ b/modules/schematics/src/entity/schema.ts @@ -36,4 +36,9 @@ export interface Schema { */ group?: boolean; + + /** + * Specifies if this is grouped within a feature + */ + feature?: boolean; } diff --git a/modules/schematics/src/feature/__snapshots__/index.spec.ts.snap b/modules/schematics/src/feature/__snapshots__/index.spec.ts.snap index 4f9eb3deee..4e9ee90d4c 100644 --- a/modules/schematics/src/feature/__snapshots__/index.spec.ts.snap +++ b/modules/schematics/src/feature/__snapshots__/index.spec.ts.snap @@ -1,5 +1,177 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Feature Schematic should create all files of a feature with an entity 1`] = ` +"import { createActionGroup, emptyProps, props } from '@ngrx/store'; +import { Update } from '@ngrx/entity'; + +import { Foo } from './foo.model'; + +export const FooActions = createActionGroup({ + source: 'Foo/API', + events: { + 'Load Foos': props<{ foos: Foo[] }>(), + 'Add Foo': props<{ foo: Foo }>(), + 'Upsert Foo': props<{ foo: Foo }>(), + 'Add Foos': props<{ foo: Foo[] }>(), + 'Upsert Foos': props<{ foo: Foo[] }>(), + 'Update Foo': props<{ foo: Update }>(), + 'Update Foos': props<{ foos: Update[] }>(), + 'Delete Foo': props<{ id: string }>(), + 'Delete Foos': props<{ ids: string[] }>(), + 'Clear Foos': emptyProps(), + } +}); +" +`; + +exports[`Feature Schematic should create all files of a feature with an entity 2`] = ` +"import { createFeature, createReducer, on } from '@ngrx/store'; +import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity'; +import { Foo } from './foo.model'; +import { FooActions } from './foo.actions'; + +export const foosFeatureKey = 'foos'; + +export interface State extends EntityState { + // additional entities state properties +} + +export const adapter: EntityAdapter = createEntityAdapter(); + +export const initialState: State = adapter.getInitialState({ + // additional entity state properties +}); + +export const reducer = createReducer( + initialState, + on(FooActions.addFoo, + (state, action) => adapter.addOne(action.foo, state) + ), + on(FooActions.upsertFoo, + (state, action) => adapter.upsertOne(action.foo, state) + ), + on(FooActions.addFoos, + (state, action) => adapter.addMany(action.foos, state) + ), + on(FooActions.upsertFoos, + (state, action) => adapter.upsertMany(action.foos, state) + ), + on(FooActions.updateFoo, + (state, action) => adapter.updateOne(action.foo, state) + ), + on(FooActions.updateFoos, + (state, action) => adapter.updateMany(action.foos, state) + ), + on(FooActions.deleteFoo, + (state, action) => adapter.removeOne(action.id, state) + ), + on(FooActions.deleteFoos, + (state, action) => adapter.removeMany(action.ids, state) + ), + on(FooActions.loadFoos, + (state, action) => adapter.setAll(action.foos, state) + ), + on(FooActions.clearFoos, + state => adapter.removeAll(state) + ), +); + +export const foosFeature = createFeature({ + name: foosFeatureKey, + reducer, + extraSelectors: ({ selectFoosState }) => ({ + ...adapter.getSelectors(selectFoosState) + }), +}); + +export const { + selectIds, + selectEntities, + selectAll, + selectTotal, +} = foosFeature; +" +`; + +exports[`Feature Schematic should create all files of a feature with an entity 3`] = ` +"import { reducer, initialState } from './foo.reducer'; + +describe('Foo Reducer', () => { + describe('unknown action', () => { + it('should return the previous state', () => { + const action = {} as any; + + const result = reducer(initialState, action); + + expect(result).toBe(initialState); + }); + }); +}); +" +`; + +exports[`Feature Schematic should create all files of a feature with an entity 4`] = ` +"import { Injectable } from '@angular/core'; +import { Actions, createEffect, ofType } from '@ngrx/effects'; + +import { concatMap } from 'rxjs/operators'; +import { Observable, EMPTY } from 'rxjs'; +import { FooActions } from './foo.actions'; + +@Injectable() +export class FooEffects { + + + loadFoos$ = createEffect(() => { + return this.actions$.pipe( + + ofType(FooActions.loadFoos), + /** An EMPTY observable only emits completion. Replace with your own observable API request */ + concatMap(() => EMPTY as Observable<{ type: string }>) + ); + }); + + constructor(private actions$: Actions) {} +} +" +`; + +exports[`Feature Schematic should create all files of a feature with an entity 5`] = ` +"import { TestBed } from '@angular/core/testing'; +import { provideMockActions } from '@ngrx/effects/testing'; +import { Observable } from 'rxjs'; + +import { FooEffects } from './foo.effects'; + +describe('FooEffects', () => { + let actions$: Observable; + let effects: FooEffects; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + FooEffects, + provideMockActions(() => actions$) + ] + }); + + effects = TestBed.inject(FooEffects); + }); + + it('should be created', () => { + expect(effects).toBeTruthy(); + }); +}); +" +`; + +exports[`Feature Schematic should create all files of a feature with an entity 6`] = ` +"export interface Foo { + id: string; +} +" +`; + exports[`Feature Schematic should have all api actions in reducer if api flag enabled 1`] = ` "import { createFeature, createReducer, on } from '@ngrx/store'; import { FooActions } from './foo.actions'; diff --git a/modules/schematics/src/feature/index.spec.ts b/modules/schematics/src/feature/index.spec.ts index 0c2d88becd..7fb1349c8d 100644 --- a/modules/schematics/src/feature/index.spec.ts +++ b/modules/schematics/src/feature/index.spec.ts @@ -21,6 +21,7 @@ describe('Feature Schematic', () => { project: 'bar', module: '', group: false, + entity: false, }; const projectPath = getTestProjectPath(); @@ -64,6 +65,7 @@ describe('Feature Schematic', () => { expect( files.includes(`${projectPath}/src/app/foo.selectors.spec.ts`) ).toBeTruthy(); + expect(files.includes(`${projectPath}/src/app/foo.model.ts`)).toBeFalsy(); }); it('should not create test files when skipTests is true', async () => { @@ -269,4 +271,27 @@ describe('Feature Schematic', () => { expect(fileContent).toMatchSnapshot(); }); + + it('should create all files of a feature with an entity', async () => { + const options = { ...defaultOptions, entity: true }; + + const tree = await schematicRunner.runSchematic( + 'feature', + options, + appTree + ); + const paths = [ + `${projectPath}/src/app/foo.actions.ts`, + `${projectPath}/src/app/foo.reducer.ts`, + `${projectPath}/src/app/foo.reducer.spec.ts`, + `${projectPath}/src/app/foo.effects.ts`, + `${projectPath}/src/app/foo.effects.spec.ts`, + `${projectPath}/src/app/foo.model.ts`, + ]; + + paths.forEach((path) => { + expect(tree.files.includes(path)).toBeTruthy(); + expect(tree.readContent(path)).toMatchSnapshot(); + }); + }); }); diff --git a/modules/schematics/src/feature/index.ts b/modules/schematics/src/feature/index.ts index 71b3e6416a..5446d66fa7 100644 --- a/modules/schematics/src/feature/index.ts +++ b/modules/schematics/src/feature/index.ts @@ -1,59 +1,78 @@ import { + chain, Rule, + schematic, SchematicContext, Tree, - chain, - schematic, } from '@angular-devkit/schematics'; + import { Schema as FeatureOptions } from './schema'; export default function (options: FeatureOptions): Rule { return (host: Tree, context: SchematicContext) => { - return chain([ - schematic('action', { - flat: options.flat, - group: options.group, - name: options.name, - path: options.path, - project: options.project, - skipTests: options.skipTests, - api: options.api, - prefix: options.prefix, - }), - schematic('reducer', { - flat: options.flat, - group: options.group, - module: options.module, - name: options.name, - path: options.path, - project: options.project, - skipTests: options.skipTests, - reducers: options.reducers, - feature: true, - api: options.api, - prefix: options.prefix, - }), - schematic('effect', { - flat: options.flat, - group: options.group, - module: options.module, - name: options.name, - path: options.path, - project: options.project, - skipTests: options.skipTests, - feature: true, - api: options.api, - prefix: options.prefix, - }), - schematic('selector', { - flat: options.flat, - group: options.group, - name: options.name, - path: options.path, - project: options.project, - skipTests: options.skipTests, - feature: true, - }), - ])(host, context); + return chain( + (options.entity + ? [ + schematic('entity', { + name: options.name, + path: options.path, + project: options.project, + flat: options.flat, + skipTests: options.skipTests, + module: options.module, + reducers: options.reducers, + group: options.group, + feature: true, + }), + ] + : [ + schematic('action', { + flat: options.flat, + group: options.group, + name: options.name, + path: options.path, + project: options.project, + skipTests: options.skipTests, + api: options.api, + prefix: options.prefix, + }), + schematic('reducer', { + flat: options.flat, + group: options.group, + module: options.module, + name: options.name, + path: options.path, + project: options.project, + skipTests: options.skipTests, + reducers: options.reducers, + feature: true, + api: options.api, + prefix: options.prefix, + }), + schematic('selector', { + flat: options.flat, + group: options.group, + name: options.name, + path: options.path, + project: options.project, + skipTests: options.skipTests, + feature: true, + }), + ] + ).concat([ + schematic('effect', { + flat: options.flat, + group: options.group, + module: options.module, + name: options.name, + path: options.path, + project: options.project, + skipTests: options.skipTests, + feature: true, + api: options.api, + prefix: options.prefix, + }), + ]) + )(host, context); }; } diff --git a/modules/schematics/src/feature/schema.json b/modules/schematics/src/feature/schema.json index ec0db76dbc..a641b47a9e 100644 --- a/modules/schematics/src/feature/schema.json +++ b/modules/schematics/src/feature/schema.json @@ -65,6 +65,13 @@ "type": "string", "default": "load", "x-prompt": "What should be the prefix of the action, effect and reducer?" + }, + "entity": { + "description": "Toggle whether an entity is created as part of the feature", + "type": "boolean", + "aliases": ["e"], + "x-prompt": "Should we use @ngrx/entity to create the reducer?", + "default": "false" } }, "required": [] diff --git a/modules/schematics/src/feature/schema.ts b/modules/schematics/src/feature/schema.ts index a5413b7555..032bd7e46a 100644 --- a/modules/schematics/src/feature/schema.ts +++ b/modules/schematics/src/feature/schema.ts @@ -46,4 +46,6 @@ export interface Schema { api?: boolean; prefix?: string; + + entity?: boolean; }