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

feat(Schematics): add success/failure effects/actions to ng generate feature #1530

Merged
merged 9 commits into from
Jan 28, 2019
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
import { Action } from '@ngrx/store';

export enum <%= classify(name) %>ActionTypes {
Load<%= classify(name) %>s = '[<%= classify(name) %>] Load <%= classify(name) %>s'
Load<%= classify(name) %>s = '[<%= classify(name) %>] Load <%= classify(name) %>s',
<% if (api) { %>Load<%= classify(name) %>sSuccess = '[<%= classify(name) %>] Load <%= classify(name) %>s Success',<% } %>
<% if (api) { %>Load<%= classify(name) %>sFailure = '[<%= classify(name) %>] Load <%= classify(name) %>s Failure',<% } %>
}

export class Load<%= classify(name) %>s implements Action {
readonly type = <%= classify(name) %>ActionTypes.Load<%= classify(name) %>s;
}
<% if (api) { %>
export class Load<%= classify(name) %>sSuccess implements Action {
readonly type = <%= classify(name) %>ActionTypes.Load<%= classify(name) %>sSuccess;
constructor(public payload: { data: any }) { }
}

export type <%= classify(name) %>Actions = Load<%= classify(name) %>s;
export class Load<%= classify(name) %>sFailure implements Action {
readonly type = <%= classify(name) %>ActionTypes.Load<%= classify(name) %>sFailure;
constructor(public payload: { error: any }) { }
}
<% } %>
<% if (api) { %>export type <%= classify(name) %>Actions = Load<%= classify(name) %>s | Load<%= classify(name) %>sSuccess | Load<%= classify(name) %>sFailure;<% } %>
<% if (!api) { %>export type <%= classify(name) %>Actions = Load<%= classify(name) %>s;<% } %>
54 changes: 54 additions & 0 deletions modules/schematics/src/action/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,58 @@ describe('Action Schematic', () => {
tree.files.indexOf(`${projectPath}/src/app/actions/foo.actions.ts`)
).toBeGreaterThanOrEqual(0);
});

it('should create a success class based on the provided name, given api', () => {
const tree = schematicRunner.runSchematic(
'action',
{
...defaultOptions,
api: true,
},
appTree
);
const fileContent = tree.readContent(
`${projectPath}/src/app/foo.actions.ts`
);

expect(fileContent).toMatch(
/export class LoadFoosSuccess implements Action/
);
});

it('should create a failure class based on the provided name, given api', () => {
const tree = schematicRunner.runSchematic(
'action',
{
...defaultOptions,
api: true,
},
appTree
);
const fileContent = tree.readContent(
`${projectPath}/src/app/foo.actions.ts`
);

expect(fileContent).toMatch(
/export class LoadFoosFailure implements Action/
);
});

