Skip to content

Breaking changes for 1.0.0 #11

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

Merged
merged 17 commits into from
Aug 27, 2015
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
189 changes: 107 additions & 82 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,56 +3,36 @@

For Angular 2 see [ng2-redux](https://github.com/wbuchwalter/ng2-redux).

**Warning: The API will be subject to breaking changes until `1.0.0` is released. You can follow progress on the `bc-1.0.0` branch**
**Warning: The API will be subject to breaking changes until `1.0.0` is released.**

[![build status](https://img.shields.io/travis/wbuchwalter/ng-redux/master.svg?style=flat-square)](https://travis-ci.org/wbuchwalter/ng-redux)
[![npm version](https://img.shields.io/npm/v/ng-redux.svg?style=flat-square)](https://www.npmjs.com/package/ng-redux)

## Installation
```js
npm install --save ng-redux
```

## Overview
*ngRedux lets you easily connect your angular components with Redux.*

ngRedux lets you easily connect your angular components with Redux.
the API is straightforward:

```JS
$ngRedux.connect(selector, callback);
```
## Table of Contents

Where `selector` is a function that takes Redux's entire store state as argument and returns an object that contains the slices of store state that your component is interested in.
e.g:
```JS
state => ({todos: state.todos})
```
Note: if you are not familiar with this syntax, go and check out the [MDN Guide on fat arrow functions (ES2015)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions)

If you haven't, check out [reselect](https://github.com/faassen/reselect), an awesome tool to create and combine selectors.
- [Installation](#installation)
- [Quick Start](#quick-start)
- [API](#api)
- [Using DevTools](#using-devtools)

## Installation

This returned object will be passed as argument to the callback provided whenever the state changes.
ngRedux checks for shallow equality of the state's selected slice whenever the Store is updated, and will call the callback only if there is a change.
**Important: It is assumed that you never mutate your states, if you do mutate them, ng-redux will not execute the callback properly.**
See [Redux's doc](http://gaearon.github.io/redux/docs/basics/Reducers.html) to understand why you should not mutate your states.

**The current npm version is outdated, and will be updated once 1.0.0 is finished**
```js
npm install --save ng-redux
```

## Getting Started
## Quick Start

#### Initialization

```JS
$ngReduxProvider.createStoreWith(reducer, [middlewares], storeEnhancer);
```
#### Parameters:
* reducer (Function): A single reducer composed of all other reducers (create with redux.combineReducer)
* [middleware] (Array of Function or String): An array containing all the middleware that should be applied. Functions and strings are both valid members. String will be resolved via Angular, allowing you to use dependency injection in your middlewares.
* storeEnhancer: Optional function that will be used to create the store, in most cases you don't need that, see [Store Enhancer official doc](http://rackt.github.io/redux/docs/Glossary.html#store-enhancer)

```JS
import reducers from './reducers';
import {combineReducers} from 'redux';
import { combineReducers } from 'redux';
import loggingMiddleware from './loggingMiddleware';
import 'ng-redux';

Expand All @@ -64,75 +44,120 @@ angular.module('app', ['ngRedux'])
```

#### Usage
```JS
export default function todoLoader() {
return {
controllerAs: 'vm',
controller: TodoLoaderController,
template: "<div ng-repeat='todo in vm.todos'>{{todo.text}}</div>",

[...]
};
}
*Note: this sample is using the ControllerAs syntax, usage is slightly different without ControllerAs, see API section for more details*

class TodoLoaderController {

constructor($ngRedux) {
this.todos = [];
$ngRedux.connect(state => ({todos: state.todos}), ({todos}) => this.todos = todos);
```JS
import * as CounterActions from '../actions/counter';

class CounterController {
constructor($ngRedux, $scope) {
/* ngRedux will merge the requested state's slice and actions onto the $scope,
you don't need to redefine them in your controller */

$ngRedux.connect($scope, this.mapStateToScope, CounterActions, 'vm');
}

[...]
// Which part of the Redux global state does our component want to receive on $scope?
mapStateToScope(state) {
return {
counter: state.counter
};
}
}
```

**Note: The callback provided to `connect` will be called once directly after creation to allow initialization of your component states**
```HTML
<div>
<p>Clicked: {{vm.counter}} times </p>
<button ng-click='vm.increment()'>+</button>
<button ng-click='vm.decrement()'>-</button>
<button ng-click='vm.incrementIfOdd()'>Increment if odd</button>
<button ng-click='vm.incrementAsync()'>Increment Async</button>
</div>
```

## API

### `createStoreWith([reducer], [middlewares], [storeEnhancers])`

You can also grab multiple slices of the state by passing an array of selectors:
Creates the Redux store, and allow `connect()` to access it.

```JS
constructor($ngRedux) {
this.todos = [];
this.users = [];
$ngRedux.connect(state => ({
todos: state.todos,
users: state.users
}),
({todos, users}) => {
this.todos = todos
this.users = users;
});
}
```
#### Arguments:
* [`reducer`] \(*Function*): A single reducer composed of all other reducers (create with redux.combineReducer)
* [`middlewares`] \(*Function[]*): Optional, An array containing all the middleware that should be applied. Functions and strings are both valid members. String will be resolved via Angular, allowing you to use dependency injection in your middlewares.
* [`storeEnhancers`] \(*Function[]*): Optional, this will be used to create the store, in most cases you don't need to pass anything, see [Store Enhancer official documentation.](http://rackt.github.io/redux/docs/Glossary.html#store-enhancer)


#### Unsubscribing
### `connect([scope], [mapStateToScope], [mapDispatchToScope], [propertyKey])`

You can close a connection like this:
Connects an Angular component to Redux.

```JS
#### Arguments
* [`scope`] \(*Object*): The `$scope` of your controller.
* [`mapStateToScope`] \(*Function*): connect will subscribe to Redux store updates. Any time it updates, mapStateToTarget will be called. Its result must be a plain object, and it will be merged into `target`.
* [`mapDispatchToScope`] \(*Object* or *Function*): If an object is passed, each function inside it will be assumed to be a Redux action creator. An object with the same function names, but bound to a Redux store, will be merged into your component `$scope`. If a function is passed, it will be given `dispatch`. It’s up to you to return an object that somehow uses `dispatch` to bind action creators in your own way. (Tip: you may use the [`bindActionCreators()`](http://gaearon.github.io/redux/docs/api/bindActionCreators.html) helper from Redux.).
* [`propertyKey`] \(*string*): If provided, `mapStateToScope` and `mapDispatchToScope` will merge onto `$scope[propertyKey]`. This is needed especially when using the `ControllerAs` syntax: in this case you should provide the same value than the value provided to controllerAs (e.g: `'vm'`). When not using `ControllerAs` syntax, you are free to omit this argument, everything will be merged directly onto `$scope`.

constructor($ngRedux) {
this.todos = [];
this.unsubscribe = $ngRedux.connect(state => ({todos: state.todos}), ({todos}) => this.todos = todos);
}
#### Remarks
As `$scope` is passed to `connect`, ngRedux will listen to the `$destroy` event and unsubscribe the change listener itself, you don't need to keep track of your subscribtions.

destroy() {
this.unsubscribe();
}
### Store API
All of redux's store methods (i.e. `dispatch`, `subscribe` and `getState`) are exposed by $ngRedux and can be accessed directly. For example:

```JS
$ngRedux.subscribe(() => {
let state = $ngRedux.getState();
//...
})
```

This means that you are free to use Redux basic API in advanced cases where `connect`'s API would not fill your needs.

#### Accessing Redux's store methods
All of redux's store methods (i.e. `dispatch`, `subscribe` and `getState`) are exposed by $ngRedux and can be accessed directly. For example:

## Using DevTools
In order to use Redux DevTools with your angular app, you need to install [react](https://www.npmjs.com/package/react), [react-redux](https://www.npmjs.com/package/react-redux) and [redux-devtools](https://www.npmjs.com/package/redux-devtools) as development dependencies.

```JS
redux.bindActionCreators(actionCreator, $ngRedux.dispatch);
[...]
import { devTools, persistState } from 'redux-devtools';
import { DevTools, DebugPanel, LogMonitor } from 'redux-devtools/lib/react';
import React, { Component } from 'react';

angular.module('app', ['ngRedux'])
.config(($ngReduxProvider) => {
$ngReduxProvider.createStoreWith(rootReducer, [thunk], [devTools()]);
})
.run(($ngRedux, $rootScope) => {
React.render(
<App store={ $ngRedux }/>,
document.getElementById('devTools')
);

//To reflect state changes when disabling/enabling actions via the monitor
//there is probably a smarter way to achieve that
$ngRedux.subscribe(_ => {
setTimeout($rootScope.$apply, 100);
});
});

class App extends Component {
render() {
return (
<div>
<DebugPanel top right bottom>
<DevTools store={ this.props.store } monitor = { LogMonitor } />
</DebugPanel>
</div>
);
}
}
```
**Note:** If you choose to use `subscribe` directly, be sure to [unsubscribe](#unsubscribing) when your current scope is $destroyed.

### Example:
An example can be found here (in TypeScript): [tsRedux](https://github.com/wbuchwalter/tsRedux/blob/master/src/components/regionLister.ts).
```HTML
<body>
<div ng-app='app'>
[...]
</div>
<div id="devTools"></div>
</body>
```
1 change: 1 addition & 0 deletions examples/counter/components/counter.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
<button ng-click='vm.increment()'>+</button>
<button ng-click='vm.decrement()'>-</button>
<button ng-click='vm.incrementIfOdd()'>Increment if odd</button>
<button ng-click='vm.incrementAsync()'>Increment Async</button>
</div>
18 changes: 8 additions & 10 deletions examples/counter/components/counter.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,14 @@ export default function counter() {

class CounterController {

constructor($ngRedux) {
this.counter = 0;
$ngRedux.connect(state => ({
counter: state.counter
}),
({counter}) => this.counter = counter);
constructor($ngRedux, $scope) {
$ngRedux.connect($scope, this.mapStateToScope, CounterActions, 'vm');
}

let {increment, decrement, incrementIfOdd} = bindActionCreators(CounterActions, $ngRedux.dispatch);
this.increment = increment;
this.decrement = decrement;
this.incrementIfOdd = incrementIfOdd;
// Which part of the Redux global state does our component want to receive on $scope?
mapStateToScope(state) {
return {
counter: state.counter
};
}
}
30 changes: 30 additions & 0 deletions examples/counter/devTools.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { persistState } from 'redux-devtools';
import { DevTools, DebugPanel, LogMonitor } from 'redux-devtools/lib/react';
import React, { Component } from 'react';

angular.module('counter')
.run(($ngRedux, $rootScope) => {
React.render(
<App store={ $ngRedux }/>,
document.getElementById('devTools')
);
//Hack to reflect state changes when disabling/enabling actions via the monitor
$ngRedux.subscribe(_ => {
setTimeout($rootScope.$apply, 100);
});
});


class App extends Component {
render() {
return (
<div>
<DebugPanel top right bottom>
<DevTools store={ this.props.store } monitor = { LogMonitor } />
</DebugPanel>
</div>
);
}
}


7 changes: 5 additions & 2 deletions examples/counter/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
<title>{%= o.htmlWebpackPlugin.options.title %}</title>
</head>
<body ng-app='counter'>
<ngr-counter></ngr-counter>
<body>
<div ng-app='counter'>
<ngr-counter></ngr-counter>
</div>
<div id="devTools"></div>
</body>
</html>
3 changes: 2 additions & 1 deletion examples/counter/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import 'ng-redux';
import rootReducer from './reducers';
import thunk from 'redux-thunk';
import counter from './components/counter';
import { devTools } from 'redux-devtools';

angular.module('counter', ['ngRedux'])
.config(($ngReduxProvider) => {
$ngReduxProvider.createStoreWith(rootReducer, [thunk]);
$ngReduxProvider.createStoreWith(rootReducer, [thunk], [devTools()]);
})
.directive('ngrCounter', counter);
2 changes: 2 additions & 0 deletions examples/counter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
"babel-loader": "^5.3.2",
"html-loader": "^0.3.0",
"html-webpack-plugin": "^1.6.1",
"react": "^0.13.3",
"redux-devtools": "^1.1.1",
"webpack": "^1.11.0",
"webpack-dev-server": "^1.10.1"
},
Expand Down
4 changes: 3 additions & 1 deletion examples/counter/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ var HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: [
'webpack/hot/dev-server',
'./index.js'
'./index.js',
//Remove the following line to remove devTools
'./devTools.js'
],
output: {
path: path.join(__dirname, 'dist'),
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
},
"dependencies": {
"invariant": "^2.1.0",
"lodash": "^3.10.1",
"redux": "^1.0.1"
}
}
Loading