From 136c5eb1e7ba45adc7d8c105e21a45fed7546ab6 Mon Sep 17 00:00:00 2001 From: John Crowson Date: Wed, 17 Jul 2019 21:27:28 -0400 Subject: [PATCH] Schematics: Add and import feature key for reducer state (#2017) Closes #1996 --- modules/data/schematics-core/index.ts | 2 ++ .../schematics-core/utility/ngrx-utils.ts | 24 ++++++++++++++----- modules/effects/schematics-core/index.ts | 2 ++ .../schematics-core/utility/ngrx-utils.ts | 24 ++++++++++++++----- modules/entity/schematics-core/index.ts | 2 ++ .../schematics-core/utility/ngrx-utils.ts | 24 ++++++++++++++----- modules/router-store/schematics-core/index.ts | 2 ++ .../schematics-core/utility/ngrx-utils.ts | 24 ++++++++++++++----- modules/schematics-core/index.ts | 2 ++ modules/schematics-core/utility/ngrx-utils.ts | 24 ++++++++++++++----- modules/schematics/schematics-core/index.ts | 2 ++ .../schematics-core/utility/ngrx-utils.ts | 24 ++++++++++++++----- ...erize@group-reducers__.reducer.ts.template | 2 ++ ...erize@group-reducers__.reducer.ts.template | 2 ++ modules/schematics/src/entity/index.spec.ts | 17 +++++++++++-- modules/schematics/src/entity/index.ts | 2 +- .../__name@dasherize__.reducer.ts.template | 2 ++ .../__name@dasherize__.reducer.ts.template | 2 ++ modules/schematics/src/reducer/index.spec.ts | 17 +++++++++++-- .../files/__statePath__/index.ts.template | 2 ++ modules/schematics/src/store/index.spec.ts | 24 +++++++++++++++++-- modules/schematics/src/store/index.ts | 6 +++-- .../store-devtools/schematics-core/index.ts | 2 ++ .../schematics-core/utility/ngrx-utils.ts | 24 ++++++++++++++----- modules/store/schematics-core/index.ts | 2 ++ .../schematics-core/utility/ngrx-utils.ts | 24 ++++++++++++++----- .../example-app/src/app/auth/auth.module.ts | 4 ++-- .../src/app/auth/reducers/auth.reducer.ts | 2 ++ .../src/app/auth/reducers/index.ts | 16 ++++++++----- .../app/auth/reducers/login-page.reducer.ts | 2 ++ .../example-app/src/app/books/books.module.ts | 4 ++-- .../src/app/books/reducers/books.reducer.ts | 2 ++ .../app/books/reducers/collection.reducer.ts | 2 ++ .../src/app/books/reducers/index.ts | 20 +++++++++------- .../src/app/books/reducers/search.reducer.ts | 2 ++ .../src/app/core/reducers/layout.reducer.ts | 2 ++ .../example-app/src/app/reducers/index.ts | 4 ++-- .../content/guide/store/recipes/injecting.md | 4 ++-- .../ngrx.io/content/guide/store/reducers.md | 12 +++++++++- .../ngrx.io/content/guide/store/selectors.md | 8 ++++--- 40 files changed, 285 insertions(+), 83 deletions(-) diff --git a/modules/data/schematics-core/index.ts b/modules/data/schematics-core/index.ts index a696576992..1252f4fb06 100644 --- a/modules/data/schematics-core/index.ts +++ b/modules/data/schematics-core/index.ts @@ -7,6 +7,7 @@ import { group, capitalize, featurePath, + pluralize, } from './utility/strings'; export { @@ -64,6 +65,7 @@ export const stringUtils = { group, capitalize, featurePath, + pluralize, }; export { updatePackage } from './utility/update'; diff --git a/modules/data/schematics-core/utility/ngrx-utils.ts b/modules/data/schematics-core/utility/ngrx-utils.ts index d856f79346..182e5d88c6 100644 --- a/modules/data/schematics-core/utility/ngrx-utils.ts +++ b/modules/data/schematics-core/utility/ngrx-utils.ts @@ -93,8 +93,11 @@ export function addReducerToStateInterface( ? stringUtils.pluralize(options.name) : stringUtils.camelize(options.name); - const keyInsert = - state + ': from' + stringUtils.classify(options.name) + '.State;'; + const keyInsert = `[from${stringUtils.classify( + options.name + )}.${stringUtils.camelize(state)}FeatureKey]: from${stringUtils.classify( + options.name + )}.State;`; const expr = node as any; let position; let toInsert; @@ -144,6 +147,7 @@ export function addReducerToActionReducerMap( return { initializer: variable.initializer, type }; }) + .filter(initWithType => initWithType.type !== undefined) .find(({ type }) => type.typeName.text === 'ActionReducerMap'); if (!actionReducerMap || !actionReducerMap.initializer) { @@ -156,8 +160,11 @@ export function addReducerToActionReducerMap( ? stringUtils.pluralize(options.name) : stringUtils.camelize(options.name); - const keyInsert = - state + ': from' + stringUtils.classify(options.name) + '.reducer,'; + const keyInsert = `[from${stringUtils.classify( + options.name + )}.${stringUtils.camelize(state)}FeatureKey]: from${stringUtils.classify( + options.name + )}.reducer,`; const expr = node as any; let position; let toInsert; @@ -227,12 +234,17 @@ export function addReducerImportToNgModule(options: any): Rule { relativePath, true ); + const state = options.plural + ? stringUtils.pluralize(options.name) + : stringUtils.camelize(options.name); const [storeNgModuleImport] = addImportToModule( source, modulePath, - `StoreModule.forFeature('${stringUtils.camelize( + `StoreModule.forFeature(from${stringUtils.classify( options.name - )}', from${stringUtils.classify(options.name)}.reducer)`, + )}.${state}FeatureKey, from${stringUtils.classify( + options.name + )}.reducer)`, relativePath ); const changes = [...commonImports, reducerImport, storeNgModuleImport]; diff --git a/modules/effects/schematics-core/index.ts b/modules/effects/schematics-core/index.ts index a696576992..1252f4fb06 100644 --- a/modules/effects/schematics-core/index.ts +++ b/modules/effects/schematics-core/index.ts @@ -7,6 +7,7 @@ import { group, capitalize, featurePath, + pluralize, } from './utility/strings'; export { @@ -64,6 +65,7 @@ export const stringUtils = { group, capitalize, featurePath, + pluralize, }; export { updatePackage } from './utility/update'; diff --git a/modules/effects/schematics-core/utility/ngrx-utils.ts b/modules/effects/schematics-core/utility/ngrx-utils.ts index d856f79346..182e5d88c6 100644 --- a/modules/effects/schematics-core/utility/ngrx-utils.ts +++ b/modules/effects/schematics-core/utility/ngrx-utils.ts @@ -93,8 +93,11 @@ export function addReducerToStateInterface( ? stringUtils.pluralize(options.name) : stringUtils.camelize(options.name); - const keyInsert = - state + ': from' + stringUtils.classify(options.name) + '.State;'; + const keyInsert = `[from${stringUtils.classify( + options.name + )}.${stringUtils.camelize(state)}FeatureKey]: from${stringUtils.classify( + options.name + )}.State;`; const expr = node as any; let position; let toInsert; @@ -144,6 +147,7 @@ export function addReducerToActionReducerMap( return { initializer: variable.initializer, type }; }) + .filter(initWithType => initWithType.type !== undefined) .find(({ type }) => type.typeName.text === 'ActionReducerMap'); if (!actionReducerMap || !actionReducerMap.initializer) { @@ -156,8 +160,11 @@ export function addReducerToActionReducerMap( ? stringUtils.pluralize(options.name) : stringUtils.camelize(options.name); - const keyInsert = - state + ': from' + stringUtils.classify(options.name) + '.reducer,'; + const keyInsert = `[from${stringUtils.classify( + options.name + )}.${stringUtils.camelize(state)}FeatureKey]: from${stringUtils.classify( + options.name + )}.reducer,`; const expr = node as any; let position; let toInsert; @@ -227,12 +234,17 @@ export function addReducerImportToNgModule(options: any): Rule { relativePath, true ); + const state = options.plural + ? stringUtils.pluralize(options.name) + : stringUtils.camelize(options.name); const [storeNgModuleImport] = addImportToModule( source, modulePath, - `StoreModule.forFeature('${stringUtils.camelize( + `StoreModule.forFeature(from${stringUtils.classify( options.name - )}', from${stringUtils.classify(options.name)}.reducer)`, + )}.${state}FeatureKey, from${stringUtils.classify( + options.name + )}.reducer)`, relativePath ); const changes = [...commonImports, reducerImport, storeNgModuleImport]; diff --git a/modules/entity/schematics-core/index.ts b/modules/entity/schematics-core/index.ts index a696576992..1252f4fb06 100644 --- a/modules/entity/schematics-core/index.ts +++ b/modules/entity/schematics-core/index.ts @@ -7,6 +7,7 @@ import { group, capitalize, featurePath, + pluralize, } from './utility/strings'; export { @@ -64,6 +65,7 @@ export const stringUtils = { group, capitalize, featurePath, + pluralize, }; export { updatePackage } from './utility/update'; diff --git a/modules/entity/schematics-core/utility/ngrx-utils.ts b/modules/entity/schematics-core/utility/ngrx-utils.ts index d856f79346..182e5d88c6 100644 --- a/modules/entity/schematics-core/utility/ngrx-utils.ts +++ b/modules/entity/schematics-core/utility/ngrx-utils.ts @@ -93,8 +93,11 @@ export function addReducerToStateInterface( ? stringUtils.pluralize(options.name) : stringUtils.camelize(options.name); - const keyInsert = - state + ': from' + stringUtils.classify(options.name) + '.State;'; + const keyInsert = `[from${stringUtils.classify( + options.name + )}.${stringUtils.camelize(state)}FeatureKey]: from${stringUtils.classify( + options.name + )}.State;`; const expr = node as any; let position; let toInsert; @@ -144,6 +147,7 @@ export function addReducerToActionReducerMap( return { initializer: variable.initializer, type }; }) + .filter(initWithType => initWithType.type !== undefined) .find(({ type }) => type.typeName.text === 'ActionReducerMap'); if (!actionReducerMap || !actionReducerMap.initializer) { @@ -156,8 +160,11 @@ export function addReducerToActionReducerMap( ? stringUtils.pluralize(options.name) : stringUtils.camelize(options.name); - const keyInsert = - state + ': from' + stringUtils.classify(options.name) + '.reducer,'; + const keyInsert = `[from${stringUtils.classify( + options.name + )}.${stringUtils.camelize(state)}FeatureKey]: from${stringUtils.classify( + options.name + )}.reducer,`; const expr = node as any; let position; let toInsert; @@ -227,12 +234,17 @@ export function addReducerImportToNgModule(options: any): Rule { relativePath, true ); + const state = options.plural + ? stringUtils.pluralize(options.name) + : stringUtils.camelize(options.name); const [storeNgModuleImport] = addImportToModule( source, modulePath, - `StoreModule.forFeature('${stringUtils.camelize( + `StoreModule.forFeature(from${stringUtils.classify( options.name - )}', from${stringUtils.classify(options.name)}.reducer)`, + )}.${state}FeatureKey, from${stringUtils.classify( + options.name + )}.reducer)`, relativePath ); const changes = [...commonImports, reducerImport, storeNgModuleImport]; diff --git a/modules/router-store/schematics-core/index.ts b/modules/router-store/schematics-core/index.ts index a696576992..1252f4fb06 100644 --- a/modules/router-store/schematics-core/index.ts +++ b/modules/router-store/schematics-core/index.ts @@ -7,6 +7,7 @@ import { group, capitalize, featurePath, + pluralize, } from './utility/strings'; export { @@ -64,6 +65,7 @@ export const stringUtils = { group, capitalize, featurePath, + pluralize, }; export { updatePackage } from './utility/update'; diff --git a/modules/router-store/schematics-core/utility/ngrx-utils.ts b/modules/router-store/schematics-core/utility/ngrx-utils.ts index d856f79346..182e5d88c6 100644 --- a/modules/router-store/schematics-core/utility/ngrx-utils.ts +++ b/modules/router-store/schematics-core/utility/ngrx-utils.ts @@ -93,8 +93,11 @@ export function addReducerToStateInterface( ? stringUtils.pluralize(options.name) : stringUtils.camelize(options.name); - const keyInsert = - state + ': from' + stringUtils.classify(options.name) + '.State;'; + const keyInsert = `[from${stringUtils.classify( + options.name + )}.${stringUtils.camelize(state)}FeatureKey]: from${stringUtils.classify( + options.name + )}.State;`; const expr = node as any; let position; let toInsert; @@ -144,6 +147,7 @@ export function addReducerToActionReducerMap( return { initializer: variable.initializer, type }; }) + .filter(initWithType => initWithType.type !== undefined) .find(({ type }) => type.typeName.text === 'ActionReducerMap'); if (!actionReducerMap || !actionReducerMap.initializer) { @@ -156,8 +160,11 @@ export function addReducerToActionReducerMap( ? stringUtils.pluralize(options.name) : stringUtils.camelize(options.name); - const keyInsert = - state + ': from' + stringUtils.classify(options.name) + '.reducer,'; + const keyInsert = `[from${stringUtils.classify( + options.name + )}.${stringUtils.camelize(state)}FeatureKey]: from${stringUtils.classify( + options.name + )}.reducer,`; const expr = node as any; let position; let toInsert; @@ -227,12 +234,17 @@ export function addReducerImportToNgModule(options: any): Rule { relativePath, true ); + const state = options.plural + ? stringUtils.pluralize(options.name) + : stringUtils.camelize(options.name); const [storeNgModuleImport] = addImportToModule( source, modulePath, - `StoreModule.forFeature('${stringUtils.camelize( + `StoreModule.forFeature(from${stringUtils.classify( options.name - )}', from${stringUtils.classify(options.name)}.reducer)`, + )}.${state}FeatureKey, from${stringUtils.classify( + options.name + )}.reducer)`, relativePath ); const changes = [...commonImports, reducerImport, storeNgModuleImport]; diff --git a/modules/schematics-core/index.ts b/modules/schematics-core/index.ts index a696576992..1252f4fb06 100644 --- a/modules/schematics-core/index.ts +++ b/modules/schematics-core/index.ts @@ -7,6 +7,7 @@ import { group, capitalize, featurePath, + pluralize, } from './utility/strings'; export { @@ -64,6 +65,7 @@ export const stringUtils = { group, capitalize, featurePath, + pluralize, }; export { updatePackage } from './utility/update'; diff --git a/modules/schematics-core/utility/ngrx-utils.ts b/modules/schematics-core/utility/ngrx-utils.ts index d856f79346..182e5d88c6 100644 --- a/modules/schematics-core/utility/ngrx-utils.ts +++ b/modules/schematics-core/utility/ngrx-utils.ts @@ -93,8 +93,11 @@ export function addReducerToStateInterface( ? stringUtils.pluralize(options.name) : stringUtils.camelize(options.name); - const keyInsert = - state + ': from' + stringUtils.classify(options.name) + '.State;'; + const keyInsert = `[from${stringUtils.classify( + options.name + )}.${stringUtils.camelize(state)}FeatureKey]: from${stringUtils.classify( + options.name + )}.State;`; const expr = node as any; let position; let toInsert; @@ -144,6 +147,7 @@ export function addReducerToActionReducerMap( return { initializer: variable.initializer, type }; }) + .filter(initWithType => initWithType.type !== undefined) .find(({ type }) => type.typeName.text === 'ActionReducerMap'); if (!actionReducerMap || !actionReducerMap.initializer) { @@ -156,8 +160,11 @@ export function addReducerToActionReducerMap( ? stringUtils.pluralize(options.name) : stringUtils.camelize(options.name); - const keyInsert = - state + ': from' + stringUtils.classify(options.name) + '.reducer,'; + const keyInsert = `[from${stringUtils.classify( + options.name + )}.${stringUtils.camelize(state)}FeatureKey]: from${stringUtils.classify( + options.name + )}.reducer,`; const expr = node as any; let position; let toInsert; @@ -227,12 +234,17 @@ export function addReducerImportToNgModule(options: any): Rule { relativePath, true ); + const state = options.plural + ? stringUtils.pluralize(options.name) + : stringUtils.camelize(options.name); const [storeNgModuleImport] = addImportToModule( source, modulePath, - `StoreModule.forFeature('${stringUtils.camelize( + `StoreModule.forFeature(from${stringUtils.classify( options.name - )}', from${stringUtils.classify(options.name)}.reducer)`, + )}.${state}FeatureKey, from${stringUtils.classify( + options.name + )}.reducer)`, relativePath ); const changes = [...commonImports, reducerImport, storeNgModuleImport]; diff --git a/modules/schematics/schematics-core/index.ts b/modules/schematics/schematics-core/index.ts index a696576992..1252f4fb06 100644 --- a/modules/schematics/schematics-core/index.ts +++ b/modules/schematics/schematics-core/index.ts @@ -7,6 +7,7 @@ import { group, capitalize, featurePath, + pluralize, } from './utility/strings'; export { @@ -64,6 +65,7 @@ export const stringUtils = { group, capitalize, featurePath, + pluralize, }; export { updatePackage } from './utility/update'; diff --git a/modules/schematics/schematics-core/utility/ngrx-utils.ts b/modules/schematics/schematics-core/utility/ngrx-utils.ts index d856f79346..182e5d88c6 100644 --- a/modules/schematics/schematics-core/utility/ngrx-utils.ts +++ b/modules/schematics/schematics-core/utility/ngrx-utils.ts @@ -93,8 +93,11 @@ export function addReducerToStateInterface( ? stringUtils.pluralize(options.name) : stringUtils.camelize(options.name); - const keyInsert = - state + ': from' + stringUtils.classify(options.name) + '.State;'; + const keyInsert = `[from${stringUtils.classify( + options.name + )}.${stringUtils.camelize(state)}FeatureKey]: from${stringUtils.classify( + options.name + )}.State;`; const expr = node as any; let position; let toInsert; @@ -144,6 +147,7 @@ export function addReducerToActionReducerMap( return { initializer: variable.initializer, type }; }) + .filter(initWithType => initWithType.type !== undefined) .find(({ type }) => type.typeName.text === 'ActionReducerMap'); if (!actionReducerMap || !actionReducerMap.initializer) { @@ -156,8 +160,11 @@ export function addReducerToActionReducerMap( ? stringUtils.pluralize(options.name) : stringUtils.camelize(options.name); - const keyInsert = - state + ': from' + stringUtils.classify(options.name) + '.reducer,'; + const keyInsert = `[from${stringUtils.classify( + options.name + )}.${stringUtils.camelize(state)}FeatureKey]: from${stringUtils.classify( + options.name + )}.reducer,`; const expr = node as any; let position; let toInsert; @@ -227,12 +234,17 @@ export function addReducerImportToNgModule(options: any): Rule { relativePath, true ); + const state = options.plural + ? stringUtils.pluralize(options.name) + : stringUtils.camelize(options.name); const [storeNgModuleImport] = addImportToModule( source, modulePath, - `StoreModule.forFeature('${stringUtils.camelize( + `StoreModule.forFeature(from${stringUtils.classify( options.name - )}', from${stringUtils.classify(options.name)}.reducer)`, + )}.${state}FeatureKey, from${stringUtils.classify( + options.name + )}.reducer)`, relativePath ); const changes = [...commonImports, reducerImport, storeNgModuleImport]; diff --git a/modules/schematics/src/entity/creator-files/__name@dasherize@if-flat__/__name@dasherize@group-reducers__.reducer.ts.template b/modules/schematics/src/entity/creator-files/__name@dasherize@if-flat__/__name@dasherize@group-reducers__.reducer.ts.template index 6b58b6055a..45ec400744 100644 --- a/modules/schematics/src/entity/creator-files/__name@dasherize@if-flat__/__name@dasherize@group-reducers__.reducer.ts.template +++ b/modules/schematics/src/entity/creator-files/__name@dasherize@if-flat__/__name@dasherize@group-reducers__.reducer.ts.template @@ -3,6 +3,8 @@ import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity'; import { <%= classify(name) %> } from '<%= featurePath(group, flat, "models", dasherize(name)) %><%= dasherize(name) %>.model'; import * as <%= classify(name) %>Actions from '<%= featurePath(group, flat, "actions", dasherize(name)) %><%= dasherize(name) %>.actions'; +export const <%= pluralize(name) %>FeatureKey = '<%= pluralize(name) %>'; + export interface State extends EntityState<<%= classify(name) %>> { // additional entities state properties } diff --git a/modules/schematics/src/entity/files/__name@dasherize@if-flat__/__name@dasherize@group-reducers__.reducer.ts.template b/modules/schematics/src/entity/files/__name@dasherize@if-flat__/__name@dasherize@group-reducers__.reducer.ts.template index ca3c401963..15b7748f82 100644 --- a/modules/schematics/src/entity/files/__name@dasherize@if-flat__/__name@dasherize@group-reducers__.reducer.ts.template +++ b/modules/schematics/src/entity/files/__name@dasherize@if-flat__/__name@dasherize@group-reducers__.reducer.ts.template @@ -2,6 +2,8 @@ import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity'; import { <%= classify(name) %> } from '<%= featurePath(group, flat, "models", dasherize(name)) %><%= dasherize(name) %>.model'; import { <%= classify(name) %>Actions, <%= classify(name) %>ActionTypes } from '<%= featurePath(group, flat, "actions", dasherize(name)) %><%= dasherize(name) %>.actions'; +export const <%= pluralize(name) %>FeatureKey = '<%= pluralize(name) %>'; + export interface State extends EntityState<<%= classify(name) %>> { // additional entities state properties } diff --git a/modules/schematics/src/entity/index.spec.ts b/modules/schematics/src/entity/index.spec.ts index c678bb004e..e73ef31ab1 100644 --- a/modules/schematics/src/entity/index.spec.ts +++ b/modules/schematics/src/entity/index.spec.ts @@ -188,8 +188,21 @@ describe('Entity Schematic', () => { expect( files.indexOf(`${projectPath}/src/app/user.reducer.spec.ts`) ).toBeGreaterThanOrEqual(0); - expect(content).toMatch(/users\: fromUser.State/); - expect(content).toMatch(/users\: fromUser.reducer/); + expect(content).toMatch(/\[fromUser.usersFeatureKey\]\: fromUser.State/); + expect(content).toMatch(/\[fromUser.usersFeatureKey\]\: fromUser.reducer/); + }); + + it('should create a plural featureKey', () => { + const tree = schematicRunner.runSchematic( + 'entity', + defaultOptions, + appTree + ); + const fileContent = tree.readContent( + `${projectPath}/src/app/foo.reducer.ts` + ); + + expect(fileContent).toMatch(/foosFeatureKey = 'foos'/); }); describe('action creators', () => { diff --git a/modules/schematics/src/entity/index.ts b/modules/schematics/src/entity/index.ts index 8e069d4de9..0595489fe1 100644 --- a/modules/schematics/src/entity/index.ts +++ b/modules/schematics/src/entity/index.ts @@ -63,7 +63,7 @@ export default function(options: EntityOptions): Rule { return chain([ addReducerToState({ ...options, plural: true }), - addReducerImportToNgModule({ ...options }), + addReducerImportToNgModule({ ...options, plural: true }), branchAndMerge( chain([mergeWith(commonTemplates), mergeWith(templateSource)]) ), diff --git a/modules/schematics/src/reducer/creator-files/__name@dasherize@if-flat__/__name@dasherize__.reducer.ts.template b/modules/schematics/src/reducer/creator-files/__name@dasherize@if-flat__/__name@dasherize__.reducer.ts.template index d49fffe915..7d200090e7 100644 --- a/modules/schematics/src/reducer/creator-files/__name@dasherize@if-flat__/__name@dasherize__.reducer.ts.template +++ b/modules/schematics/src/reducer/creator-files/__name@dasherize@if-flat__/__name@dasherize__.reducer.ts.template @@ -1,6 +1,8 @@ import { Action, createReducer, on } from '@ngrx/store'; <% if(feature) { %>import * as <%= classify(name) %>Actions from '<%= featurePath(group, flat, "actions", dasherize(name)) %><%= dasherize(name) %>.actions';<% } %> +export const <%= camelize(name) %>FeatureKey = '<%= camelize(name) %>'; + export interface State { } diff --git a/modules/schematics/src/reducer/files/__name@dasherize@if-flat__/__name@dasherize__.reducer.ts.template b/modules/schematics/src/reducer/files/__name@dasherize@if-flat__/__name@dasherize__.reducer.ts.template index 7901446675..81114cb46b 100644 --- a/modules/schematics/src/reducer/files/__name@dasherize@if-flat__/__name@dasherize__.reducer.ts.template +++ b/modules/schematics/src/reducer/files/__name@dasherize@if-flat__/__name@dasherize__.reducer.ts.template @@ -1,6 +1,8 @@ <% if(!feature) { %>import { Action } from '@ngrx/store';<% } %> <% if(feature) { %>import { <%= classify(name) %>Actions, <%= classify(name) %>ActionTypes } from '<%= featurePath(group, flat, "actions", dasherize(name)) %><%= dasherize(name) %>.actions';<% } %> +export const <%= camelize(name) %>FeatureKey = '<%= camelize(name) %>'; + export interface State { } diff --git a/modules/schematics/src/reducer/index.spec.ts b/modules/schematics/src/reducer/index.spec.ts index 0c46c76907..1d8e8ac7aa 100644 --- a/modules/schematics/src/reducer/index.spec.ts +++ b/modules/schematics/src/reducer/index.spec.ts @@ -76,6 +76,19 @@ describe('Reducer Schematic', () => { ).toBeGreaterThanOrEqual(0); }); + it('should create a featureKey', () => { + const tree = schematicRunner.runSchematic( + 'reducer', + defaultOptions, + appTree + ); + const fileContent = tree.readContent( + `${projectPath}/src/app/foo.reducer.ts` + ); + + expect(fileContent).toMatch(/fooFeatureKey = 'foo'/); + }); + it('should create an reducer function', () => { const tree = schematicRunner.runSchematic( 'reducer', @@ -117,7 +130,7 @@ describe('Reducer Schematic', () => { `${projectPath}/src/app/reducers/index.ts` ); - expect(reducers).toMatch(/foo\: fromFoo.State/); + expect(reducers).toMatch(/\[fromFoo.fooFeatureKey\]\: fromFoo.State/); }); it('should add the reducer function to the ActionReducerMap', () => { @@ -128,7 +141,7 @@ describe('Reducer Schematic', () => { `${projectPath}/src/app/reducers/index.ts` ); - expect(reducers).toMatch(/foo\: fromFoo.reducer/); + expect(reducers).toMatch(/\[fromFoo.fooFeatureKey\]\: fromFoo.reducer/); }); it('should group within a "reducers" folder if group is set', () => { diff --git a/modules/schematics/src/store/files/__statePath__/index.ts.template b/modules/schematics/src/store/files/__statePath__/index.ts.template index 0d86d8d1db..375b910b27 100644 --- a/modules/schematics/src/store/files/__statePath__/index.ts.template +++ b/modules/schematics/src/store/files/__statePath__/index.ts.template @@ -6,6 +6,8 @@ import { MetaReducer } from '@ngrx/store'; <% if (!isLib) { %>import { environment } from '<%= environmentsPath %>';<% } %> +<% if (!root) { %> +export const <%= camelize(name) %>FeatureKey = '<%= camelize(name) %>';<% } %> export interface <%= classify(stateInterface) %> { diff --git a/modules/schematics/src/store/index.spec.ts b/modules/schematics/src/store/index.spec.ts index 490fddac63..cf793533fa 100644 --- a/modules/schematics/src/store/index.spec.ts +++ b/modules/schematics/src/store/index.spec.ts @@ -80,7 +80,7 @@ describe('Store Schematic', () => { const content = tree.readContent(`${projectPath}/src/app/app.module.ts`); expect(content).toMatch( - /StoreModule\.forFeature\('foo', fromFoo\.reducers, { metaReducers: fromFoo.metaReducers }\)/ + /StoreModule\.forFeature\(fromFoo.fooFeatureKey, fromFoo\.reducers, { metaReducers: fromFoo.metaReducers }\)/ ); expect( tree.files.indexOf(`${projectPath}/src/app/reducers/index.ts`) @@ -198,7 +198,7 @@ describe('Store Schematic', () => { const content = tree.readContent(`${projectPath}/src/app/app.module.ts`); expect(content).toMatch( - /StoreModule\.forFeature\('foo', fromFoo\.reducers, { metaReducers: fromFoo.metaReducers }\)/ + /StoreModule\.forFeature\(fromFoo.fooFeatureKey, fromFoo\.reducers, { metaReducers: fromFoo.metaReducers }\)/ ); }); @@ -282,6 +282,26 @@ describe('Store Schematic', () => { }).not.toThrow(); }); + it('should add a feature key if not root', () => { + const options = { ...defaultOptions, root: false }; + + const tree = schematicRunner.runSchematic('store', options, appTree); + const content = tree.readContent( + `${projectPath}/src/app/reducers/index.ts` + ); + expect(content).toMatch(/fooFeatureKey = 'foo'/); + }); + + it('should not add a feature key if root', () => { + const options = { ...defaultOptions }; + + const tree = schematicRunner.runSchematic('store', options, appTree); + const content = tree.readContent( + `${projectPath}/src/app/reducers/index.ts` + ); + expect(content).not.toMatch(/fooFeatureKey = 'foo'/); + }); + it('should add store runtime checks', () => { const options = { ...defaultOptions, module: 'app.module.ts' }; diff --git a/modules/schematics/src/store/index.ts b/modules/schematics/src/store/index.ts index 487e47b587..f8282c4db9 100644 --- a/modules/schematics/src/store/index.ts +++ b/modules/schematics/src/store/index.ts @@ -82,9 +82,11 @@ function addImportToNgModule(options: StoreOptions): Rule { modulePath, options.root ? `StoreModule.forRoot(${rootStoreReducers}, ${rootStoreConfig})` - : `StoreModule.forFeature('${stringUtils.camelize( + : `StoreModule.forFeature(from${stringUtils.classify( options.name - )}', from${stringUtils.classify( + )}.${stringUtils.camelize( + options.name + )}FeatureKey, from${stringUtils.classify( options.name )}.reducers, { metaReducers: from${stringUtils.classify( options.name diff --git a/modules/store-devtools/schematics-core/index.ts b/modules/store-devtools/schematics-core/index.ts index a696576992..1252f4fb06 100644 --- a/modules/store-devtools/schematics-core/index.ts +++ b/modules/store-devtools/schematics-core/index.ts @@ -7,6 +7,7 @@ import { group, capitalize, featurePath, + pluralize, } from './utility/strings'; export { @@ -64,6 +65,7 @@ export const stringUtils = { group, capitalize, featurePath, + pluralize, }; export { updatePackage } from './utility/update'; diff --git a/modules/store-devtools/schematics-core/utility/ngrx-utils.ts b/modules/store-devtools/schematics-core/utility/ngrx-utils.ts index d856f79346..182e5d88c6 100644 --- a/modules/store-devtools/schematics-core/utility/ngrx-utils.ts +++ b/modules/store-devtools/schematics-core/utility/ngrx-utils.ts @@ -93,8 +93,11 @@ export function addReducerToStateInterface( ? stringUtils.pluralize(options.name) : stringUtils.camelize(options.name); - const keyInsert = - state + ': from' + stringUtils.classify(options.name) + '.State;'; + const keyInsert = `[from${stringUtils.classify( + options.name + )}.${stringUtils.camelize(state)}FeatureKey]: from${stringUtils.classify( + options.name + )}.State;`; const expr = node as any; let position; let toInsert; @@ -144,6 +147,7 @@ export function addReducerToActionReducerMap( return { initializer: variable.initializer, type }; }) + .filter(initWithType => initWithType.type !== undefined) .find(({ type }) => type.typeName.text === 'ActionReducerMap'); if (!actionReducerMap || !actionReducerMap.initializer) { @@ -156,8 +160,11 @@ export function addReducerToActionReducerMap( ? stringUtils.pluralize(options.name) : stringUtils.camelize(options.name); - const keyInsert = - state + ': from' + stringUtils.classify(options.name) + '.reducer,'; + const keyInsert = `[from${stringUtils.classify( + options.name + )}.${stringUtils.camelize(state)}FeatureKey]: from${stringUtils.classify( + options.name + )}.reducer,`; const expr = node as any; let position; let toInsert; @@ -227,12 +234,17 @@ export function addReducerImportToNgModule(options: any): Rule { relativePath, true ); + const state = options.plural + ? stringUtils.pluralize(options.name) + : stringUtils.camelize(options.name); const [storeNgModuleImport] = addImportToModule( source, modulePath, - `StoreModule.forFeature('${stringUtils.camelize( + `StoreModule.forFeature(from${stringUtils.classify( options.name - )}', from${stringUtils.classify(options.name)}.reducer)`, + )}.${state}FeatureKey, from${stringUtils.classify( + options.name + )}.reducer)`, relativePath ); const changes = [...commonImports, reducerImport, storeNgModuleImport]; diff --git a/modules/store/schematics-core/index.ts b/modules/store/schematics-core/index.ts index a696576992..1252f4fb06 100644 --- a/modules/store/schematics-core/index.ts +++ b/modules/store/schematics-core/index.ts @@ -7,6 +7,7 @@ import { group, capitalize, featurePath, + pluralize, } from './utility/strings'; export { @@ -64,6 +65,7 @@ export const stringUtils = { group, capitalize, featurePath, + pluralize, }; export { updatePackage } from './utility/update'; diff --git a/modules/store/schematics-core/utility/ngrx-utils.ts b/modules/store/schematics-core/utility/ngrx-utils.ts index d856f79346..182e5d88c6 100644 --- a/modules/store/schematics-core/utility/ngrx-utils.ts +++ b/modules/store/schematics-core/utility/ngrx-utils.ts @@ -93,8 +93,11 @@ export function addReducerToStateInterface( ? stringUtils.pluralize(options.name) : stringUtils.camelize(options.name); - const keyInsert = - state + ': from' + stringUtils.classify(options.name) + '.State;'; + const keyInsert = `[from${stringUtils.classify( + options.name + )}.${stringUtils.camelize(state)}FeatureKey]: from${stringUtils.classify( + options.name + )}.State;`; const expr = node as any; let position; let toInsert; @@ -144,6 +147,7 @@ export function addReducerToActionReducerMap( return { initializer: variable.initializer, type }; }) + .filter(initWithType => initWithType.type !== undefined) .find(({ type }) => type.typeName.text === 'ActionReducerMap'); if (!actionReducerMap || !actionReducerMap.initializer) { @@ -156,8 +160,11 @@ export function addReducerToActionReducerMap( ? stringUtils.pluralize(options.name) : stringUtils.camelize(options.name); - const keyInsert = - state + ': from' + stringUtils.classify(options.name) + '.reducer,'; + const keyInsert = `[from${stringUtils.classify( + options.name + )}.${stringUtils.camelize(state)}FeatureKey]: from${stringUtils.classify( + options.name + )}.reducer,`; const expr = node as any; let position; let toInsert; @@ -227,12 +234,17 @@ export function addReducerImportToNgModule(options: any): Rule { relativePath, true ); + const state = options.plural + ? stringUtils.pluralize(options.name) + : stringUtils.camelize(options.name); const [storeNgModuleImport] = addImportToModule( source, modulePath, - `StoreModule.forFeature('${stringUtils.camelize( + `StoreModule.forFeature(from${stringUtils.classify( options.name - )}', from${stringUtils.classify(options.name)}.reducer)`, + )}.${state}FeatureKey, from${stringUtils.classify( + options.name + )}.reducer)`, relativePath ); const changes = [...commonImports, reducerImport, storeNgModuleImport]; diff --git a/projects/example-app/src/app/auth/auth.module.ts b/projects/example-app/src/app/auth/auth.module.ts index 861ab78d2f..c87379f0d7 100644 --- a/projects/example-app/src/app/auth/auth.module.ts +++ b/projects/example-app/src/app/auth/auth.module.ts @@ -10,7 +10,7 @@ import { } from '@example-app/auth/components'; import { AuthEffects } from '@example-app/auth/effects'; -import { reducers } from '@example-app/auth/reducers'; +import * as fromAuth from '@example-app/auth/reducers'; import { MaterialModule } from '@example-app/material'; import { AuthRoutingModule } from './auth-routing.module'; @@ -26,7 +26,7 @@ export const COMPONENTS = [ ReactiveFormsModule, MaterialModule, AuthRoutingModule, - StoreModule.forFeature('auth', reducers), + StoreModule.forFeature(fromAuth.authFeatureKey, fromAuth.reducers), EffectsModule.forFeature([AuthEffects]), ], declarations: COMPONENTS, diff --git a/projects/example-app/src/app/auth/reducers/auth.reducer.ts b/projects/example-app/src/app/auth/reducers/auth.reducer.ts index 49a675d0be..b7cc394e9b 100644 --- a/projects/example-app/src/app/auth/reducers/auth.reducer.ts +++ b/projects/example-app/src/app/auth/reducers/auth.reducer.ts @@ -2,6 +2,8 @@ import { createReducer, on } from '@ngrx/store'; import { AuthApiActions, AuthActions } from '@example-app/auth/actions'; import { User } from '@example-app/auth/models'; +export const statusFeatureKey = 'status'; + export interface State { user: User | null; } diff --git a/projects/example-app/src/app/auth/reducers/index.ts b/projects/example-app/src/app/auth/reducers/index.ts index ef59f9a451..5e53f84623 100644 --- a/projects/example-app/src/app/auth/reducers/index.ts +++ b/projects/example-app/src/app/auth/reducers/index.ts @@ -8,23 +8,27 @@ import * as fromRoot from '@example-app/reducers'; import * as fromAuth from '@example-app/auth/reducers/auth.reducer'; import * as fromLoginPage from '@example-app/auth/reducers/login-page.reducer'; +export const authFeatureKey = 'auth'; + export interface AuthState { - status: fromAuth.State; - loginPage: fromLoginPage.State; + [fromAuth.statusFeatureKey]: fromAuth.State; + [fromLoginPage.loginPageFeatureKey]: fromLoginPage.State; } export interface State extends fromRoot.State { - auth: AuthState; + [authFeatureKey]: AuthState; } export function reducers(state: AuthState | undefined, action: Action) { return combineReducers({ - status: fromAuth.reducer, - loginPage: fromLoginPage.reducer, + [fromAuth.statusFeatureKey]: fromAuth.reducer, + [fromLoginPage.loginPageFeatureKey]: fromLoginPage.reducer, })(state, action); } -export const selectAuthState = createFeatureSelector('auth'); +export const selectAuthState = createFeatureSelector( + authFeatureKey +); export const selectAuthStatusState = createSelector( selectAuthState, diff --git a/projects/example-app/src/app/auth/reducers/login-page.reducer.ts b/projects/example-app/src/app/auth/reducers/login-page.reducer.ts index dfc7981ab6..cd5cc8653c 100644 --- a/projects/example-app/src/app/auth/reducers/login-page.reducer.ts +++ b/projects/example-app/src/app/auth/reducers/login-page.reducer.ts @@ -1,6 +1,8 @@ import { AuthApiActions, LoginPageActions } from '@example-app/auth/actions'; import { createReducer, on } from '@ngrx/store'; +export const loginPageFeatureKey = 'loginPage'; + export interface State { error: string | null; pending: boolean; diff --git a/projects/example-app/src/app/books/books.module.ts b/projects/example-app/src/app/books/books.module.ts index 217adb950b..fcc6895488 100644 --- a/projects/example-app/src/app/books/books.module.ts +++ b/projects/example-app/src/app/books/books.module.ts @@ -20,7 +20,7 @@ import { } from '@example-app/books/containers'; import { BookEffects, CollectionEffects } from '@example-app/books/effects'; -import { reducers } from '@example-app/books/reducers'; +import * as fromBooks from '@example-app/books/reducers'; import { MaterialModule } from '@example-app/material'; import { PipesModule } from '@example-app/shared/pipes'; @@ -51,7 +51,7 @@ export const CONTAINERS = [ * eagerly or lazily and will be dynamically added to * the existing state. */ - StoreModule.forFeature('books', reducers), + StoreModule.forFeature(fromBooks.booksFeatureKey, fromBooks.reducers), /** * Effects.forFeature is used to register effects diff --git a/projects/example-app/src/app/books/reducers/books.reducer.ts b/projects/example-app/src/app/books/reducers/books.reducer.ts index 388e7349a3..f219200635 100644 --- a/projects/example-app/src/app/books/reducers/books.reducer.ts +++ b/projects/example-app/src/app/books/reducers/books.reducer.ts @@ -9,6 +9,8 @@ import { } from '@example-app/books/actions'; import { Book } from '@example-app/books/models'; +export const booksFeatureKey = 'books'; + /** * @ngrx/entity provides a predefined interface for handling * a structured dictionary of records. This interface diff --git a/projects/example-app/src/app/books/reducers/collection.reducer.ts b/projects/example-app/src/app/books/reducers/collection.reducer.ts index 27ff2748f5..7430c64247 100644 --- a/projects/example-app/src/app/books/reducers/collection.reducer.ts +++ b/projects/example-app/src/app/books/reducers/collection.reducer.ts @@ -5,6 +5,8 @@ import { CollectionPageActions, } from '@example-app/books/actions'; +export const collectionFeatureKey = 'collection'; + export interface State { loaded: boolean; loading: boolean; diff --git a/projects/example-app/src/app/books/reducers/index.ts b/projects/example-app/src/app/books/reducers/index.ts index 2487269383..e63207fd17 100644 --- a/projects/example-app/src/app/books/reducers/index.ts +++ b/projects/example-app/src/app/books/reducers/index.ts @@ -10,22 +10,24 @@ import * as fromBooks from '@example-app/books/reducers/books.reducer'; import * as fromCollection from '@example-app/books/reducers/collection.reducer'; import * as fromRoot from '@example-app/reducers'; +export const booksFeatureKey = 'books'; + export interface BooksState { - search: fromSearch.State; - books: fromBooks.State; - collection: fromCollection.State; + [fromSearch.searchFeatureKey]: fromSearch.State; + [fromBooks.booksFeatureKey]: fromBooks.State; + [fromCollection.collectionFeatureKey]: fromCollection.State; } export interface State extends fromRoot.State { - books: BooksState; + [booksFeatureKey]: BooksState; } /** Provide reducer in AoT-compilation happy way */ export function reducers(state: BooksState | undefined, action: Action) { return combineReducers({ - search: fromSearch.reducer, - books: fromBooks.reducer, - collection: fromCollection.reducer, + [fromSearch.searchFeatureKey]: fromSearch.reducer, + [fromBooks.booksFeatureKey]: fromBooks.reducer, + [fromCollection.collectionFeatureKey]: fromCollection.reducer, })(state, action); } @@ -49,7 +51,9 @@ export function reducers(state: BooksState | undefined, action: Action) { * The createFeatureSelector function selects a piece of state from the root of the state object. * This is used for selecting feature states that are loaded eagerly or lazily. */ -export const getBooksState = createFeatureSelector('books'); +export const getBooksState = createFeatureSelector( + booksFeatureKey +); /** * Every reducer module exports selector functions, however child reducers diff --git a/projects/example-app/src/app/books/reducers/search.reducer.ts b/projects/example-app/src/app/books/reducers/search.reducer.ts index cebcf29a1a..e50dd6d577 100644 --- a/projects/example-app/src/app/books/reducers/search.reducer.ts +++ b/projects/example-app/src/app/books/reducers/search.reducer.ts @@ -4,6 +4,8 @@ import { } from '@example-app/books/actions'; import { createReducer, on } from '@ngrx/store'; +export const searchFeatureKey = 'search'; + export interface State { ids: string[]; loading: boolean; diff --git a/projects/example-app/src/app/core/reducers/layout.reducer.ts b/projects/example-app/src/app/core/reducers/layout.reducer.ts index 0ac87bb8c5..74d93adf48 100644 --- a/projects/example-app/src/app/core/reducers/layout.reducer.ts +++ b/projects/example-app/src/app/core/reducers/layout.reducer.ts @@ -2,6 +2,8 @@ import { createReducer, on } from '@ngrx/store'; import { LayoutActions } from '@example-app/core/actions'; +export const layoutFeatureKey = 'layout'; + export interface State { showSidenav: boolean; } diff --git a/projects/example-app/src/app/reducers/index.ts b/projects/example-app/src/app/reducers/index.ts index f2882c2ad8..89337bcf8c 100644 --- a/projects/example-app/src/app/reducers/index.ts +++ b/projects/example-app/src/app/reducers/index.ts @@ -25,7 +25,7 @@ import { InjectionToken } from '@angular/core'; * our top level state interface is just a map of keys to inner state types. */ export interface State { - layout: fromLayout.State; + [fromLayout.layoutFeatureKey]: fromLayout.State; router: fromRouter.RouterReducerState; } @@ -38,7 +38,7 @@ export const ROOT_REDUCERS = new InjectionToken< ActionReducerMap >('Root reducers token', { factory: () => ({ - layout: fromLayout.reducer, + [fromLayout.layoutFeatureKey]: fromLayout.reducer, router: fromRouter.routerReducer, }), }); diff --git a/projects/ngrx.io/content/guide/store/recipes/injecting.md b/projects/ngrx.io/content/guide/store/recipes/injecting.md index e045ad9adf..b28c96b350 100644 --- a/projects/ngrx.io/content/guide/store/recipes/injecting.md +++ b/projects/ngrx.io/content/guide/store/recipes/injecting.md @@ -43,7 +43,7 @@ export function getReducers(): ActionReducerMap<fromFeature.State> { } @NgModule({ - imports: [StoreModule.forFeature('feature', FEATURE_REDUCER_TOKEN)], + imports: [StoreModule.forFeature(fromFeature.featureKey, FEATURE_REDUCER_TOKEN)], providers: [ { provide: FEATURE_REDUCER_TOKEN, @@ -114,7 +114,7 @@ export function getConfig(someService: SomeService): StoreConfig<fromFeature. } @NgModule({ - imports: [StoreModule.forFeature('feature', fromFeature.reducers, FEATURE_CONFIG_TOKEN)], + imports: [StoreModule.forFeature(fromFeature.featureKey, fromFeature.reducers, FEATURE_CONFIG_TOKEN)], providers: [ { provide: FEATURE_CONFIG_TOKEN, diff --git a/projects/ngrx.io/content/guide/store/reducers.md b/projects/ngrx.io/content/guide/store/reducers.md index 85dc02663b..874fe7a282 100644 --- a/projects/ngrx.io/content/guide/store/reducers.md +++ b/projects/ngrx.io/content/guide/store/reducers.md @@ -151,6 +151,10 @@ This registers your application with an empty object for the root state. Now use the `scoreboard` reducer with a feature `NgModule` named `ScoreboardModule` to register additional state. + +export const scoreboardFeatureKey = 'game'; + + import { NgModule } from '@angular/core'; import { StoreModule } from '@ngrx/store'; @@ -158,12 +162,18 @@ import * as fromScoreboard from './reducers/scoreboard.reducer'; @NgModule({ imports: [ - StoreModule.forFeature('game', fromScoreboard.reducer) + StoreModule.forFeature(fromScoreboard.scoreboardFeatureKey, fromScoreboard.reducer) ], }) export class ScoreboardModule {} +
+ +**Note:** It is recommended to abstract a feature key string to prevent hardcoding strings when registering feature state and calling `createFeatureSelector`. + +
+ Add the `ScoreboardModule` to the `AppModule` to load the state eagerly. diff --git a/projects/ngrx.io/content/guide/store/selectors.md b/projects/ngrx.io/content/guide/store/selectors.md index 169433f107..d3fd59c49f 100644 --- a/projects/ngrx.io/content/guide/store/selectors.md +++ b/projects/ngrx.io/content/guide/store/selectors.md @@ -135,6 +135,8 @@ The `createFeatureSelector` is a convenience method for returning a top level fe import { createSelector, createFeatureSelector } from '@ngrx/store'; +export const featureKey = 'feature'; + export interface FeatureState { counter: number; } @@ -143,7 +145,7 @@ export interface AppState { feature: FeatureState; } -export const selectFeature = createFeatureSelector<AppState, FeatureState>('feature'); +export const selectFeature = createFeatureSelector<AppState, FeatureState>(featureKey); export const selectFeatureCount = createSelector( selectFeature, @@ -151,10 +153,10 @@ export const selectFeatureCount = createSelector( ); -The following selector below would not compile because `foo` is not a feature slice of `AppState`. +The following selector below would not compile because `fooFeatureKey` (`'foo'`) is not a feature slice of `AppState`. -export const selectFeature = createFeatureSelector<AppState, FeatureState>('foo'); +export const selectFeature = createFeatureSelector<AppState, FeatureState>(fooFeatureKey); ## Resetting Memoized Selectors