diff --git a/README.md b/README.md index 3307f96..41425fb 100644 --- a/README.md +++ b/README.md @@ -8,116 +8,32 @@ For Angular 1 see [ng-redux](https://github.com/wbuchwalter/ng-redux) [![CircleCI](https://img.shields.io/circleci/project/github/angular-redux/store.svg)](https://github.com/angular-redux/store) [![npm version](https://img.shields.io/npm/v/@angular-redux/store.svg)](https://www.npmjs.com/package/@angular-redux/store) -`@angular-redux/store` lets you easily connect your Angular components with Redux, while still respecting the Angular idiom. - -Features include: -* The ability to access slices of store state as `Observables` -* Compatibility with existing Redux middleware and enhancers -* Compatibility with the existing Redux devtools Chrome extension -* A rich, declarative selection syntax using the `@select` decorator - -In addition, we are committed to providing insight on clean strategies for integrating with Angular's change detection and other framework features. - -## Table of Contents - -- [Installation](#installation) -- [Quick Start](#quick-start) -- [Examples](#examples) -- [Resources](#resources) -- [In Depth Usage](#in-depth-usage) -- [API](docs/api.md) - -## Installation - -`@angular-redux/store` has a peer dependency on redux, so we need to install it as well. - -```sh -npm install --save redux @angular-redux/store -``` - -## Quick Start - -```typescript -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { AppModule } from './containers/app.module'; - -platformBrowserDynamic().bootstrapModule(AppModule); -``` -Import the `NgReduxModule` class and add it to your application module as an -`import`. Once you've done this, you'll be able to inject `NgRedux` into your -Angular components. In your top-level app module, you -can configure your Redux store with reducers, initial state, -and optionally middlewares and enhancers as you would in Redux directly. - -```typescript -import { NgReduxModule, NgRedux } from '@angular-redux/store'; -import reduxLogger from 'redux-logger'; -import { rootReducer } from './reducers'; - -interface IAppState { /* ... */ }; - -@NgModule({ - /* ... */ - imports: [ /* ... */, NgReduxModule ] -}) -export class AppModule { - constructor(ngRedux: NgRedux) { - ngRedux.configureStore(rootReducer, {}, [ createLogger() ]); - } -} -``` - -Or if you prefer to create the Redux store yourself you can do that and use the -`provideStore()` function instead: - -```typescript -import { - applyMiddleware, - Store, - combineReducers, - compose, - createStore -} from 'redux'; -import { NgReduxModule, NgRedux } from '@angular-redux/store'; -import reduxLogger from 'redux-logger'; -import { rootReducer } from './reducers'; - -interface IAppState { /* ... */ }; - -export const store: Store = createStore( - rootReducer, - compose(applyMiddleware(reduxLogger))); - -@NgModule({ - /* ... */ - imports: [ /* ... */, NgReduxModule ] -}) -class AppModule { - constructor(ngRedux: NgRedux) { - ngRedux.provideStore(store); - } -} -``` - -Now your Angular app has been reduxified! Use the `@select` decorator to -access your store state, and `.dispatch()` to dispatch actions: - -```typescript -import { select } from '@angular-redux/store'; - -@Component({ - template: '' -}) -class App { - @select() count$: Observable; - - constructor(private ngRedux: NgRedux) {} - - onClick() { - this.ngRedux.dispatch({ type: INCREMENT }); - } -} -``` +## What is Redux? + +Redux is a popular approach to managing state in applications. It emphasises: + +* A single, immutable data store. +* One-way data flow. +* An approach to change based on pure functions and a stream of actions. + +You can find lots of excellent documentation here: [Redux](http://redux.js.org/). + +## What is @angular-redux? + +We provide a set of npm packages that help you integrate your redux store +into your Angular 2+ applications. Our approach helps you by bridging the gap +with some of Angular's advanced features, including: + +* Change processing with RxJS observables. +* Compile time optimizations with `NgModule` and Ahead-of-Time compilation. +* Integration with the Angular change detector. + +## Getting Started + +* I already know what Redux and RxJS are. [Give me the TL;DR](docs/quickstart.md). +* I'm just learning about Redux. [Break it down for me](docs/intro-tutorial.md)! +* Talk is cheap. [Show me a complete code example](https://github.com/angular-redux/example-app). +* Take me to the [API docs](docs/api.md). ## Examples diff --git a/docs/images/counter-hooked.png b/docs/images/counter-hooked.png new file mode 100644 index 0000000..fdeea8f Binary files /dev/null and b/docs/images/counter-hooked.png differ diff --git a/docs/images/counter-unhooked.png b/docs/images/counter-unhooked.png new file mode 100644 index 0000000..a5df713 Binary files /dev/null and b/docs/images/counter-unhooked.png differ diff --git a/docs/images/devtools.png b/docs/images/devtools.png new file mode 100644 index 0000000..3a2b36f Binary files /dev/null and b/docs/images/devtools.png differ diff --git a/docs/images/startup.png b/docs/images/startup.png new file mode 100644 index 0000000..c322362 Binary files /dev/null and b/docs/images/startup.png differ diff --git a/docs/intro-tutorial.md b/docs/intro-tutorial.md new file mode 100644 index 0000000..bf42558 --- /dev/null +++ b/docs/intro-tutorial.md @@ -0,0 +1,535 @@ +# Beginners' Tutorial + +In this tutorial, we'll start from scratch and build a simple counter UI with Angular, +Redux, and @angular-redux/store. I'll try to explain the basic concepts as we go. + +## Installation + +Let's start by generating a simple Angular application using the +[Angular-CLI](https://github.com/angular/angular-cli). + +```sh +# Install Angular CLI +npm install -g angular-cli + +# Use it to spin up a new app. +ng new angular-redux-quickstart +cd angular-redux-quickstart +ng serve +``` + +You should now be able to see your new Angular app running at http://localhost:4200. + +![](images/startup.png) + +Now let's install Redux into your new app: + +```sh +npm install redux @angular-redux/store +``` + +This installs Redux, and @angular-redux/store, the Redux bindings for Angular. + +## Importing @angular-redux/store into your App. + +The first thing we need to do is tell Angular about the new redux functionality +we just installed. We do that by importing the `NgReduxModule` into our application. + +Open up your app's `src/app/app.module.ts` and add the following lines: + +`src/app/app.module.ts`: +```typescript +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { HttpModule } from '@angular/http'; + +import { NgReduxModule, NgRedux } from 'angular-redux/store'; + +import { AppComponent } from './app.component'; // <-- New + +@NgModule({ + declarations: [ + AppComponent + ], + imports: [ + BrowserModule, + FormsModule, + HttpModule, + NgReduxModule, // <-- New + ], + providers: [], + bootstrap: [AppComponent] +}) +export class AppModule { } +``` + +This will allow us to inject services from `@angular-redux` store into our app. + +## A Concrete Example + +Let's build something in our app. We're going to make a simple piece of UI +that can be used as a counter; it will have two buttons, one for incremenenting +a value, and one for decrementing it. + +Open up `src/app/app.component.html` and add the following code: + +```html +
+ Count: {{ count }} + + +
+``` + +Then open `src/app/app.component.ts` and add some fields: + +```typescript +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] +}) +export class AppComponent { + title = 'app works!'; + count: number; // <- New + + increment() {} // <- New + decrement() {} // <- New +} +``` + +![](images/counter-unhooked.png) + +## Modeling the App + +Right now, our counter UI does nothing; it's not hooked up to any state or +logic. Let's step back a bit and think about what we need to make this simple +counter work. + +### Application State + +In order to do its job, the counter component needs to maintain some state: +the current value of the counter. In Redux, we try to avoid keeping application +state in the UI component itself; instead we can keep it in our store. That +way it's easy to find, and it's protected by the immutability guarantees of the +Redux architecture. + +So we can define the type of our state as follows: + +```typescript +interface IAppState { + count: number; +} +``` + +Defining an interface for your store may be overkill for this simple example, +but in larger apps you'll be using `combineReducers` to split your store state +into manageable parts; strong typings will help you keep it all organized. + +### Actions + +There are two events to which we want our application to respond: clicking the +increment and decrement buttons. We will model these as Redux `action`s. + +At any given time, the current value of the count will be modelled as the `reduction` +over the sequence of `INCREMENT` and `DECREMENT` actions that have been triggered. + +So we can think of our application conceptually like this: + +```js +// Pseudocode +const nextValueOfCount = streamOfActions.reduce( + (currentValueOfCount, action) => { + switch(action.type) { + case 'INCREMENT': return state + 1; + case 'DECREMENT': return state - 1; + } + + return state; + }, + { count: 0 }); +``` + +Great! we've just expressed the essence of what our `rootReducer` needs to be for this +simple, one variable, two-action application. + +Let's go ahead and formalize this in our codebase. Create two new files as follows: + +`src/app/app.actions.ts`: +```typescript +import { Injectable } from '@angular/core'; +import { Action } from 'redux'; + +@Injectable() +export class CounterActions { + static INCREMENT = 'INCREMENT'; + static DECREMENT = 'DECREMENT'; + + increment(): Action { + return { type: CounterActions.INCREMENT }; + } + + decrement(): Action { + return { type: CounterActions.DECREMENT }; + } +} +``` + +`src/store.ts`: +```typescript +import { Action } from 'redux'; +import { CounterActions } from './app/app.actions'; + +export interface IAppState { + count: number; +} + +export const INITIAL_STATE: IAppState = { + count: 0, +}; + +export function rootReducer(lastState: IAppState, action: Action): IAppState { + switch(action.type) { + case CounterActions.INCREMENT: return { count: lastState.count + 1 }; + case CounterActions.DECREMENT: return { count: lastState.count - 1 }; + } + + // We don't care about any other actions right now. + return lastState; +} +``` + +## Hooking it up to Angular + +In Redux, most if not all of your application state is collected into something +called a 'store'. You can think of this as a client-side DB that contains the +current data used by your application: in effect your UI is at any time a pure +function of the current state of your store. + +So, let's use the ingredients above to create a Redux store and hook it up to Angular using +`NgRedux.configureStore`. + +`src/app/app.module.ts`: +```typescript +// ... imports as above + +import { rootReducer, IAppStore, INTIAL_STATE } from './store'; // < New +import { CounterActions } from './app.actions'; // <- New + +@NgModule({ + declarations: [ + AppComponent + ], + imports: [ + BrowserModule, + FormsModule, + HttpModule, + NgReduxModule, + ], + providers: [CounterActions], // <- New + bootstrap: [AppComponent] +}) +export class AppModule { + constructor(ngRedux: NgRedux) { + // Tell @angular-redux/store about our rootReducer and our initial state. + // It will use this to create a redux store for us and wire up all the + // events. + ngRedux.configureStore( + rootReducer, + INTIAL_STATE); + } +} +``` + +> Note that if your codebase already has a Redux store set up in non-Angular code, you can register +it with NgRedux using `ngRedux.provideStore` instead of `ngRedux.configureStore`. + +## What's a Reducer Anyway? + +At its heart, a store in Redux is simply a JavaScript object with some data +in it. However, it is immutable. That means it gets wrapped in an interface +that makes it impossible to simply set fields on it like you would normally do. + +Instead, all changes to an application's state are made using one or more 'reducer' +functions. + +In effect, we're modeling our application's behaviour as a collection of events +(or `Actions`) over time, combined with an initial state. + +Actions typically represent things a user has done; however they can also represent +any event affecting your application from an external source (e.g. data coming in +from the network, etc.). + +Each time a new action comes in, the `rootReducer` takes the last state of the +application, considers information provided by the action, and computes the next +state of the store. Once this is done, that new state is broadcasted to the UI, +which recomputes itself from the new state. + +If you're familiar with `Array.prototype.reduce`, your application basically +ends up looking conceptually a bit like this: + +```typescript +// Pseudocode +const finalAppState:IAppState = actionsOverTime.reduce( + rootReducer, + INITIAL_STATE); +``` + +Or perhaps more usefully: + +```typescript +// Pseudocode +const nextState = rootReducer(lastState, mostRecentAction); +UI.render(nextState); +``` + +## Generating Actions + +OK, we've defined our store and hooked it up. However our counter's buttons aren't doing +anything yet. Let's hook that up now. + +What we need to do is make those buttons `dispatch` actions to the Redux store. Remember that +we defined `INCREMENT` and `DECREMENT` actions in `src/app/app.actions.ts`. Let's make sure +they are dispatched when the user clicks the buttons: + +`src/app/app.component.ts`: +```typescript +// Imports as before. + +import { NgRedux } from '@angular-redux/store'; // <- New +import { CounterActions } from './app.actions.ts'; // <- New + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] +}) +export class AppComponent { + title = 'app works!'; + count: number; + + constructor( // <- New + private ngRedux: NgRedux // <- New + private actions: CounterActions) {} // <- New + + increment() { + this.ngRedux.dispatch(this.actions.increment()); // <- New + } + + decrement() { + this.ngRedux.dispatch(this.actions.decrement()); // <- New + } +} +``` + +## Displaying State + +The last thing we need to do is tell our counter component about the current value of `count`. + +We do this by 'selecting' it out of the NgRedux store as an `Observable`. An observable is something +that lets you get the latest value of something that changes over time. Go back to `src/app/app.component.ts` +and `select` the `count` property into your component: + +```typescript +// Imports as before. + +// Decorator as before +export class AppComponent { + title = 'app works!'; + count: number; + subscription; // <- New; + + constructor( + private ngRedux: NgRedux + private actions: CounterActions) { + this.subscription = ngRedux.select('count') // <- New + .subscribe(newCount => this.count = newCount); // <- New + } + + ngOnDestroy() { // <- New + this.subscription.unsubscribe(); // <- New + } // <- New + + // Rest of class as before. +} +``` + +Here, we're listening to a selected observable which will receive the new value of `count` each +time an action happens. We've also added an `ngOnDestroy` so we can 'un-listen' to those events +when our component is unmounted from the DOM. + +At this point your counter should be functional. Try clicking the buttons and see the displayed +number update accordingly. + +![](images/counter-hooked.png) + +## But wait... There's More! + +This is the essence of using `NgRedux`. However, one of the benefits of using Observables with Angular +is that Angular has first-class, optimized support for rendering them via a construct called +[async pipe](https://angular.io/docs/ts/latest/api/common/index/AsyncPipe-pipe.html). + +Instead of manually subscribing to our selected observable, and then remembering to unsubscribe, +we can use `| async` in our template. This causes Angular to manage the subscription, and also allows +for some optimizations at Angular's change detection level. Less boilerplate and faster too! + +Let's give it a try. + +```typescript +// Imports as before. + +import { Observable } from 'rxjs/Observable'; + +// Decorator as before +export class AppComponent { + title = 'app works!'; + readonly count$: Observable; // <- New + + constructor( + private ngRedux: NgRedux + private actions: CounterActions) { + this.count$ = ngRedux.select('count'); // <- New + } + + // Delete ngOnDestroy: it's no longer needed. + // Rest of class as before. +} +``` + +Here, we're saving a reference to the observable itself (`count$: Observable`) instead of to the values it's +being pushed (`count: number`). That `$` on the end is just a convention to let people reading your +code know that this value is an Observable of something, rather than a static value. + +We can now throw a `| async` in our template, and Angular will take care of subscribing to `count$` and +unpacking its values as they come in: + +`app/app.component.html`: +```html + + + Count: {{ count$ | async }} + + +``` + +## But wait... There's Even More! + +`ngRedux.select` is a powerful way to get unfettered access to store Observables; allowing you +to do lots of transformations with RxJS operators to massage the store data in to what more complex +UIs need. However in this scenario it's overkill: we just want to display the current value of +a property in the store. + +For simple cases like this, `@angular-redux/store` exposes a shorthand for selection in the form +of the `@select` decorator. With `@select`, the whole component can be boiled down to the following: + +```typescript +import { Component } from '@angular/core'; +import { NgRedux, select } from '@angular-redux/store'; +import { CounterActions } from './app.actions'; +import { IAppState } from '../store'; +import { Observable } from 'rxjs/Observable'; + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] +}) +export class AppComponent { + title = 'app works!'; + @select() readonly count$: Observable; + + constructor( + private actions: CounterActions, + private ngRedux: NgRedux) {} + + increment() { + this.ngRedux.dispatch(this.actions.increment()); + } + + decrement() { + this.ngRedux.dispatch(this.actions.decrement()); + } +} +``` + +When called with no arguments, `@select` replaces the property it decorates with an Observable +of the store property with the same name as the member variable in question. + +You can also specify a name or even a nested store path manually: + +```typescript +class MyComponent { + @select('count') readonly differentVarNameInComponent$: Observable + @select(['deeply', 'nested', 'store', 'property']) readonly deeplyNested$: Observable; +} +``` + +> There's actually quite a lot more you can do with `@select` and `ngRedux.select`. Check out the +> [API docs](https://github.com/angular-redux/store/blob/master/docs/api.md#selectkey--path--function) for more info. + +## The Redux Community + +The Redux community has a lot of powerful extensions that can be plugged into your store to +enhance it in different ways. Libraries that let you + +* [persist parts of your store to localStorage](https://www.npmjs.com/package/redux-localstorage) +* [handle side-effects and business logic in clean ways](https://www.npmjs.com/package/redux-observable) +* [collect analytics data](https://www.npmjs.com/package/redux-beacon) +* and many more... + +These libraries are implemented as [Redux Middleware](http://redux.js.org/docs/advanced/Middleware.html) +or [StoreEnhancers](https://github.com/reactjs/redux/blob/master/docs/Glossary.md#store-enhancer) and +can be connected to NgRedux using the optional 3rd and fourth parameters of `ngRedux.configureStore`. + +## Troubleshooting + +One of the things that makes Redux's simplified state management model so appealing is that it +allows for some very powerful debugging tools. To try them out on an Angular project, install the +[Redux DevTools chrome extension](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=en). + +Then, make a quick adjustment to enable them in your app: + +`app/app.module.ts` +```typescript +// Other imports as before +import { NgReduxModule, NgRedux, DevToolsExtension } from '@angular-redux/store'; + +@NgModule({ + // Decorator as before +}) +export class AppModule { + constructor( + ngRedux: NgRedux, + devTools: DevToolsExtension) { + + const storeEnhancers = devTools.isEnabled() ? // <- New + [ devTools.enhancer() ] : // <- New + []; // <- New + + ngRedux.configureStore( + rootReducer, + INITIAL_STATE, + [], // <- New + storeEnhancers); // <- New + } +} +``` + +Here, we inject a glue class from NgRedux that can tell if the chrome extension is +installed; if so it exposes it as a Redux store enhancer which can be passed to the +last argument of `ngRedux.configureStore`. + +When this is done, your Chrome devtools will have a new tab that logs all your actions, +displays your current state, and even allows you to rewind or play forward your application! + +![](images/devtools.png) + +## More to Explore + +Take a look at https://github.com/angular-redux/example-app for a more complex example, including +redux integration with Angular's router and forms APIs. + +Also check out the [docs](docs) folder for deep-dives into specific subjects people have asked about. diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 0000000..9f4b532 --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,91 @@ +## Installation + +`@angular-redux/store` has a peer dependency on redux, so we need to install it as well. + +```sh +npm install --save redux @angular-redux/store +``` + +## Quick Start + +```typescript +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { AppModule } from './containers/app.module'; + +platformBrowserDynamic().bootstrapModule(AppModule); +``` +Import the `NgReduxModule` class and add it to your application module as an +`import`. Once you've done this, you'll be able to inject `NgRedux` into your +Angular components. In your top-level app module, you +can configure your Redux store with reducers, initial state, +and optionally middlewares and enhancers as you would in Redux directly. + +```typescript +import { NgReduxModule, NgRedux } from '@angular-redux/store'; +import reduxLogger from 'redux-logger'; +import { rootReducer } from './reducers'; + +interface IAppState { /* ... */ }; + +@NgModule({ + /* ... */ + imports: [ /* ... */, NgReduxModule ] +}) +export class AppModule { + constructor(ngRedux: NgRedux) { + ngRedux.configureStore(rootReducer, {}, [ createLogger() ]); + } +} +``` + +Or if you prefer to create the Redux store yourself you can do that and use the +`provideStore()` function instead: + +```typescript +import { + applyMiddleware, + Store, + combineReducers, + compose, + createStore +} from 'redux'; +import { NgReduxModule, NgRedux } from '@angular-redux/store'; +import reduxLogger from 'redux-logger'; +import { rootReducer } from './reducers'; + +interface IAppState { /* ... */ }; + +export const store: Store = createStore( + rootReducer, + compose(applyMiddleware(reduxLogger))); + +@NgModule({ + /* ... */ + imports: [ /* ... */, NgReduxModule ] +}) +class AppModule { + constructor(ngRedux: NgRedux) { + ngRedux.provideStore(store); + } +} +``` + +Now your Angular app has been reduxified! Use the `@select` decorator to +access your store state, and `.dispatch()` to dispatch actions: + +```typescript +import { select } from '@angular-redux/store'; + +@Component({ + template: '' +}) +class App { + @select() count$: Observable; + + constructor(private ngRedux: NgRedux) {} + + onClick() { + this.ngRedux.dispatch({ type: INCREMENT }); + } +} +```