Skip to content
This repository has been archived by the owner on Dec 30, 2019. It is now read-only.

Commit

Permalink
@ngrx/* update, including required code adoptions.
Browse files Browse the repository at this point in the history
  • Loading branch information
DorianGrey committed Jul 24, 2017
1 parent 0dc3165 commit aa539a1
Show file tree
Hide file tree
Showing 20 changed files with 390 additions and 234 deletions.
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
6.10.2
6.11.1
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ env:
- CXX=g++-4.8
language: node_js
node_js:
- "6.10.2"
- "6.11.1"
addons:
apt:
sources:
Expand Down
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# ng-webpack-template

[![Build Status](https://travis-ci.org/DorianGrey/ng-webpack-template.svg?branch=master)](https://travis-ci.org/DorianGrey/ng-webpack-template)
[![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier)

This project provides a template for an [angular](https://angular.io/) project setup with [webpack](http://webpack.github.io).
It started as a companion of [ng-jspm-template](https://github.com/flaviait/ng2-jspm-template), with the primary purpose to provide an almost identical codebase and feature set compared to its brother to make it easier to figure out which template fits better to the daily requirements of development with [angular](https://angular.io/).
Expand All @@ -10,7 +11,8 @@ It started as a companion of [ng-jspm-template](https://github.com/flaviait/ng2-
To start using this template, you might either
- pick the [latest release](https://github.com/DorianGrey/ng-webpack-template/releases/latest)
- clone the repository directly for the most recent features and updates:
- git clone https://github.com/DorianGrey/ng-webpack-template.git

`git clone https://github.com/DorianGrey/ng-webpack-template.git`

You need to install a node.js version >= 6.9, since this project uses ES2015 language features, and we only support node versions from the most recent LTS upwards.
Things might work from 4.x upwards, but we do not provide any official support for this.
Expand All @@ -26,14 +28,13 @@ The version favors to use [yarn](https://github.com/yarnpkg/yarn) for faster and
```
npm install -g yarn
```
Alternatively, you might use good old `npm`, if you REALLY want to. If that is the case, just replace the `yarn` part of the commands listed below with `npm`.
Alternatively, you might use good old `npm`, if you REALLY want to. If that is the case, I recommend to use a version >= 5.x to get a proper `package-lock.json`. Just replace the `yarn` part of the commands listed below with `npm`.

## Project structure
The intended project structure, how to work with it and possibly extend it is documented in the [docs folder](https://github.com/DorianGrey/ng-webpack-template/tree/master/docs).

- [General structure](https://github.com/DorianGrey/ng-webpack-template/blob/master/docs/general_structure.md)
- [The application state and how to extend it](https://github.com/DorianGrey/ng-webpack-template/blob/master/docs/app_state.md)
- [The linters and why they are not tied to the webpack build](https://github.com/DorianGrey/ng-webpack-template/blob/master/docs/linters.md)

## Additional docs
- [The impact of using long term caching strategies on your assets size](https://github.com/DorianGrey/ng-webpack-template/blob/master/docs/longterm_caching_impact.md)
Expand Down Expand Up @@ -65,8 +66,11 @@ Just note that at the moment, the `Advanced` optimization mode is not yet usable
Using [build-optimizer](https://github.com/angular/devkit/tree/master/packages/angular_devkit/build_optimizer) has an even better impact on the vendor bundle's size. However, it is still extremely experimental, so **beware**. Also note that the changes it applies clash with [Closure Compiler](https://github.com/google/closure-compiler-npm), so you cannot use both in conjunction.

The AoT versions are using the [@ngtools/webpack plugin](https://github.com/angular/angular-cli/blob/master/packages/webpack/README.md).
Please keep an eye on the list of [issues marked as related to it](https://github.com/angular/angular-cli/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aopen%20aot) in case you're facing any errors.

**Beware**: The whole AoT processing currently enforces rather strict rules (see a rather good explanation [here](https://medium.com/@isaacplmann/making-your-angular-2-library-statically-analyzable-for-aot-e1c6f3ebedd5)) on how not only your own code has to be written, but also the code of the libraries you are using. While **I'd strongly recommend** to head this way if it is possible in any way, esp. the latter restriction might screw up this plan.


**Beware**: The whole AoT processing currently enforces rather strict rules (see a rather good explanation [here](https://medium.com/@isaacplmann/making-your-angular-2-library-statically-analyzable-for-aot-e1c6f3ebedd5)) on how not only your own code has to be written, but also the code of the libraries you are using. Before you consider using AoT optimization, you will have to check if all your libraries support it. However, **I'd strongly recommend** to head this way if it is possible in any way. You should also keep an eye on the list of [issues marked as related to it](https://github.com/angular/angular-cli/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aopen%20aot).

The tables below will provide a full overview of the relevant commands.
Note that each build tasks will invoke the `test` task (includes linting, generating the translations and executing the unit-tests) **before** the real build.
Expand Down
81 changes: 45 additions & 36 deletions docs/app_state.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# The application state
The application's globally relevant state is stored using a store from the [ngrx/store](https://github.com/ngrx/store) library. It's content is described in `src/app.store.ts`, in the interface `AppState`:
The application's core state is stored using a store from the [ngrx/store](https://github.com/ngrx/store) library. It's content is described in `src/app.store.ts`, in the interface `AppState`:

export interface AppState {
todos: List<Todo>;
watchTime: number;
export interface CoreAppState {
todos: TodoState;
language: LanguageState;
router: RouterReducerState;
}

It is not required to define an interface for describing your application state, but it simplifies type safe injection of the application's store. E.g., in `src/todos/todo.service.ts`, you will see an injection like this:
Expand All @@ -14,14 +15,15 @@ Without defining an explicit interface, it would be required to use `any` here.

To properly work with your application state during runtime, you need to define a set of `reducers` for each of its entries. In our case, this is also defined in `src/app.store.ts`:

const reducers = {
todos: todosReducer,
watchTime: watchTimeReducer
export const reducers: ActionReducerMap<CoreAppState> = {
todos: todosReducer,
language: languageReducer,
router: routerReducer
};

This hash of reducers gets combined to a single root reducer using the `combineReducers` helper function from `ngrx/store`.
This hash of reducers gets combined to a single root reducer using `StoreModule.forRoot` (see `src/app/app.imports.ts`).

You might have recognized that the name of the keys used in the reducers object and the interface definition is equivalent. This is *intended*, since it simplifies to understand how an entry of the interface is mapped to its counterpart in the reducers objects. We strongly recommend to keep follow this convention.
You might have recognized the `ActionReducerMap<CoreAppState>` interface. Technically, it's a helper to ensure that the hash contains a reducer for exactly every field of `CoreAppState` - if you don't, the transpiler will comply and crash you build.

Please read the source documentation in `src/app.store.ts` to get more detailed information about the values and structures used in there.

Expand All @@ -30,17 +32,32 @@ Extending the application's state is easier than it appears - we'll go through t

First, you should create a `[component-or-module-name].store.ts` along the component or module file that this part of the application state belongs to or is primarily used by. State parts that do not refer to a particular component or module should be added to the global definitions in `src/app.store.ts`.

To properly deal with the state itself, you need to define a list of actions that may alter that state. It most cases, it is sufficient to use an enum or a hash with string fields inside. E.g., for the `todos` page, we've use the latter version like:
To properly deal with the state itself, you need to define a list of actions that may alter that state. First of all, you need to define some named actions that illustrate the potential modifications. An example from the `todos` part of the store:

export const ACTION_TYPES = {
const ACTION_TYPES = {
ADD_TODO: "ADD_TODO"
};

In case of the hash strategy, don't forget to properly freeze this object to prevent its accidental modification:

Object.freeze(ACTION_TYPES);

This is not required for (const) enums, since they cannot be modified during runtime. However, this might need some more `.toString()` calls, since the actions dispatched by the stored have to be identified by strings.
To get the most out of types, it is recommended to use classes to represent you actions. The `type` should be turned into a `readonly` field to avoid accidental modification.

export class AddTodoAction implements Action {
readonly type = ACTION_TYPES.ADD_TODO;
constructor(public payload: Todo) {}
}
Please note that since ngrx v4, the `Action` interface no longer contains a `payload`, so you have to define it yourself as illustrated below. However, it is recommended to stick to this naming convention.

Next, define a type alias with a union of all potential action types to properly type you reducer. Example from `src/app/todos/todos.store.ts`:

export type TodoActions = AddTodoAction | CompleteTodoAction;

Furthermore, you should define an interface or a type alias describing your state part's type.

export interface State {
current: List<Todo>;
completed: List<Todo>;
}

This simplifies defining your reducer (later) and composing your part with the others.

Next, you need to define an initial value for your state. This value will be used when your application gets started, before the first dispatch is executed. In the example above, we've used an empty list of `Todo` entries:

Expand All @@ -50,43 +67,35 @@ For those who argued: This template uses [immutable-js](https://facebook.github.

Go ahead with defining a proper `reducer` for your state. A `reducer` receives two parameters:
- The current state for the particular entry.
- The requested action. This parameters contains two fields:
- `type` is one of your defined actions names. In the example above, `"ADD_TODO"` would be the only possible value. Take care that you define your initial state as the default value of this parameter to get things to properly work on startup and hot reload.
- `payload` is an optional value referring to this action. In the example above, this would be a `Todo` instance that should be added to the list of todos.
- The requested action. If you have followed the convention above when defining your actions, it will have some fields:
- `type` is one of your defined action's name. In the example above, `"ADD_TODO"` would be the only possible value. Take care that you define your initial state as the default value of this parameter to get things to properly work on startup and hot reload.
- `payload` is an optional value referring to this action. In the example above, this would be a `Todo` instance that should be added to the list of todos. Please keep in mind that you might have to type-cast this field, since you're asserting a union type.

The reducer is responsible for properly evaluating these parameters and - in case it accepts the provided action type - returning a new application state. If you want things to work in a reasonable manner, you should take care of two aspects:
1. **Never** alter the state that is provided here!
2. **Always** return a new object containing your state!

If you want to omit at least one of these aspects... don't tell anyone you've not been warned.

In the example mentioned above, the reducer looks like:
In the example mentioned above, the reducer looks like (make sure to export it as a function to remain AoT conforming):

export const todosReducer: ActionReducer<any> = (state: List<Todo> = initialTodoList, action: Action) => {
export function todosReducer(
state: State = initialTodoList, action: TodoActions
): State {
switch (action.type) {
case ACTION_TYPES.ADD_TODO:
return state.push(action.payload);
default:
return state;
}
};

}

As the last step, add your state part to the global `AppState` definition and the corresponding reducer to the global one. Oh, and take care that your reducer and your set of action types is properly exported, to be usable outside of the definition file.

Once you did all this stuff, you are ready to select your new state part from the injected `Store` instance:

store.select(state => state.todos)

# Optional: Use action creators

While exploring the file that we picked the examples from, you might have recognized a so-called `ActionCreator`:

export class TodoActionCreator {
add: (todo: Todo) => Action = todo => {
return {type: ACTION_TYPES.ADD_TODO, payload: todo};
};
}

First of all, using these constructs is entirely optional. However, it simplifies the process of creating mutation actions for your stated, since it hides the concrete structure of the action that gets dispatched by the store. Also, it adds some expressiveness.
# State extension using feature modules

If you decide to use these, don't forget to add them as providers, so that you can properly inject them. Alternatively, since the action creators themselves do not contain any kind of state, you can boil them down to simple helper functions placed in your module.
Since ngrx v4, it is possible to extend the application store during runtime when loading feature modules. Please see
`src/app/+lazy-test/lazy-test.store.ts` and `src/app/+lazy-test/lazy-test.module.ts` for further details and explanations - the steps are almost equivalent to the ones for extending the core state, except that the definition must not be added to the root in `app.store.ts`.
22 changes: 0 additions & 22 deletions docs/linters.md

This file was deleted.

8 changes: 3 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"@angular/platform-server": "^4.3.1",
"@angularclass/hmr": "^2.1.3",
"@angularclass/hmr-loader": "^3.0.4",
"@ngrx/store-devtools": "^3.2.4",
"@ngrx/store-devtools": "^4.0.0",
"@ngtools/webpack": "^1.5.1",
"@types/jasmine": "^2.5.53",
"@types/lodash-es": "^4.14.5",
Expand Down Expand Up @@ -135,17 +135,15 @@
"@angular/platform-browser": "^4.3.1",
"@angular/platform-browser-dynamic": "^4.3.1",
"@angular/router": "^4.3.1",
"@ngrx/core": "1.2.0",
"@ngrx/router-store": "^1.2.6",
"@ngrx/store": "^2.2.3",
"@ngrx/router-store": "^4.0.0",
"@ngrx/store": "^4.0.0",
"@ngx-translate/core": "^7.1.0",
"@types/lodash": "^4.14.70",
"angular-router-loader": "^0.6.0",
"core-js": "2.4.1",
"immutable": "3.8.1",
"lodash-es": "^4.17.4",
"normalize.css": "^7.0.0",
"reselect": "^3.0.1",
"rxjs": "^5.4.2",
"zone.js": "^0.8.14"
},
Expand Down
12 changes: 9 additions & 3 deletions src/app/+lazy-test/lazy-test.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,18 @@ import { SharedModule } from "../shared/shared.module";

import { LAZY_TEST_ROUTES } from "./lazy-test.routes";
import { LazyTestComponent } from "./lazy-test.component";
import { LazyTestActionCreator } from "./lazy-test.store";
import { LAZY_TEST_FEATURE_NAME, watchTimeReducer } from "./lazy-test.store";

import { LazyTestService } from "./lazy-test.service";
import { StoreModule } from "@ngrx/store";

@NgModule({
imports: [LAZY_TEST_ROUTES, SharedModule],
imports: [
LAZY_TEST_ROUTES,
SharedModule,
StoreModule.forFeature(LAZY_TEST_FEATURE_NAME, watchTimeReducer)
],
declarations: [LazyTestComponent],
providers: [LazyTestService, LazyTestActionCreator]
providers: [LazyTestService]
})
export class LazyTestModule {}
14 changes: 7 additions & 7 deletions src/app/+lazy-test/lazy-test.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@ import { Injectable } from "@angular/core";
import { Store } from "@ngrx/store";
import { Observable } from "rxjs/Observable";

import { AppState, getWatchTime } from "../app.store";
import { LazyTestActionCreator } from "./lazy-test.store";
import {
getWatchTime,
LazyTestStateSlice,
IncrementSecondsAction
} from "./lazy-test.store";

@Injectable()
export class LazyTestService {
watchTime: Observable<number>;

constructor(
private store: Store<AppState>,
private actionCreator: LazyTestActionCreator
) {
constructor(private store: Store<LazyTestStateSlice>) {
this.watchTime = this.store.select(getWatchTime);
}

updateSeconds() {
this.store.dispatch(this.actionCreator.increaseWatchTimeSecond());
this.store.dispatch(new IncrementSecondsAction());
}
}
Loading

0 comments on commit aa539a1

Please sign in to comment.