it('should create the union type with success and failure based on the provided name, given api', () => {
const tree = schematicRunner.runSchematic(
'action',
{
...defaultOptions,
api: true,
},
appTree
);
const fileContent = tree.readContent(
`${projectPath}/src/app/foo.actions.ts`
);

expect(fileContent).toMatch(
/export type FooActions = LoadFoos \| LoadFoosSuccess \| LoadFoosFailure/
);
});
});
7 changes: 7 additions & 0 deletions modules/schematics/src/action/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@
"default": false,
"description": "Group actions file within 'actions' folder",
"aliases": ["g"]
},
"api": {
"type": "boolean",
"default": false,
"description":
"Specifies if api success and failure actions should be generated.",
"aliases": ["a"]
}
},
"required": []
Expand Down
14 changes: 11 additions & 3 deletions modules/schematics/src/action/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,37 @@ export interface Schema {
/**
* The name of the component.
*/

name: string;

/**
* The path to create the component.
*/

path?: string;

/**
* The name of the project.
*/
project?: string;

/**
* Specifies if a spec file is generated.
*/
spec?: boolean;

/**
* Flag to indicate if a dir is created.
*/

flat?: boolean;

/**
* Group actions file within 'actions' folder
*/

group?: boolean;

/**
* Specifies if api success and failure actions
* should be generated.
*/
api?: boolean;
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,30 @@
import { Injectable } from '@angular/core';
import { Actions, Effect<% if (feature) { %>, ofType<% } %> } from '@ngrx/effects';
<% if (feature) { %>import { <%= classify(name) %>ActionTypes } from '<%= featurePath(group, flat, "actions", dasherize(name)) %><%= dasherize(name) %>.actions';<% } %>
<% if (feature && api) { %>import { catchError, map, concatMap } from 'rxjs/operators';<% } %>
<% if (feature && api) { %>import { EMPTY, of } from 'rxjs';<% } %>
<% if (feature && api) { %>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 (feature && !api) { %>import { <%= classify(name) %>ActionTypes } from '<%= featurePath(group, flat, "actions", dasherize(name)) %><%= dasherize(name) %>.actions';<% } %>

@Injectable()
export class <%= classify(name) %>Effects {
<% if (feature) { %>
<% if (feature && api) { %>
@Effect()
load<%= classify(name) %>s$ = this.actions$.pipe(
ofType(<%= classify(name) %>ActionTypes.Load<%= classify(name) %>s),
concatMap(() =>
EMPTY.pipe(
Copy link
Member

Choose a reason for hiding this comment

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

I feel like we need some comment here to tell the user to replace the empty observable?

Suggested change
EMPTY.pipe(
/** An EMPTY observable only emits completion. Replace with your own observable API request */
EMPTY.pipe(

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That makes sense to me.

map(data => new Load<%= classify(name) %>sSuccess({ data })),
catchError(error => of(new Load<%= classify(name) %>sFailure({ error }))))
)
);
<% } %>
<% if (feature && !api) { %>
@Effect()
load<%= classify(name) %>s$ = this.actions$.pipe(ofType(<%= classify(name) %>ActionTypes.Load<%= classify(name) %>s));
<% } %>
<% if (feature && api) { %>
constructor(private actions$: Actions<<%= classify(name) %>Actions>) {}
<% } else { %>
constructor(private actions$: Actions) {}
<% } %>
}
33 changes: 33 additions & 0 deletions modules/schematics/src/effect/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,4 +277,37 @@ describe('Effect Schematic', () => {
/loadFoos\$ = this\.actions\$.pipe\(ofType\(FooActionTypes\.LoadFoos\)\);/
);
});

it('should create an api effect that describes a source of actions within a feature', () => {
const options = { ...defaultOptions, feature: true, api: true };

const tree = schematicRunner.runSchematic('effect', options, appTree);
const content = tree.readContent(
`${projectPath}/src/app/foo/foo.effects.ts`
);
expect(content).toMatch(
/import { Actions, Effect, ofType } from '@ngrx\/effects';/
);
expect(content).toMatch(
/import { catchError, map, concatMap } from 'rxjs\/operators';/
);
expect(content).toMatch(/import { EMPTY, of } from 'rxjs';/);
expect(content).toMatch(
/import { LoadFoosFailure, LoadFoosSuccess, FooActionTypes, FooActions } from '\.\/foo.actions';/
);

expect(content).toMatch(/export class FooEffects/);
expect(content).toMatch(/loadFoos\$ = this\.actions\$.pipe\(/);
expect(content).toMatch(/ofType\(FooActionTypes\.LoadFoos\),/);
expect(content).toMatch(/concatMap\(\(\) =>/);
expect(content).toMatch(/EMPTY\.pipe\(/);
expect(content).toMatch(/map\(data => new LoadFoosSuccess\({ data }\)\),/);
expect(content).toMatch(
/catchError\(error => of\(new LoadFoosFailure\({ error }\)\)\)\)/
);

expect(content).toMatch(
/constructor\(private actions\$: Actions<FooActions>\) {}/
);
});
});
7 changes: 7 additions & 0 deletions modules/schematics/src/effect/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@
"default": false,
"description": "Group effects file within 'effects' folder",
"aliases": ["g"]
},
"api": {
"type": "boolean",
"default": false,
"description":
"Specifies if effect has api success and failure actions wired up",
"aliases": ["a"]
}
},
"required": []
Expand Down
16 changes: 13 additions & 3 deletions modules/schematics/src/effect/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,50 @@ export interface Schema {
/**
* The name of the component.
*/

name: string;

/**
* The path to create the effect.
*/

path?: string;

/**
* The name of the project.
*/
project?: string;

/**
* Flag to indicate if a dir is created.
*/
flat?: boolean;

/**
* Specifies if a spec file is generated.
*/
spec?: boolean;

/**
* Allows specification of the declaring module.
*/
module?: string;

/**
* Specifies if this is a root-level effect
*/
root?: boolean;

/**
* Specifies if this is grouped within a feature
*/
feature?: boolean;

/**
* Specifies if this is grouped within an 'effects' folder
*/

group?: boolean;

/**
* Specifies if effect has api success and failure actions wired up
*/
api?: boolean;
}
66 changes: 66 additions & 0 deletions modules/schematics/src/feature/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,4 +126,70 @@ describe('Feature Schematic', () => {
/import \* as fromFoo from '\.\/foo\/reducers\/foo.reducer';/
);
});

it('should have all three api actions in actions type union if api flag enabled', () => {
const options = {
...defaultOptions,
api: true,
};

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

expect(fileContent).toMatch(
/export type FooActions = LoadFoos \| LoadFoosSuccess \| LoadFoosFailure/
);
});

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

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

expect(fileContent).toMatch(
/import { Actions, Effect, ofType } from '@ngrx\/effects';/
);
expect(fileContent).toMatch(
/import { catchError, map, concatMap } from 'rxjs\/operators';/
);
expect(fileContent).toMatch(/import { EMPTY, of } from 'rxjs';/);
expect(fileContent).toMatch(
/import { LoadFoosFailure, LoadFoosSuccess, FooActionTypes, FooActions } from '\.\/foo.actions';/
);

expect(fileContent).toMatch(/export class FooEffects/);
expect(fileContent).toMatch(/loadFoos\$ = this\.actions\$.pipe\(/);
expect(fileContent).toMatch(/ofType\(FooActionTypes\.LoadFoos\),/);
expect(fileContent).toMatch(/concatMap\(\(\) =>/);
expect(fileContent).toMatch(/EMPTY\.pipe\(/);
expect(fileContent).toMatch(
/map\(data => new LoadFoosSuccess\({ data }\)\),/
);
expect(fileContent).toMatch(
/catchError\(error => of\(new LoadFoosFailure\({ error }\)\)\)\)/
);
});

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

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

expect(fileContent).toMatch(/case FooActionTypes\.LoadFoosSuccess/);
expect(fileContent).toMatch(/case FooActionTypes\.LoadFoosFailure/);
});
});
3 changes: 3 additions & 0 deletions modules/schematics/src/feature/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export default function(options: FeatureOptions): Rule {
path: options.path,
project: options.project,
spec: false,
api: options.api,
}),
schematic('reducer', {
flat: options.flat,
Expand All @@ -28,6 +29,7 @@ export default function(options: FeatureOptions): Rule {
spec: options.spec,
reducers: options.reducers,
feature: true,
api: options.api,
}),
schematic('effect', {
flat: options.flat,
Expand All @@ -38,6 +40,7 @@ export default function(options: FeatureOptions): Rule {
project: options.project,
spec: options.spec,
feature: true,
api: options.api,
}),
])(host, context);
};
Expand Down
7 changes: 7 additions & 0 deletions modules/schematics/src/feature/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@
"description":
"Group actions, reducers and effects within relative subfolders",
"aliases": ["g"]
},
"api": {
"type": "boolean",
"default": false,
"description":
"Specifies if api success and failure actions, reducer, and effects should be generated as part of this feature.",
"aliases": ["a"]
}
},
"required": []
Expand Down
Loading