Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(schematics): Use prefix option in feature schematic #3139

Merged
merged 5 commits into from
Sep 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions modules/schematics/schematics-core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export {
addReducerImportToNgModule,
addReducerToActionReducerMap,
omit,
getPrefix,
} from './utility/ngrx-utils';

export { getProjectPath, getProject, isLib } from './utility/project';
Expand Down
6 changes: 6 additions & 0 deletions modules/schematics/schematics-core/utility/ngrx-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,3 +268,9 @@ export function omit<T extends { [key: string]: any }>(
.filter((key) => key !== keyToRemove)
.reduce((result, key) => Object.assign(result, { [key]: object[key] }), {});
}

export function getPrefix(options: any) {
return options.creators
? stringUtils.camelize(options.prefix || 'load')
: stringUtils.capitalize(options.prefix || 'load');
}
12 changes: 7 additions & 5 deletions modules/schematics/src/action/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,18 @@ import {
SchematicContext,
} from '@angular-devkit/schematics';
import { Schema as ActionOptions } from './schema';
import { getProjectPath, stringUtils, parseName } from '../../schematics-core';
import { capitalize, camelize } from '../../schematics-core/utility/strings';
import {
getProjectPath,
stringUtils,
parseName,
getPrefix,
} from '../../schematics-core';

