Skip to content

Commit

Permalink
feat(schematics): add support for action creators to schematics (#1765)
Browse files Browse the repository at this point in the history
Closes #1670
  • Loading branch information
brandonroberts authored Apr 17, 2019
1 parent b52b573 commit 876f80a
Show file tree
Hide file tree
Showing 25 changed files with 387 additions and 83 deletions.
4 changes: 4 additions & 0 deletions modules/schematics/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ npm_package(
":README.md",
] + glob([
"**/src/*/files/**/*",
"**/src/*/common-files/**/*",
"**/src/*/creator-files/**/*",
"**/schema.json",
"**/migration.json",
]),
Expand All @@ -51,6 +53,8 @@ ts_test_library(
),
data = glob([
"**/src/*/files/**/*",
"**/src/*/common-files/**/*",
"**/src/*/creator-files/**/*",
"**/*.json",
]),
deps = [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import * as from<%= classify(name) %> from './<%= dasherize(name) %>.actions';

describe('load<%= classify(name) %>s', () => {
it('should return an action', () => {
expect(from<%= classify(name) %>.load<%= classify(name) %>s().type).toBe('[<%= classify(name) %>] Load <%= classify(name) %>s');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createAction, union } from '@ngrx/store';

export const load<%= classify(name) %>s = createAction('[<%= classify(name) %>] Load <%= classify(name) %>s');
<% if (api) { %>export const load<%= classify(name) %>sSuccess = createAction('[<%= classify(name) %>] Load <%= classify(name) %>s Success');<% } %>
<% if (api) { %>export const load<%= classify(name) %>sFailure = createAction('[<%= classify(name) %>] Load <%= classify(name) %>s Failure');<% } %>

const all = union({
load<%= classify(name) %>s,
<% if (api) { %>
load<%= classify(name) %>sSuccess,
load<%= classify(name) %>sFailure
<% } %>
});
export Union = typeof all;
87 changes: 54 additions & 33 deletions modules/schematics/src/action/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,43 +75,64 @@ describe('Action Schematic', () => {
).toBeGreaterThanOrEqual(0);
});

it('should create an enum named "Foo"', () => {
const tree = schematicRunner.runSchematic(
'action',
defaultOptions,
appTree
);
const fileContent = tree.readContent(
`${projectPath}/src/app/foo.actions.ts`
);

expect(fileContent).toMatch(/export enum FooActionTypes/);
});
describe('action classes', () => {
it('should create an enum named "Foo"', () => {
const tree = schematicRunner.runSchematic(
'action',
defaultOptions,
appTree
);
const fileContent = tree.readContent(
`${projectPath}/src/app/foo.actions.ts`
);

expect(fileContent).toMatch(/export enum FooActionTypes/);
});

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

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

expect(fileContent).toMatch(/export class LoadFoos implements Action/);
it('should create the union type based on the provided name', () => {
const tree = schematicRunner.runSchematic(
'action',
defaultOptions,
appTree
);
const fileContent = tree.readContent(
`${projectPath}/src/app/foo.actions.ts`
);

expect(fileContent).toMatch(/export type FooActions = LoadFoos/);
});
});

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

expect(fileContent).toMatch(/export type FooActions = LoadFoos/);
describe('action creators', () => {
const creatorOptions = { ...defaultOptions, actionCreators: true };

it('should create a const for the action creator', () => {
const tree = schematicRunner.runSchematic(
'action',
creatorOptions,
appTree
);
const fileContent = tree.readContent(
`${projectPath}/src/app/foo.actions.ts`
);

expect(fileContent).toMatch(
/export const loadFoos = createAction\('\[Foo\] Load Foos'\);/
);
});
});

it('should group within an "actions" folder if group is set', () => {
Expand Down
33 changes: 18 additions & 15 deletions modules/schematics/src/action/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,24 @@ export default function(options: ActionOptions): Rule {
options.name = parsedPath.name;
options.path = parsedPath.path;

const templateSource = apply(url('./files'), [
options.spec
? noop()
: filter(path => !path.endsWith('.spec.ts.template')),
applyTemplates({
...stringUtils,
'if-flat': (s: string) =>
stringUtils.group(
options.flat ? '' : s,
options.group ? 'actions' : ''
),
...options,
}),
move(parsedPath.path),
]);
const templateSource = apply(
url(options.actionCreators ? './creator-files' : './files'),
[
options.spec
? noop()
: filter(path => !path.endsWith('.spec.ts.template')),
applyTemplates({
...stringUtils,
'if-flat': (s: string) =>
stringUtils.group(
options.flat ? '' : s,
options.group ? 'actions' : ''
),
...options,
}),
move(parsedPath.path),
]
);

return chain([branchAndMerge(chain([mergeWith(templateSource)]))])(
host,
Expand Down
7 changes: 7 additions & 0 deletions modules/schematics/src/action/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@
"description":
"Specifies if api success and failure actions should be generated.",
"aliases": ["a"]
},
"actionCreators": {
"type": "boolean",
"default": false,
"description":
"Specifies whether to generate action creators instead of action classes.",
"aliases": ["ac"]
}
},
"required": []
Expand Down
6 changes: 6 additions & 0 deletions modules/schematics/src/action/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,10 @@ export interface Schema {
* should be generated.
*/
api?: boolean;

/**
* Specifies whether to generate action creators
* instead of action classes.
*/
actionCreators?: boolean;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TestBed, inject } from '@angular/core/testing';
import { TestBed } from '@angular/core/testing';
import { provideMockActions } from '@ngrx/effects/testing';
import { Observable } from 'rxjs';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@ 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 { EMPTY, of } from 'rxjs';
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 (!effectCreators) {%>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 (effectCreators) {%>import * as <%= classify(name) %>Actions from '<%= featurePath(group, flat, "actions", dasherize(name)) %><%= dasherize(name) %>.actions';<% } %>
<% } %>
<% if (feature && !api) { %>import { concatMap } from 'rxjs/operators';
import { EMPTY } from 'rxjs';
import { <%= classify(name) %>ActionTypes, <%= classify(name) %>Actions } from '<%= featurePath(group, flat, "actions", dasherize(name)) %><%= dasherize(name) %>.actions';
<% if (!effectCreators) {%>import { <%= classify(name) %>ActionTypes, <%= classify(name) %>Actions } from '<%= featurePath(group, flat, "actions", dasherize(name)) %><%= dasherize(name) %>.actions';<% } %>
<% if (effectCreators) {%>import * as <%= classify(name) %>Actions from '<%= featurePath(group, flat, "actions", dasherize(name)) %><%= dasherize(name) %>.actions';<% } %>
<% } %>

@Injectable()
export class <%= classify(name) %>Effects {
<% if (feature && api) { %>
<% if (feature && api && !effectCreators) { %>
<%= effectStart %>
ofType(<%= classify(name) %>ActionTypes.Load<%= classify(name) %>s),
concatMap(() =>
Expand All @@ -21,18 +23,34 @@ export class <%= classify(name) %>Effects {
catchError(error => of(new Load<%= classify(name) %>sFailure({ error }))))
)
<%= effectEnd %>
<% } else if (feature && api && effectCreators) { %>
<%= effectStart %>
ofType(<%= classify(name) %>Actions.load<%= 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 }))))
)
<%= effectEnd %>
<% } %>

<% if (feature && !api) { %>
<% if (feature && !api && !effectCreators) { %>
<%= effectStart %>
ofType(<%= classify(name) %>ActionTypes.Load<%= classify(name) %>s),
/** An EMPTY observable only emits completion. Replace with your own observable API request */
concatMap(() => EMPTY)
<%= effectEnd %>
<% } else if (feature && !api && effectCreators) { %>
<%= effectStart %>
ofType(<%= classify(name) %>Actions.load<%= classify(name) %>s),
/** An EMPTY observable only emits completion. Replace with your own observable API request */
concatMap(() => EMPTY)
<%= effectEnd %>
<% } %>

<% if (feature) { %>
<% if (feature && !effectCreators) { %>
constructor(private actions$: Actions<<%= classify(name) %>Actions>) {}
<% } else if (feature && effectCreators) { %>
constructor(private actions$: Actions) {}
<% } else { %>
constructor(private actions$: Actions) {}
<% } %>
Expand Down
15 changes: 15 additions & 0 deletions modules/schematics/src/effect/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,21 @@ describe('Effect Schematic', () => {
);
});

it('should use action creators when effectCreators is enabled in a feature', () => {
const options = { ...defaultOptions, effectCreators: true, feature: true };

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

expect(content).toMatch(
/import { Actions, createEffect, ofType } from '@ngrx\/effects';/
);
expect(content).toMatch(/import \* as FooActions from '\.\/foo\.actions';/);
expect(content).toMatch(/ofType\(FooActions\.loadFoos\),/);
});

it('should create an api effect using creator function', () => {
const options = {
...defaultOptions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { reducer, initialState } from '<%= featurePath(group, flat, "reducers",

describe('<%= classify(name) %> Reducer', () => {
describe('unknown action', () => {
it('should return the initial state', () => {
it('should return the previous state', () => {
const action = {} as any;

const result = reducer(initialState, action);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { createAction, props, union } from '@ngrx/store';
import { Update } from '@ngrx/entity';

import { <%= classify(name) %> } from '<%= featurePath(group, flat, "models", dasherize(name)) %><%= dasherize(name) %>.model';

export const load<%= classify(name) %>s = createAction('[<%= classify(name) %>/API] Load <%= classify(name) %>s', props<{ <%= camelize(name) %>s: <%= classify(name) %>[] }>());
export const add<%= classify(name) %> = createAction('[<%= classify(name) %>/API] Add <%= classify(name) %>', props<{ <%= camelize(name) %>: <%= classify(name) %> }>());
export const upsert<%= classify(name) %> = createAction('[<%= classify(name) %>/API] Upsert <%= classify(name) %>', props<{ <%= camelize(name) %>: <%= classify(name) %> }>());
export const add<%= classify(name) %>s = createAction('[<%= classify(name) %>/API] Add <%= classify(name) %>s', props<{ <%= camelize(name) %>: <%= classify(name) %> }>());
export const upsert<%= classify(name) %>s = createAction('[<%= classify(name) %>/API] Upsert <%= classify(name) %>s', props<{ <%= camelize(name) %>s: <%= classify(name) %>[] }>());
export const update<%= classify(name) %> = createAction('[<%= classify(name) %>/API] Update <%= classify(name) %>', props<{ <%= camelize(name) %>: Update<<%= classify(name) %>> }>());
export const update<%= classify(name) %>s = createAction('[<%= classify(name) %>/API] Update <%= classify(name) %>s', props<{ <%= camelize(name) %>s: Update<<%= classify(name) %>>[] }>());
export const delete<%= classify(name) %> = createAction('[<%= classify(name) %>/API] Delete <%= classify(name) %>', props<{ id: string }>());
export const delete<%= classify(name) %>s = createAction('[<%= classify(name) %>/API] Delete <%= classify(name) %>s', props<{ id: string[] }>());
export const clear<%= classify(name) %>s = createAction('[<%= classify(name) %>/API] Clear <%= classify(name) %>s');

const all = union({
load<%= classify(name) %>s,
add<%= classify(name) %>,
upsert<%= classify(name) %>,
add<%= classify(name) %>s,
upsert<%= classify(name) %>s,
update<%= classify(name) %>,
update<%= classify(name) %>s,
delete<%= classify(name) %>,
delete<%= classify(name) %>s,
clear<%= classify(name) %>s
});
export type Union = typeof all;
Loading

0 comments on commit 876f80a

Please sign in to comment.