├── .bowerrc
├── .editorconfig
├── .ember-cli
├── .gitignore
├── .jshintrc
├── .npmignore
├── .travis.yml
├── .watchmanconfig
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── LICENSE.md
├── README.md
├── addon
├── .gitkeep
├── lib
│ ├── deserialize-from-ember.js
│ └── ember-logger-middleware.js
├── mixins
│ └── ember-redux.js
└── services
│ └── redux-store.js
├── app
├── .gitkeep
├── mixins
│ └── ember-redux.js
└── services
│ └── redux-store.js
├── blueprints
├── .jshintrc
└── ember-cli-redux
│ └── index.js
├── bower.json
├── config
├── ember-try.js
└── environment.js
├── ember-cli-build.js
├── index.js
├── package.json
├── testem.js
├── testem.json
├── tests
├── .jshintrc
├── dummy
│ ├── app
│ │ ├── app.js
│ │ ├── components
│ │ │ └── .gitkeep
│ │ ├── controllers
│ │ │ └── .gitkeep
│ │ ├── helpers
│ │ │ └── .gitkeep
│ │ ├── index.html
│ │ ├── models
│ │ │ └── .gitkeep
│ │ ├── reducers
│ │ │ └── index.js
│ │ ├── resolver.js
│ │ ├── router.js
│ │ ├── routes
│ │ │ └── .gitkeep
│ │ ├── styles
│ │ │ └── app.css
│ │ └── templates
│ │ │ ├── application.hbs
│ │ │ └── components
│ │ │ └── .gitkeep
│ ├── config
│ │ └── environment.js
│ └── public
│ │ ├── crossdomain.xml
│ │ └── robots.txt
├── helpers
│ ├── destroy-app.js
│ ├── module-for-acceptance.js
│ ├── resolver.js
│ └── start-app.js
├── index.html
├── test-helper.js
└── unit
│ ├── .gitkeep
│ ├── lib
│ └── ember-logger-middleware-test.js
│ ├── mixins
│ └── ember-redux-test.js
│ └── services
│ └── redux-store-test.js
└── vendor
└── .gitkeep
/.bowerrc:
--------------------------------------------------------------------------------
1 | {
2 | "directory": "bower_components",
3 | "analytics": false
4 | }
5 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 |
8 | [*]
9 | end_of_line = lf
10 | charset = utf-8
11 | trim_trailing_whitespace = true
12 | insert_final_newline = true
13 | indent_style = space
14 | indent_size = 2
15 |
16 | [*.js]
17 | indent_style = space
18 | indent_size = 2
19 |
20 | [*.hbs]
21 | insert_final_newline = false
22 | indent_style = space
23 | indent_size = 2
24 |
25 | [*.css]
26 | indent_style = space
27 | indent_size = 2
28 |
29 | [*.html]
30 | indent_style = space
31 | indent_size = 2
32 |
33 | [*.{diff,md}]
34 | trim_trailing_whitespace = false
35 |
--------------------------------------------------------------------------------
/.ember-cli:
--------------------------------------------------------------------------------
1 | {
2 | /**
3 | Ember CLI sends analytics information by default. The data is completely
4 | anonymous, but there are times when you might want to disable this behavior.
5 |
6 | Setting `disableAnalytics` to true will prevent any data from being sent.
7 | */
8 | "disableAnalytics": false
9 | }
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 |
7 | # dependencies
8 | /node_modules
9 | /bower_components
10 |
11 | # misc
12 | /.sass-cache
13 | /connect.lock
14 | /coverage/*
15 | /libpeerconnection.log
16 | npm-debug.log
17 | testem.log
18 | profiler-output
19 | .DS_Store
20 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "predef": [
3 | "_",
4 | "document",
5 | "window",
6 | "-Promise",
7 | "Rollbar",
8 | "moment",
9 | "$",
10 | "unescape",
11 | "loadImage"
12 | ],
13 | "browser": true,
14 | "boss": true,
15 | "curly": true,
16 | "debug": false,
17 | "devel": true,
18 | "eqeqeq": true,
19 | "evil": true,
20 | "forin": false,
21 | "immed": false,
22 | "laxbreak": false,
23 | "newcap": true,
24 | "noarg": true,
25 | "noempty": false,
26 | "nonew": false,
27 | "nomen": false,
28 | "onevar": false,
29 | "plusplus": false,
30 | "regexp": false,
31 | "undef": true,
32 | "sub": true,
33 | "strict": false,
34 | "white": false,
35 | "eqnull": true,
36 | "esnext": true,
37 | "unused": true
38 | }
39 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | /bower_components
2 | /config/ember-try.js
3 | /dist
4 | /tests
5 | /tmp
6 | **/.gitkeep
7 | .bowerrc
8 | .editorconfig
9 | .ember-cli
10 | .gitignore
11 | .jshintrc
12 | .watchmanconfig
13 | .travis.yml
14 | bower.json
15 | ember-cli-build.js
16 | testem.js
17 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | ---
2 | language: node_js
3 | node_js:
4 | - "0.12"
5 |
6 | sudo: false
7 |
8 | cache:
9 | directories:
10 | - node_modules
11 |
12 | env:
13 | - EMBER_TRY_SCENARIO=default
14 | - EMBER_TRY_SCENARIO=ember-1-13
15 | - EMBER_TRY_SCENARIO=ember-release
16 | - EMBER_TRY_SCENARIO=ember-beta
17 | - EMBER_TRY_SCENARIO=ember-canary
18 |
19 | matrix:
20 | fast_finish: true
21 | allow_failures:
22 | - env: EMBER_TRY_SCENARIO=ember-canary
23 |
24 | before_install:
25 | - export PATH=/usr/local/phantomjs-2.0.0/bin:$PATH
26 | - "npm config set spin false"
27 | - "npm install -g npm@^2"
28 |
29 | install:
30 | - npm install -g bower
31 | - npm install
32 | - bower install
33 |
34 | script:
35 | - ember try $EMBER_TRY_SCENARIO test
36 |
--------------------------------------------------------------------------------
/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {
2 | "ignore_dirs": ["tmp", "dist"]
3 | }
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Ember CLI Redux Changelog
2 |
3 | #### v0.2.0
4 | * [Breaking Change] We no longer provide the EmberStore to your reducers. This was originally intended as a way to support actions from code unaware of Ember Data. However, it proved to complicate testing quite a bit. If you need access to the store, pass it in using an action. When updating your reducer, simply remove the first `emberStore` parameter.
5 |
6 | ```javascript
7 | // Before
8 | export default function todo(emberStore, state = initialState, action = null) {
9 | switch (action.type) {
10 | /* ... */
11 | }
12 | }
13 |
14 | // After
15 | export default function todo(state = initialState, action = null) {
16 | switch (action.type) {
17 | /* ... */
18 | }
19 | }
20 | ```
21 |
22 | * [Improvement] ember-cli-redux modules are now namespaced.
23 | ```javascript
24 | // Before
25 | import EmberRedux from '../mixins/ember-redux';
26 |
27 | // After
28 | import EmberRedux from 'ember-cli-redux/mixins/ember-redux'
29 | ```
30 |
31 | * [Improvement] The ReduxStore Mixin's `state` property is now a Read Only computed property. This helps ensure that the only changing the state tree is the reducer.
32 | * [Improvement] The ReduxStore is now configurable. You can add specify your own middleware.
33 | * [Improvement] Along with the configurable stores, the Ember logger now takes an `enabled` option to make it easy to disable on production.
34 | * [Fix] Add-on now sets `isDevelopingAddon` to true only when it is npm-linked.
35 |
36 | ## v0.1.0
37 |
38 | * The redux-store service now passes the Ember Data store into the root reducer. This makes it so actions can be dispatched from anywhere without an instance of the Ember store.
39 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Code of Conduct
2 |
3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
4 |
5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion.
6 |
7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
8 |
9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct.
10 |
11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
12 |
13 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
14 |
15 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright AltSchool, PBC (c) 2016
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/AltSchool/ember-cli-redux)
2 |
3 | __ALPHA: API changes likely to come__
4 |
5 | This add-on isn't ready to be used in production. It's a RFC proof-of-concept intended to further the conversation of how state is managed in Ember apps.
6 |
7 | PRs and constructive questions and comments via [GitHub issues](https://github.com/AltSchool/ember-cli-redux/issues/new) are highly encouraged.
8 |
9 | [Example TodoMVC App](http://matthewconstantine.github.io/ember-cli-redux-todos/) ([code](https://github.com/matthewconstantine/ember-cli-redux-todos))
10 |
11 | # Ember-cli-redux
12 |
13 | State management in ambitious Ember apps is difficult. This add-on provides a way to manage the state of your application in a predictable, testable way.
14 |
15 | [Redux](http://redux.js.org/) is an evolution of [Facebook's Flux pattern](https://facebook.github.io/flux/docs/overview.html#content). It was developed for React applications but it plays well with other view libraries. It works surprisingly well with Ember.
16 |
17 | This project provides a Redux Store service and a Mixin for some syntactic sugar. By default, we include the popular `redux-thunk` middleware and a simple Ember-aware logger.
18 |
19 | ## Installation
20 |
21 | * `ember install ember-cli-redux`
22 |
23 | ## Usage
24 |
25 | In any ember object, use the reduxStore service to dispatch actions and read state.
26 |
27 | ### Data Flow Example
28 |
29 | First, the reducer defines an `initialState`, the starting state of your app. Then it does the work of modifying the state and returns the new state when actions are dispatched. The rest of your app updates automatically from this central state using computed properties.
30 |
31 | ```javascript
32 | // app/reducers/index.js
33 | import redux from 'npm:redux';
34 |
35 | const initialState = Ember.Object.create({
36 | count: 0
37 | })
38 |
39 | export default function(emberStore = null, state = initialState, action = null) {
40 | switch (action.type) {
41 | case 'INCREMENT_COUNT':
42 | state.setProperties({count: state.count + 1});
43 | return state;
44 |
45 | default:
46 | return state;
47 | }
48 | };
49 | ```
50 |
51 | Next, The route's `incrementCount` action dispatches a Redux Action to the reducer via the reduxStore.
52 |
53 |
54 | ```javascript
55 | // app/routes/application.js
56 | import Ember from 'ember';
57 | import EmberRedux from 'ember-cli-redux/mixins/ember-redux';
58 |
59 | export default Ember.Route.extend(EmberRedux, {
60 | reduxStore: Ember.inject.service(),
61 |
62 | actions: {
63 | incrementCount() {
64 | this.dispatch({ type: 'INCREMENT_COUNT' })
65 | }
66 | }
67 | });
68 | ```
69 |
70 | Then, the controller provides a `state` computed property to the template.
71 |
72 | ```javascript
73 | // app/controllers/application.js
74 | import Ember from 'ember';
75 |
76 | export default Ember.Controller.extend({
77 | reduxStore: Ember.inject.service(),
78 | state: Ember.computed.alias('reduxStore.state')
79 | });
80 | ```
81 |
82 | Finally, the template renders the state and fires an Ember action on click. The Ember action dispatches an `INCREMENT_COUNT` action, the reducer receives the action, updates the state and Ember rerenders the template.
83 |
84 | ```handlebars
85 | {{!-- app/templates/application.hbs --}}
86 |
Current count: {{state.count}}
87 |
88 | ```
89 |
90 |
91 | ## Core concepts
92 |
93 | Redux introduces a few new concepts. Don't worry, the learning curve is a breeze compared to Ember.
94 |
95 | ### State
96 |
97 | Instead of storing your state across your app in controllers, routes and components, store it in a single nested data structure.
98 |
99 | Now, you have a single place to look for problems related to state. No longer do you have to hunt through your application to figure out which place your state went awry.
100 |
101 | The only way to change that state is by dispatching a Redux Action to a Reducer. More on that next.
102 |
103 | ### Redux Actions
104 |
105 | Redux Actions describe the fact that something happened. They are simple atomic objects that contain a `type` and an optional payload. Here are some examples:
106 |
107 | ```javascript
108 | {
109 | type: FETCH_TODOS
110 | }
111 | {
112 | type: EDIT_TODO,
113 | todo: todo,
114 | title: 'Fly to mars'
115 | }
116 | {
117 | type: TOGGLE_COMPLETED,
118 | todo: todo
119 | }
120 | ```
121 |
122 | You'll notice that actions are just plain javascript objects. They contain just enough information for the Reducer to change the state.
123 |
124 | You often will dispatch Redux actions from your Ember actions:
125 |
126 | ```javascript
127 | actions: {
128 | editTodo(todo) {
129 | this.dispatch({
130 | type: EDIT_TODO,
131 | todo
132 | })
133 | }
134 | }
135 | ```
136 |
137 | Asynchronous actions are similarly easy to deal with. Here, we dispatch an action from a route's model hook:
138 |
139 | ```javascript
140 | model() => {
141 | this.dispatch({type: 'REQUEST_TODOS'});
142 | return this.store.findAll('todo').then((todos) => {
143 | this.dispatch({
144 | type: 'RECEIVE_TODOS',
145 | todos
146 | });
147 | }
148 | ```
149 |
150 | Actions describe what happened and `dispatch` to passes them onto Reducers.
151 |
152 | [More on Actions.](http://redux.js.org/docs/basics/Actions.html)
153 |
154 |
155 | ### Reducer
156 |
157 | The Reducer's job is to specify how an application's state changes in response to an action.
158 |
159 | > For now, just remember that the reducer must be pure. Given the same arguments, it should calculate the next state and return it. No surprises. No side effects. No API calls. No mutations. Just a calculation.
160 |
161 | Global state could easily become unmanageable so Ember CLI Redux keeps it read-only. The only thing that may change the state is a Reducer. A reducer looks something like this:
162 |
163 | ```javascript
164 | function todo(state = initialState, action = null) {
165 | switch (action.type) {
166 | case 'UPDATE_TODO':
167 | action.todo.set('title', action.title); // side effect
168 | action.todo.save(); // side effect
169 | state.setProperties({editingTodo: null});
170 | return state;
171 | ```
172 |
173 | So, about those side effects: Because of Ember's computed properties, we unfortunately create side-effects. However the intent is clear. The reducer takes an action and modifies the state. If we can't have truly immutable state objects in Ember, we can at least limit the mutations to a single testable place.
174 |
175 | Redux provides a helpful `combineReducers` function which simply chains reducers together. It expects a State shape like:
176 |
177 | ```javascript
178 | import redux from 'npm:redux';
179 | const { combineReducers } = redux;
180 |
181 | import auth from './session';
182 | import todoLists from './todo-lists';
183 | import todo from './todo';
184 |
185 | export default combineReducers({
186 | // Add additional reducers here in order of data dependency.
187 | session,
188 | todoLists,
189 | todo
190 | });
191 |
192 | ```
193 |
194 | It expects a state where the top level keys match the reducer names. Upon dispatch, the reducers run in order responding to the actions. Because the state tree is guaranteed to be in a stable state after each dispatch, this strict order makes it easier to handle asynchronous apps since each child reducer is guaranteed the results from previous ones.
195 |
196 | ```
197 | const initialState= {
198 | session,
199 | todoLists,
200 | todo
201 | }
202 | ```
203 |
204 | [More on Reducers.](http://redux.js.org/docs/basics/Reducers.html)
205 |
206 | ### Middleware
207 |
208 | By dispatching actions to a Reducer, we have a chance of action on those actions. That's where middleware comes in. It provides a single extensible interface for adding cross-cutting capabilites to your app. It's perfect for logging, crash reporting, managing asynchronous actions. Here's a [long list of middlewares](https://github.com/xgrommx/awesome-redux#react---a-javascript-library-for-building-user-interfaces). By default Ember CLI Redux provides the popular [redux-thunk](https://github.com/gaearon/redux-thunk) middleware and a simple Ember aware logger.
209 |
210 | #### Customizing Middleware
211 |
212 | To customize or add your own middleware, extend the reduxStore like this:
213 |
214 | ```javascript
215 | // app/services/redux-store.js
216 | import ReduxStore from 'ember-cli-redux/services/redux-store';
217 | import reducer from '../reducers/index';
218 | import emberLoggerMiddleware from 'ember-cli-redux/lib/ember-logger-middleware';
219 |
220 | const logger = emberLoggerMiddleware({
221 | enabled: true
222 | });
223 |
224 | export default ReduxStore.extend({
225 | reducer,
226 |
227 | middleware: [logger],
228 | });
229 | ```
230 |
231 | Adding your own middleware is straightforward:
232 | ```javascript
233 | const customLogger = (/* store */) => next => action => {
234 | console.log(`Hey! The action is ${action.type}`, action);
235 | return next(action);
236 | };
237 | ```
238 |
239 | [More on Middleware.](http://redux.js.org/docs/advanced/Middleware.html)
240 |
241 | That's it for the core concepts. State, Actions, Reducers and Middleware.
242 |
243 | ## The problems Redux alleviates in Ember Apps
244 |
245 | ### Routes are heavy
246 |
247 | Routes in Ember apps routes tend to hold a lot of logic. They're responsible for fetching data, setting up controllers, managing url parameters and performing common-ancestor duties.
248 |
249 | They're responsible for pausing transitions while data loads. They're the only place you can choose which data is required for render and which can be loaded afterwards.
250 |
251 | Because of that, they are tightly coupled to your view. If you need a sidebar to load before your app body, you need to nest a route. Got that wrong? You'll need to re-architect your route structure and likely a handful of files that go along with it.
252 |
253 | Redux can help. It moves the heavy lifting to new Reducer layer. Multiple routes can dispatch redux actions to the Reducer. That puts your routes on a diet making them easier to deal with.
254 |
255 | If your app has routes that perform similar actions, Redux is a natural fit. Say you have an `/items` route and a `/admin/items` route. Rather than relying on mixins or inheritance, just create two routes that dispatch the same actions. The admin action can dispatch additional actions.
256 |
257 | ### Ember State is Everywhere
258 |
259 | Say you wanted to save the state of your Ember app, as it is at any given moment. How would you do it?
260 |
261 | Keep in mind, we want to restore **everything** as it was, which data was loaded, which items were selected, which views were toggled, what the user had started to type, etc. How would you do it? You would find state stored in different controllers, components and routes - any place you used `this.set()` or `controller.set()`. Gathering up this state would prove enormously difficult. Implementing app-wide feature like Undo and Redo would prove daunting. As would sending error reports with the exact state a user sees and a ledger of actions leading to it.
262 |
263 | Redux helps by centralizing the state and all the ways it can be transformed. After each dispatch your app is guaranteed to be in a stable serializable state. You can log all the transformations and replay them elsewhere. Undo and Redo becomes fun to implement.
264 |
265 | ### Debugging Ember Apps is hard
266 |
267 | A typical lage-scale Ember app has state distributed across the app. Add to that asynchronous events that modify that state. Throw in a handful of mixins, injected dependencies, long computed property chains and data passed through deep layers of routes and components. Soon it becomes difficult to know the facts about what happened in your app, when it happened, and what changed as a result.
268 |
269 | By increasing the rigor around actions and state we reduce the number of places you need to look for problems.
270 |
271 | Debugging an Ember app with Redux becomes much easier:
272 |
273 | 1. Did your route, controller or component dispatch a Redux Action? If not, why not?
274 | 2. Did your reducer modify the state properly? If not, why not? And by the way, here's a log of the exact state before and after the action fired.
275 |
276 | # Anticipated Questions
277 |
278 | ### Is this ready to be used in production?
279 |
280 | No. Expect some breaking API changes as we work through more use cases.
281 |
282 | ### Is this enormous?
283 |
284 | Redux is a tiny (About 2kB) and provides a pattern that Ember apps could really benefit from. The API is similarly tiny. If you've struggled to learn the depth of EmberData you'll find this to be a breeze in comparison.
285 |
286 | ### Does this replace Ember Data?
287 |
288 | This plays well with Ember Data. It provides a top-level state tree. You're free to add any kind of data to that tree. This alpha version includes a logger which will deserialize your ember models for easier debugging.
289 |
290 | ### How is this different than the EmberData Store?
291 |
292 | The EmberData store only holds Ember Models. The rest of your application's state has to be captured elsewhere. This includes lists of models that are loaded from the Ember Store. Without centralized state you're on your own to find appropriate places across your app to store that state.
293 |
294 | With Redux, you keep your state in one place and let EmberData do what it does best, fetch and cache Ember models.
295 |
296 | ### I've heard Globals are evil, how is this different?
297 |
298 | The Redux store can't be changed at-will by your app. It's only modified by Reducers which in turn can only take Redux Actions (simple objects like `{type: CREATE_ITEM, title: 'Foo'}`). This means the state can only change in predictable, easy-to-test ways. Furthermore, logging reveals exactly what changed and when.
299 |
300 | ### Does this require major all-or-nothing changes to my Ember app?
301 |
302 | Remarkably, no. You can ease a complex Ember app into using Redux. However, the more of your app you transition the more you benefit. If you plan to use a time-traveling-debugger, or error reports that include the full application state, you'll need to transition more of your application state to Redux.
303 |
304 | ### How are Redux Actions and Ember Actions different?
305 |
306 | Ember Actions modify the application state directly. Redux Actions are a simple data format with a `type` and optional payload. You use `store.dispatch` to send the action to the Reducer. The Reducer does the work of changing the application state. Your UI then observes the state and gets the changes in real-time.
307 |
308 | You use them by simply dispatch a payload to a Reducer which updates the Redux State. In most cases you can replace your Ember actions with calls to `this.dispatch`.
309 |
310 | ### Do I need this?
311 |
312 | If your app has little or no state, then probably not. If your application state is well-represented and easy to debug, then maybe not. If your routes cleanly match your UI, with no need for mixins then move on. In other words, if your app matches the happy-path laid out for you by Ember then you might find less benefit to using this.
313 |
314 | You might need this if:
315 |
316 | * You use `controller.set` in more than a couple places in your app.
317 | * If your routing and view layer seem too tightly-coupled.
318 | * Changes to the UI's layout require excessive churn in your codebase.
319 | * You have long computed-property chains that reach up the routing hierarchy.
320 | * Error reports from your production environments are hard to reproduce. Especially when race conditions are involved.
321 | * You find it difficult to specify what promises should and shouldn't pause your route transitions.
322 |
323 | ### What might this lead to?
324 |
325 | Centralized state is key for features like [time traveling debugging](https://github.com/gaearon/redux-devtools) and [hot module replacement](https://webpack.github.io/docs/hot-module-replacement.html), two technologies that can dramatically improve the development experience. If we can build a reasonable pattern for managing and serializing state in Ember, we'll have a foundation for some pretty useful tech.
326 |
327 | ## Running Tests
328 |
329 | * `ember test`
330 | * `ember test --server`
331 |
332 | For more information on using ember-cli, visit [http://www.ember-cli.com/](http://www.ember-cli.com/).
333 |
334 |
335 |
--------------------------------------------------------------------------------
/addon/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AltSchool/ember-cli-redux/603623f0dd35d7151ec1bdacf36858dfebd5d049/addon/.gitkeep
--------------------------------------------------------------------------------
/addon/lib/deserialize-from-ember.js:
--------------------------------------------------------------------------------
1 | const emberModelType = function(model) {
2 | var constructor = model.constructor.toString();
3 | var match = constructor.match(/model:(.+):/);
4 | if (!match || !match[1]) {throw `No model constructor found for ${constructor}`;}
5 | return match[1];
6 | };
7 |
8 | const deserializeFromEmber = function(o, depth = 10) {
9 | if(o && o.toJSON) {
10 | const type = emberModelType(o);
11 | return o.store && o.store.normalize ?
12 | o.store.normalize(type, o.toJSON({includeId:true})) :
13 | o.toJSON();
14 | } else if (Array.isArray(o)) {
15 | return o.reduce((acc, item) => {
16 | acc.push(deserializeFromEmber(item, depth - 1));
17 | return acc;
18 | }, []);
19 | } else if(o && o.then) {
20 | return o.toString();
21 | } else if (o instanceof Object) {
22 | return Object.keys(o).reduce((acc, key) => {
23 | var value = o.get ? o.get(key) : o[key];
24 | acc[key] = depth > 0 ? deserializeFromEmber(value, depth - 1) : value;
25 | return acc;
26 | }, {});
27 | } else {
28 | return o;
29 | }
30 | };
31 |
32 | export default deserializeFromEmber;
33 |
--------------------------------------------------------------------------------
/addon/lib/ember-logger-middleware.js:
--------------------------------------------------------------------------------
1 | import deserializeFromEmber from '../lib/deserialize-from-ember';
2 |
3 | const defaultOptions = {
4 | enabled: true
5 | };
6 |
7 | export default (options = defaultOptions) => store => next => action => {
8 | if (options.enabled) {
9 | console.group(action.type);
10 | console.info(
11 | `%c action`,
12 | `color: #03A9F4; font-weight: bold`,
13 | action
14 | );
15 | }
16 | let result = next(action);
17 | if (options.enabled) {
18 | console.log(
19 | `%c next state`,
20 | `color: #4CAF50; font-weight: bold`,
21 | deserializeFromEmber(store.getState())
22 | );
23 | console.groupEnd(action.type);
24 | }
25 | return result;
26 | };
27 |
--------------------------------------------------------------------------------
/addon/mixins/ember-redux.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 |
3 | export default Ember.Mixin.create({
4 | reduxStore: Ember.inject.service(),
5 |
6 | dispatch(action) {
7 | return this.get('reduxStore').dispatch(action);
8 | },
9 |
10 | dispatchAction(actionName, ...args) {
11 | return this.dispatch(this.action(actionName).apply(this, args));
12 | },
13 |
14 | getState(path) {
15 | return path ?
16 | this.get(`reduxStore.state.${path}`) :
17 | this.get('reduxStore.state');
18 | },
19 |
20 | action(actionName) {
21 | if (!this.reduxActions[actionName]) {throw new Error(`No redux action found for ${actionName}`);}
22 | return this.reduxActions[actionName].bind(this);
23 | },
24 | });
25 |
--------------------------------------------------------------------------------
/addon/services/redux-store.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | import redux from 'npm:redux';
3 | import thunk from 'npm:redux-thunk';
4 | import deserializeFromEmber from '../lib/deserialize-from-ember';
5 | import emberLoggerMiddleware from '../lib/ember-logger-middleware';
6 |
7 | const { createStore, applyMiddleware } = redux;
8 |
9 | const createStoreWithMiddleware = (middleware) =>
10 | applyMiddleware.apply(null, middleware)(createStore);
11 |
12 | const emberLogger = emberLoggerMiddleware();
13 |
14 | export default Ember.Service.extend({
15 | reducer: null, // Provided by the host app's app/reducers/index.js via app/services/redux-store
16 | state: Ember.computed.readOnly('_state'),
17 |
18 | middleware: [thunk, emberLogger],
19 |
20 | init() {
21 | this._store = createStoreWithMiddleware(this.get('middleware'))(this.get('reducer'));
22 |
23 | this._store.subscribe(() => {
24 | this.set('_state', this._store.getState());
25 | this.notifyPropertyChange('_state');
26 | });
27 |
28 | this.set('_state', this._store.getState());
29 | },
30 |
31 | stateString: Ember.computed('state', function() {
32 | return JSON.stringify(deserializeFromEmber(this.get('state')));
33 | }),
34 |
35 | dispatch(action) {
36 | this._store.dispatch(action);
37 | }
38 | });
39 |
--------------------------------------------------------------------------------
/app/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AltSchool/ember-cli-redux/603623f0dd35d7151ec1bdacf36858dfebd5d049/app/.gitkeep
--------------------------------------------------------------------------------
/app/mixins/ember-redux.js:
--------------------------------------------------------------------------------
1 | import EmberReduxMixin from 'ember-cli-redux/mixins/ember-redux';
2 | import redux from 'npm:redux';
3 | import thunk from 'npm:redux-thunk';
4 |
5 | export default EmberReduxMixin;
6 |
--------------------------------------------------------------------------------
/app/services/redux-store.js:
--------------------------------------------------------------------------------
1 | import ReduxStore from 'ember-cli-redux/services/redux-store';
2 | import reducer from '../reducers/index';
3 | import redux from 'npm:redux';
4 | import thunk from 'npm:redux-thunk';
5 |
6 | export default ReduxStore.extend({
7 | reducer
8 | });
9 |
--------------------------------------------------------------------------------
/blueprints/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "predef": [
3 | "console"
4 | ],
5 | "strict": false
6 | }
7 |
--------------------------------------------------------------------------------
/blueprints/ember-cli-redux/index.js:
--------------------------------------------------------------------------------
1 | var RSVP = require('rsvp');
2 |
3 | module.exports = {
4 | description: 'Install ember-cli-redux dependencies into your app.',
5 |
6 | // Allow `ember generate ember-cli-redux` to run without error
7 | normalizeEntityName: function() {},
8 |
9 | // Install redux and tools to the host app
10 | afterInstall: function() {
11 | return RSVP.all([
12 | this.addPackageToProject("redux", "^3.0.4"),
13 | this.addPackageToProject("redux-thunk", "^1.0.0"),
14 | this.addPackageToProject("redux-logger", "^2.3.1"),
15 | this.addPackageToProject("browserify", "12.0.1"),
16 | this.addPackageToProject("ember-browserify", "^1.1.4")
17 | ]);
18 | }
19 | };
20 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ember-cli-redux",
3 | "dependencies": {
4 | "ember": "~2.4.1",
5 | "ember-cli-shims": "0.1.0",
6 | "ember-cli-test-loader": "0.2.2",
7 | "ember-qunit-notifications": "0.1.0",
8 | "bind-polyfill": "~1.0.0"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/config/ember-try.js:
--------------------------------------------------------------------------------
1 | /*jshint node:true*/
2 | module.exports = {
3 | scenarios: [
4 | {
5 | name: 'default',
6 | bower: {
7 | dependencies: { }
8 | }
9 | },
10 | {
11 | name: 'ember-1-13',
12 | bower: {
13 | dependencies: {
14 | 'ember': '~1.13.0'
15 | },
16 | resolutions: {
17 | 'ember': '~1.13.0'
18 | }
19 | }
20 | },
21 | {
22 | name: 'ember-release',
23 | bower: {
24 | dependencies: {
25 | 'ember': 'components/ember#release'
26 | },
27 | resolutions: {
28 | 'ember': 'release'
29 | }
30 | }
31 | },
32 | {
33 | name: 'ember-beta',
34 | bower: {
35 | dependencies: {
36 | 'ember': 'components/ember#beta'
37 | },
38 | resolutions: {
39 | 'ember': 'beta'
40 | }
41 | }
42 | },
43 | {
44 | name: 'ember-canary',
45 | bower: {
46 | dependencies: {
47 | 'ember': 'components/ember#canary'
48 | },
49 | resolutions: {
50 | 'ember': 'canary'
51 | }
52 | }
53 | }
54 | ]
55 | };
56 |
--------------------------------------------------------------------------------
/config/environment.js:
--------------------------------------------------------------------------------
1 | /*jshint node:true*/
2 | 'use strict';
3 |
4 | module.exports = function(/* environment, appConfig */) {
5 | return { };
6 | };
7 |
--------------------------------------------------------------------------------
/ember-cli-build.js:
--------------------------------------------------------------------------------
1 | /*jshint node:true*/
2 | /* global require, module */
3 | var EmberAddon = require('ember-cli/lib/broccoli/ember-addon');
4 |
5 | module.exports = function(defaults) {
6 | var app = new EmberAddon(defaults, {
7 | // Add options here
8 | });
9 |
10 | /*
11 | This build file specifies the options for the dummy test app of this
12 | addon, located in `/tests/dummy`
13 | This build file does *not* influence how the addon or the app using it
14 | behave. You most likely want to be modifying `./index.js` or app's build file
15 | */
16 | app.import({ test: 'bower_components/bind-polyfill/index.js' });
17 | return app.toTree();
18 | };
19 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | /* jshint node: true */
2 | 'use strict';
3 |
4 | var path = require('path');
5 | var fs = require('fs');
6 |
7 | module.exports = {
8 | name: 'ember-cli-redux',
9 |
10 | // Turn on file watching for livereload support when this addon is npm linked.
11 | isDevelopingAddon: function() {
12 | try {
13 | fs.readlinkSync('node_modules/ember-cli-redux');
14 | console.log("Ember-cli-redux npm link detected.");
15 | } catch (e) {
16 | return false;
17 | }
18 | return true;
19 | }
20 | };
21 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ember-cli-redux",
3 | "version": "0.2.0",
4 | "description": "Ambitious state management for Ember",
5 | "directories": {
6 | "doc": "doc",
7 | "test": "tests"
8 | },
9 | "scripts": {
10 | "build": "ember build",
11 | "start": "ember server",
12 | "test": "ember try:testall"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "git+https://github.com/AltSchool/ember-cli-redux.git"
17 | },
18 | "engines": {
19 | "node": ">= 0.12.7"
20 | },
21 | "author": "Matthew Constantine ",
22 | "license": "MIT",
23 | "devDependencies": {
24 | "broccoli-asset-rev": "^2.2.0",
25 | "ember-ajax": "0.7.1",
26 | "ember-browserify": "^1.1.4",
27 | "ember-cli": "2.4.2",
28 | "ember-cli-app-version": "^1.0.0",
29 | "ember-cli-dependency-checker": "^1.2.0",
30 | "ember-cli-htmlbars": "^1.0.1",
31 | "ember-cli-htmlbars-inline-precompile": "^0.3.1",
32 | "ember-cli-inject-live-reload": "^1.3.1",
33 | "ember-cli-qunit": "^1.2.1",
34 | "ember-cli-release": "0.2.8",
35 | "ember-cli-sri": "^2.1.0",
36 | "ember-cli-uglify": "^1.2.0",
37 | "ember-data": "^2.4.0",
38 | "ember-disable-prototype-extensions": "^1.1.0",
39 | "ember-disable-proxy-controllers": "^1.0.1",
40 | "ember-export-application-global": "^1.0.4",
41 | "ember-load-initializers": "^0.5.0",
42 | "ember-resolver": "^2.0.3",
43 | "ember-try": "^0.1.2",
44 | "loader.js": "^4.0.0",
45 | "redux": "^3.0.4",
46 | "redux-logger": "^2.3.1",
47 | "redux-thunk": "^1.0.0"
48 | },
49 | "keywords": [
50 | "ember-addon",
51 | "redux",
52 | "flux"
53 | ],
54 | "dependencies": {
55 | "ember-cli-babel": "^5.1.5"
56 | },
57 | "ember-addon": {
58 | "configPath": "tests/dummy/config"
59 | },
60 | "main": "index.js",
61 | "bugs": {
62 | "url": "https://github.com/AltSchool/ember-cli-redux/issues"
63 | },
64 | "homepage": "https://github.com/AltSchool/ember-cli-redux#readme"
65 | }
66 |
--------------------------------------------------------------------------------
/testem.js:
--------------------------------------------------------------------------------
1 | /*jshint node:true*/
2 | module.exports = {
3 | "framework": "qunit",
4 | "test_page": "tests/index.html?hidepassed",
5 | "disable_watching": true,
6 | "launch_in_ci": [
7 | "PhantomJS"
8 | ],
9 | "launch_in_dev": [
10 | "PhantomJS",
11 | "Chrome"
12 | ]
13 | };
14 |
--------------------------------------------------------------------------------
/testem.json:
--------------------------------------------------------------------------------
1 | {
2 | "framework": "qunit",
3 | "test_page": "tests/index.html?hidepassed",
4 | "disable_watching": true,
5 | "launch_in_ci": [
6 | "PhantomJS"
7 | ],
8 | "launch_in_dev": [
9 | "PhantomJS",
10 | "Chrome"
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/tests/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "predef": [
3 | "document",
4 | "window",
5 | "location",
6 | "setTimeout",
7 | "$",
8 | "-Promise",
9 | "define",
10 | "console",
11 | "visit",
12 | "exists",
13 | "fillIn",
14 | "click",
15 | "keyEvent",
16 | "triggerEvent",
17 | "find",
18 | "findWithAssert",
19 | "wait",
20 | "DS",
21 | "andThen",
22 | "currentURL",
23 | "currentPath",
24 | "currentRouteName"
25 | ],
26 | "node": false,
27 | "browser": false,
28 | "boss": true,
29 | "curly": true,
30 | "debug": false,
31 | "devel": false,
32 | "eqeqeq": true,
33 | "evil": true,
34 | "forin": false,
35 | "immed": false,
36 | "laxbreak": false,
37 | "newcap": true,
38 | "noarg": true,
39 | "noempty": false,
40 | "nonew": false,
41 | "nomen": false,
42 | "onevar": false,
43 | "plusplus": false,
44 | "regexp": false,
45 | "undef": true,
46 | "sub": true,
47 | "strict": false,
48 | "white": false,
49 | "eqnull": true,
50 | "esnext": true,
51 | "unused": true
52 | }
53 |
--------------------------------------------------------------------------------
/tests/dummy/app/app.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | import Resolver from './resolver';
3 | import loadInitializers from 'ember-load-initializers';
4 | import config from './config/environment';
5 |
6 | let App;
7 |
8 | Ember.MODEL_FACTORY_INJECTIONS = true;
9 |
10 | App = Ember.Application.extend({
11 | modulePrefix: config.modulePrefix,
12 | podModulePrefix: config.podModulePrefix,
13 | Resolver
14 | });
15 |
16 | loadInitializers(App, config.modulePrefix);
17 |
18 | export default App;
19 |
--------------------------------------------------------------------------------
/tests/dummy/app/components/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AltSchool/ember-cli-redux/603623f0dd35d7151ec1bdacf36858dfebd5d049/tests/dummy/app/components/.gitkeep
--------------------------------------------------------------------------------
/tests/dummy/app/controllers/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AltSchool/ember-cli-redux/603623f0dd35d7151ec1bdacf36858dfebd5d049/tests/dummy/app/controllers/.gitkeep
--------------------------------------------------------------------------------
/tests/dummy/app/helpers/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AltSchool/ember-cli-redux/603623f0dd35d7151ec1bdacf36858dfebd5d049/tests/dummy/app/helpers/.gitkeep
--------------------------------------------------------------------------------
/tests/dummy/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Dummy
7 |
8 |
9 |
10 | {{content-for "head"}}
11 |
12 |
13 |
14 |
15 | {{content-for "head-footer"}}
16 |
17 |
18 | {{content-for "body"}}
19 |
20 |
21 |
22 |
23 | {{content-for "body-footer"}}
24 |
25 |
26 |
--------------------------------------------------------------------------------
/tests/dummy/app/models/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AltSchool/ember-cli-redux/603623f0dd35d7151ec1bdacf36858dfebd5d049/tests/dummy/app/models/.gitkeep
--------------------------------------------------------------------------------
/tests/dummy/app/reducers/index.js:
--------------------------------------------------------------------------------
1 | export default function counter(state = {count: 0}, action = {}) {
2 | switch (action.type) {
3 | case 'INCREMENT_COUNT':
4 | return {count: state.count + 1};
5 |
6 | default:
7 | return state;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/tests/dummy/app/resolver.js:
--------------------------------------------------------------------------------
1 | import Resolver from 'ember-resolver';
2 |
3 | export default Resolver;
4 |
--------------------------------------------------------------------------------
/tests/dummy/app/router.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | import config from './config/environment';
3 |
4 | const Router = Ember.Router.extend({
5 | location: config.locationType
6 | });
7 |
8 | Router.map(function() {
9 | });
10 |
11 | export default Router;
12 |
--------------------------------------------------------------------------------
/tests/dummy/app/routes/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AltSchool/ember-cli-redux/603623f0dd35d7151ec1bdacf36858dfebd5d049/tests/dummy/app/routes/.gitkeep
--------------------------------------------------------------------------------
/tests/dummy/app/styles/app.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AltSchool/ember-cli-redux/603623f0dd35d7151ec1bdacf36858dfebd5d049/tests/dummy/app/styles/app.css
--------------------------------------------------------------------------------
/tests/dummy/app/templates/application.hbs:
--------------------------------------------------------------------------------
1 |