export default function (options: ActionOptions): Rule {
return (host: Tree, context: SchematicContext) => {
options.path = getProjectPath(host, options);

options.prefix = options.creators
? camelize(options.prefix || 'load')
: capitalize(options.prefix || 'load');
options.prefix = getPrefix(options);

const parsedPath = parseName(options.path, options.name);
options.name = parsedPath.name;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { Actions, <%= effectMethod %><% if (feature) { %>, ofType<% } %> } from '@ngrx/effects';
<% if (feature && api) { %>import { catchError, map, concatMap } from 'rxjs/operators';
import { Observable, EMPTY, of } from 'rxjs';
<% if (!creators) {%>import { Load<%= classify(name) %>sFailure, Load<%= classify(name) %>sSuccess, <%= classify(name) %>ActionTypes, <%= classify(name) %>Actions } from '<%= featurePath(group, flat, "actions", dasherize(name)) %><%= dasherize(name) %>.actions';<% } %>
<% if (!creators) {%>import { <%= prefix %><%= classify(name) %>sFailure, <%= prefix %><%= classify(name) %>sSuccess, <%= classify(name) %>ActionTypes, <%= classify(name) %>Actions } from '<%= featurePath(group, flat, "actions", dasherize(name)) %><%= dasherize(name) %>.actions';<% } %>
<% if (creators) {%>import * as <%= classify(name) %>Actions from '<%= featurePath(group, flat, "actions", dasherize(name)) %><%= dasherize(name) %>.actions';<% } %>
<% } %>
<% if (feature && !api) { %>import { concatMap } from 'rxjs/operators';
Expand All @@ -15,34 +15,34 @@ import { Observable, EMPTY } from 'rxjs';
export class <%= classify(name) %>Effects {
<% if (feature && api && !creators) { %>
<%= effectStart %>
ofType(<%= classify(name) %>ActionTypes.Load<%= classify(name) %>s),
ofType(<%= classify(name) %>ActionTypes.<%= prefix %><%= classify(name) %>s),
concatMap(() =>
/** An EMPTY observable only emits completion. Replace with your own observable API request */
EMPTY.pipe(
map(data => new Load<%= classify(name) %>sSuccess({ data })),
catchError(error => of(new Load<%= classify(name) %>sFailure({ error }))))
map(data => new <%= prefix %><%= classify(name) %>sSuccess({ data })),
catchError(error => of(new <%= prefix %><%= classify(name) %>sFailure({ error }))))
)
<%= effectEnd %>
<% } else if (feature && api && creators) { %>
<%= effectStart %>
ofType(<%= classify(name) %>Actions.load<%= classify(name) %>s),
ofType(<%= classify(name) %>Actions.<%= prefix %><%= classify(name) %>s),
concatMap(() =>
/** An EMPTY observable only emits completion. Replace with your own observable API request */
EMPTY.pipe(
map(data => <%= classify(name) %>Actions.load<%= classify(name) %>sSuccess({ data })),
catchError(error => of(<%= classify(name) %>Actions.load<%= classify(name) %>sFailure({ error }))))
map(data => <%= classify(name) %>Actions.<%= prefix %><%= classify(name) %>sSuccess({ data })),
catchError(error => of(<%= classify(name) %>Actions.<%= prefix %><%= classify(name) %>sFailure({ error }))))
)
<%= effectEnd %>
<% } %>
<% if (feature && !api && !creators) { %>
<%= effectStart %>
ofType(<%= classify(name) %>ActionTypes.Load<%= classify(name) %>s),
ofType(<%= classify(name) %>ActionTypes.<%= prefix %><%= classify(name) %>s),
/** An EMPTY observable only emits completion. Replace with your own observable API request */
concatMap(() => EMPTY as Observable<{ type: string }>)
<%= effectEnd %>
<% } else if (feature && !api && creators) { %>
<%= effectStart %>
ofType(<%= classify(name) %>Actions.load<%= classify(name) %>s),
ofType(<%= classify(name) %>Actions.<%= prefix %><%= classify(name) %>s),
/** An EMPTY observable only emits completion. Replace with your own observable API request */
concatMap(() => EMPTY as Observable<{ type: string }>)
<%= effectEnd %>
Expand Down
63 changes: 63 additions & 0 deletions modules/schematics/src/effect/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ describe('Effect Schematic', () => {
root: false,
group: false,
creators: false,
prefix: 'load',
};

const projectPath = getTestProjectPath();
Expand Down Expand Up @@ -463,4 +464,66 @@ describe('Effect Schematic', () => {

expect(content).toMatch(/effects = TestBed\.inject\(FooEffects\);/);
});

it('should add prefix to the effect', async () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One small remark, could you also write a test for when the action creators option is true please, because that's the one that is most used. The rest is looking good 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@timdeschryver , added a test with creators option set as true for in the effect spec file. I'm not sure but do we need tests with creators: true in feature and reducer as well?

Copy link
Member

@timdeschryver timdeschryver Sep 23, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The feature and reducer tests are by default creators: true, so this is fine for me.
Thanks for updating the PR! 👍

const options = {
...defaultOptions,
prefix: 'custom',
feature: true,
api: true,
};

const tree = await schematicRunner
.runSchematicAsync('effect', options, appTree)
.toPromise();
const content = tree.readContent(
`${projectPath}/src/app/foo/foo.effects.ts`
);

expect(content).toMatch(
/import { CustomFoosFailure, CustomFoosSuccess, FooActionTypes, FooActions } from '\.\/foo.actions';/
);

expect(content).toMatch(/customFoos\$ = this\.actions\$.pipe\(/);
expect(content).toMatch(/ofType\(FooActionTypes\.CustomFoos\),/);

expect(content).toMatch(
/map\(data => new CustomFoosSuccess\({ data }\)\),/
);

expect(content).toMatch(
/catchError\(error => of\(new CustomFoosFailure\({ error }\)\)\)\)/
);
});

it('should add prefix to the effect using creator function', async () => {
const options = {
...defaultOptions,
creators: true,
api: true,
feature: true,
prefix: 'custom',
};

const tree = await schematicRunner
.runSchematicAsync('effect', options, appTree)
.toPromise();
const content = tree.readContent(
`${projectPath}/src/app/foo/foo.effects.ts`
);

expect(content).toMatch(
/customFoos\$ = createEffect\(\(\) => {\s* return this.actions\$.pipe\(/
);

expect(content).toMatch(/ofType\(FooActions.customFoos\),/);

expect(content).toMatch(
/map\(data => FooActions.customFoosSuccess\({ data }\)\),/
);

expect(content).toMatch(
/catchError\(error => of\(FooActions.customFoosFailure\({ error }\)\)\)\)/
);
});
});
22 changes: 18 additions & 4 deletions modules/schematics/src/effect/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
insertImport,
parseName,
stringUtils,
getPrefix,
} from '../../schematics-core';
import { Schema as EffectOptions } from './schema';

Expand Down Expand Up @@ -105,12 +106,19 @@ function getEffectMethod(creators?: boolean) {
return creators ? 'createEffect' : 'Effect';
}

function getEffectStart(name: string, creators?: boolean): string {
function getEffectStart(
name: string,
effectPrefix: string,
creators?: boolean
): string {
const effectName = stringUtils.classify(name);
const effectMethodPrefix = stringUtils.camelize(effectPrefix);

return creators
? `load${effectName}s$ = createEffect(() => {` +
? `${effectMethodPrefix}${effectName}s$ = createEffect(() => {` +
'\n return this.actions$.pipe( \n'
: '@Effect()\n' + ` load${effectName}s$ = this.actions$.pipe(`;
: '@Effect()\n' +
` ${effectMethodPrefix}${effectName}s$ = this.actions$.pipe(`;
}

function getEffectEnd(creators?: boolean) {
Expand All @@ -121,6 +129,8 @@ export default function (options: EffectOptions): Rule {
return (host: Tree, context: SchematicContext) => {
options.path = getProjectPath(host, options);

options.prefix = getPrefix(options);

if (options.module) {
options.module = findModuleFromOptions(host, options);
}
Expand All @@ -142,7 +152,11 @@ export default function (options: EffectOptions): Rule {
options.group ? 'effects' : ''
),
effectMethod: getEffectMethod(options.creators),
effectStart: getEffectStart(options.name, options.creators),
effectStart: getEffectStart(
options.name,
options.prefix,
options.creators
),
effectEnd: getEffectEnd(options.creators),
...(options as object),
} as any),
Expand Down
6 changes: 6 additions & 0 deletions modules/schematics/src/effect/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@
"type": "boolean",
"default": false,
"description": "Setup root effects module without registering initial effects."
},
"prefix": {
"description": "The prefix of the effect.",
"type": "string",
"default": "load",
"x-prompt": "What should be the prefix of the effect?"
}
},
"required": []
Expand Down
5 changes: 5 additions & 0 deletions modules/schematics/src/effect/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,9 @@ export interface Schema {
* Setup root effects module without registering initial effects.
*/
minimal?: boolean;

/**
* The prefix for the effects.
*/
prefix?: string;
}
48 changes: 48 additions & 0 deletions modules/schematics/src/feature/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,4 +271,52 @@ describe('Feature Schematic', () => {
/on\(FooActions.loadFoosFailure, \(state, action\) => state\),/
);
});

it('should have all api effect with prefix if api flag enabled', async () => {
const options = {
...defaultOptions,
api: true,
prefix: 'custom',
};

const tree = await schematicRunner
.runSchematicAsync('feature', options, appTree)
.toPromise();
const fileContent = tree.readContent(
`${projectPath}/src/app/foo.effects.ts`
);

expect(fileContent).toMatch(/customFoos\$ = createEffect\(\(\) => {/);
expect(fileContent).toMatch(/ofType\(FooActions.customFoos\),/);

expect(fileContent).toMatch(
/map\(data => FooActions.customFoosSuccess\({ data }\)\),/
);
expect(fileContent).toMatch(
/catchError\(error => of\(FooActions.customFoosFailure\({ error }\)\)\)\)/
);
});

it('should have all api actions with prefix in reducer if api flag enabled', async () => {
const options = {
...defaultOptions,
api: true,
prefix: 'custom',
};

const tree = await schematicRunner
.runSchematicAsync('feature', options, appTree)
.toPromise();
const fileContent = tree.readContent(
`${projectPath}/src/app/foo.reducer.ts`
);

expect(fileContent).toMatch(/on\(FooActions.customFoos, state => state\),/);
expect(fileContent).toMatch(
/on\(FooActions.customFoosSuccess, \(state, action\) => state\),/
);
expect(fileContent).toMatch(
/on\(FooActions.customFoosFailure, \(state, action\) => state\),/
);
});
});
3 changes: 3 additions & 0 deletions modules/schematics/src/feature/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default function (options: FeatureOptions): Rule {
skipTests: options.skipTests,
api: options.api,
creators: options.creators,
prefix: options.prefix,
}),
schematic('reducer', {
flat: options.flat,
Expand All @@ -32,6 +33,7 @@ export default function (options: FeatureOptions): Rule {
feature: true,
api: options.api,
creators: options.creators,
prefix: options.prefix,
}),
schematic('effect', {
flat: options.flat,
Expand All @@ -44,6 +46,7 @@ export default function (options: FeatureOptions): Rule {
feature: true,
api: options.api,
creators: options.creators,
prefix: options.prefix,
}),
schematic('selector', {
flat: options.flat,
Expand Down
6 changes: 6 additions & 0 deletions modules/schematics/src/feature/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@
"description": "Specifies if the actions, reducers, and effects should be created using creator functions",
"aliases": ["c"],
"x-prompt": "Do you want to use the create functions?"
},
"prefix": {
"description": "The prefix of the action, effect and reducer.",
"type": "string",
"default": "load",
"x-prompt": "What should be the prefix of the action, effect and reducer?"
}
},
"required": []
Expand Down
2 changes: 2 additions & 0 deletions modules/schematics/src/feature/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,6 @@ export interface Schema {
* Specifies whether to use creator functions for actions, reducers, and effects.
*/
creators?: boolean;

prefix?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,18 @@ export const initialState: State = {
export const reducer = createReducer(
initialState,
<% if(feature) { %>
on(<%= classify(name) %>Actions.load<%= classify(name) %>s, state => state),
<% if(api) { %> on(<%= classify(name) %>Actions.load<%= classify(name) %>sSuccess, (state, action) => state),
on(<%= classify(name) %>Actions.load<%= classify(name) %>sFailure, (state, action) => state),
on(<%= classify(name) %>Actions.<%= prefix %><%= classify(name) %>s, state => state),
<% if(api) { %> on(<%= classify(name) %>Actions.<%= prefix %><%= classify(name) %>sSuccess, (state, action) => state),
on(<%= classify(name) %>Actions.<%= prefix %><%= classify(name) %>sFailure, (state, action) => state),
<% } %><% } %>
);
<% }else { %>
const <%= camelize(name) %>Reducer = createReducer(
initialState,
<% if(feature) { %>
on(<%= classify(name) %>Actions.load<%= classify(name) %>s, state => state),
<% if(api) { %> on(<%= classify(name) %>Actions.load<%= classify(name) %>sSuccess, (state, action) => state),
on(<%= classify(name) %>Actions.load<%= classify(name) %>sFailure, (state, action) => state),
on(<%= classify(name) %>Actions.<%= prefix %><%= classify(name) %>s, state => state),
<% if(api) { %> on(<%= classify(name) %>Actions.<%= prefix %><%= classify(name) %>sSuccess, (state, action) => state),
on(<%= classify(name) %>Actions.<%= prefix %><%= classify(name) %>sFailure, (state, action) => state),
<% } %><% } %>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ export const initialState: State = {
export function reducer(state = initialState, action: <% if(feature) { %><%= classify(name) %>Actions<% } else { %>Action<% } %>): State {
switch (action.type) {
<% if(feature) { %>
case <%= classify(name) %>ActionTypes.Load<%= classify(name) %>s:
case <%= classify(name) %>ActionTypes.<%= prefix %><%= classify(name) %>s:
return state;
<% if(api) { %>
case <%= classify(name) %>ActionTypes.Load<%= classify(name) %>sSuccess:
case <%= classify(name) %>ActionTypes.<%= prefix %><%= classify(name) %>sSuccess:
return state;

case <%= classify(name) %>ActionTypes.Load<%= classify(name) %>sFailure:
case <%= classify(name) %>ActionTypes.<%= prefix %><%= classify(name) %>sFailure:
return state;
<% } %><% } %>
default:
Expand Down
Loading