├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── dist ├── extensible-duck.js ├── extensible-duck.js.map ├── extensible-duck.min.js └── extensible-duck.min.js.map ├── gulpfile.js ├── package.json ├── src └── extensible-duck.js ├── test ├── .eslintrc ├── runner.html ├── setup │ ├── .globals.json │ ├── browser.js │ ├── node.js │ └── setup.js └── unit │ └── extensible-duck.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": ["transform-object-rest-spread"] 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | root = true; 4 | 5 | [*] 6 | # Ensure there's no lingering whitespace 7 | trim_trailing_whitespace = true 8 | # Ensure a newline at the end of each file 9 | insert_final_newline = true 10 | 11 | [*.js] 12 | # Unix-style newlines 13 | end_of_line = lf 14 | charset = utf-8 15 | indent_style = space 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 6, 4 | "sourceType": "module", 5 | "ecmaFeatures": { 6 | "experimentalObjectRestSpread": true 7 | } 8 | }, 9 | "rules": {}, 10 | "env": { 11 | "browser": true, 12 | "node": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | bower_components 27 | coverage 28 | tmp 29 | 30 | # Users Environment Variables 31 | .lock-wscript 32 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | - "stable" 5 | sudo: false 6 | script: "gulp coverage" 7 | after_success: 8 | - npm install -g codeclimate-test-reporter 9 | - codeclimate-test-reporter < coverage/lcov.info 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### [1.0.0](https://github.com/investtools/extensible-duck/releases/tag/v1.0.0) 2 | 3 | - The first release 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Andre Aizim Kelmanosn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # extensible-duck 2 | 3 | extensible-duck is an implementation of the [Ducks proposal](https://github.com/erikras/ducks-modular-redux). With this library you can create reusable and extensible ducks. 4 | 5 | [![Travis build status](http://img.shields.io/travis/investtools/extensible-duck.svg?style=flat)](https://travis-ci.org/investtools/extensible-duck) 6 | [![Code Climate](https://codeclimate.com/github/investtools/extensible-duck/badges/gpa.svg)](https://codeclimate.com/github/investtools/extensible-duck) 7 | [![Test Coverage](https://codeclimate.com/github/investtools/extensible-duck/badges/coverage.svg)](https://codeclimate.com/github/investtools/extensible-duck) 8 | [![Dependency Status](https://david-dm.org/investtools/extensible-duck.svg)](https://david-dm.org/investtools/extensible-duck) 9 | [![devDependency Status](https://david-dm.org/investtools/extensible-duck/dev-status.svg)](https://david-dm.org/investtools/extensible-duck#info=devDependencies) 10 | ![](http://img.badgesize.io/investtools/extensible-duck/master/dist/extensible-duck.min.js?compression=gzip) 11 | 12 | 13 | 14 | - [Basic Usage](#basic-usage) 15 | - [Constructor Arguments](#constructor-arguments) 16 | - [Duck Accessors](#duck-accessors) 17 | - [Defining the Reducer](#defining-the-reducer) 18 | - [Defining the Creators](#defining-the-creators) 19 | - [Defining the sagas](#defining-the-sagas) 20 | - [Defining the Initial State](#defining-the-initial-state) 21 | - [Defining the Selectors](#defining-the-selectors) 22 | - [Defining the Types](#defining-the-types) 23 | - [Defining the Constants](#defining-the-constants) 24 | - [Creating Reusable Ducks](#creating-reusable-ducks) 25 | - [Extending Ducks](#extending-ducks) 26 | - [Creating Reusable Duck Extensions](#creating-reusable-duck-extensions) 27 | - [Creating Ducks with selectors](#creating-ducks-with-selectors) 28 | 29 | 30 | 31 | ## Basic Usage 32 | 33 | ```js 34 | // widgetsDuck.js 35 | 36 | import Duck from 'extensible-duck' 37 | 38 | export default new Duck({ 39 | namespace: 'my-app', store: 'widgets', 40 | types: ['LOAD', 'CREATE', 'UPDATE', 'REMOVE'], 41 | initialState: {}, 42 | reducer: (state, action, duck) => { 43 | switch(action.type) { 44 | // do reducer stuff 45 | default: return state 46 | } 47 | }, 48 | selectors: { 49 | root: state => state 50 | }, 51 | creators: (duck) => ({ 52 | loadWidgets: () => ({ type: duck.types.LOAD }), 53 | createWidget: widget => ({ type: duck.types.CREATE, widget }), 54 | updateWidget: widget => ({ type: duck.types.UPDATE, widget }), 55 | removeWidget: widget => ({ type: duck.types.REMOVE, widget }) 56 | }) 57 | }) 58 | ``` 59 | 60 | ```js 61 | // reducers.js 62 | 63 | import { combineReducers } from 'redux' 64 | import widgetDuck from './widgetDuck' 65 | 66 | export default combineReducers({ [widgetDuck.store]: widgetDuck.reducer }) 67 | ``` 68 | 69 | ### Constructor Arguments 70 | 71 | const { namespace, store, types, consts, initialState, creators } = options 72 | 73 | | Name | Description | Type | Example | 74 | |--------------|---------------------------------------------------------|--------------------------------|---------------------------------------------| 75 | | namespace | Used as a prefix for the types | String | `'my-app'` | 76 | | store | Used as a prefix for the types and as a redux state key | String | `'widgets'` | 77 | | storePath | Object path of the store from root infinal redux state. Defaults to the [duck.store] value. Can be used to define duck store location in nested state | String | `'foo.bar'` | 78 | | types | List of action types | Array | `[ 'CREATE', 'UPDATE' ]` | 79 | | consts | Constants you may need to declare | Object of Arrays | `{ statuses: [ 'LOADING', 'LOADED' ] }` | 80 | | initialState | State passed to the reducer when the state is undefined | Anything | `{}` | 81 | | reducer | Action reducer | function(state, action, duck) | `(state, action, duck) => { return state }` | 82 | | creators | Action creators | function(duck) | `duck => ({ type: types.CREATE })` | 83 | | sagas | Action sagas | function(duck) | `duck => ({ fetchData: function* { yield ... }` | 84 | | takes | Action takes | function(duck) | `duck => ([ takeEvery(types.FETCH, sagas.fetchData) ])` | 85 | | selectors | state selectors | Object of functions
or
function(duck) | `{ root: state => state}`
or
`duck => ({ root: state => state })` | 86 | 87 | ### Duck Accessors 88 | 89 | * duck.store 90 | * duck.storePath 91 | * duck.reducer 92 | * duck.creators 93 | * duck.sagas 94 | * duck.takes 95 | * duck.selectors 96 | * duck.types 97 | * for each const, duck.\ 98 | 99 | ### Helper functions 100 | 101 | * **constructLocalized(selectors)**: maps selectors syntax from `(globalStore) => selectorBody` into `(localStore, globalStore) => selectorBody`. `localStore` is derived from `globalStore` on every selector execution using `duck.storage` key. Use to simplify selectors syntax when used in tandem with reduxes' `combineReducers` to bind the duck to a dedicated state part ([example](#creating-ducks-with-selectors)). If defined will use the duck.storePath value to determine the localized state in deeply nested redux state trees. 102 | 103 | ### Defining the Reducer 104 | 105 | While a plain vanilla reducer would be defined by something like this: 106 | 107 | ```js 108 | function reducer(state={}, action) { 109 | switch (action.type) { 110 | // ... 111 | default: 112 | return state 113 | } 114 | } 115 | ``` 116 | 117 | Here the reducer has two slight differences: 118 | 119 | * It receives the duck itself as the third argument 120 | * It doesn't define the initial state (see [Defining the Initial State](#defining-the-initial-state)) 121 | 122 | ```js 123 | new Duck({ 124 | // ... 125 | reducer: (state, action, duck) => { 126 | switch (action.type) { 127 | // ... 128 | default: 129 | return state 130 | } 131 | } 132 | }) 133 | ``` 134 | 135 | With the `duck` argument you can access the types, the constants, etc (see [Duck Accessors](#duck-accessors)). 136 | 137 | ### Defining the Creators 138 | 139 | While plain vanilla creators would be defined by something like this: 140 | 141 | ```js 142 | export function createWidget(widget) { 143 | return { type: CREATE, widget } 144 | } 145 | 146 | // Using thunk 147 | export function updateWidget(widget) { 148 | return dispatch => { 149 | dispatch({ type: UPDATE, widget }) 150 | } 151 | } 152 | ``` 153 | 154 | With extensible-duck you define it as an Object of functions: 155 | 156 | ```js 157 | export default new Duck({ 158 | // ... 159 | creators: { 160 | createWidget: widget => ({ type: 'CREATE', widget }) 161 | 162 | // Using thunk 163 | updateWidget: widget => dispatch => { 164 | dispatch({ type: 'UPDATE', widget }) 165 | } 166 | } 167 | }) 168 | ``` 169 | 170 | If you need to access any duck attribute, you can define a function that returns the Object of functions: 171 | 172 | ```js 173 | export default new Duck({ 174 | // ... 175 | types: [ 'CREATE' ], 176 | creators: (duck) => ({ 177 | createWidget: widget => ({ type: duck.types.CREATE, widget }) 178 | }) 179 | }) 180 | ``` 181 | 182 | ### Defining the Sagas 183 | 184 | While plain vanilla creators would be defined by something like this: 185 | 186 | ```js 187 | function* fetchData() { 188 | try{ 189 | yield put({ type: reducerDuck.types.FETCH_PENDING }) 190 | const payload = yield call(Get, 'data') 191 | yield put({ 192 | type: reducerDuck.types.FETCH_FULFILLED, 193 | payload 194 | }) 195 | } catch(err) { 196 | yield put({ 197 | type: reducerDuck.types.FETCH_FAILURE, 198 | err 199 | }) 200 | } 201 | } 202 | 203 | // Defining observer 204 | export default [ takeEvery(reducerDuck.types.FETCH, fetchData) ] 205 | ``` 206 | 207 | With extensible-duck you define it as an Object of functions accessing any duck attribute: 208 | 209 | ```js 210 | export default new Duck({ 211 | // ... 212 | sagas: { 213 | fetchData: function* (duck) { 214 | try{ 215 | yield put({ type: duck.types.FETCH_PENDING }) 216 | const payload = yield call(Get, 'data') 217 | yield put({ 218 | type: duck.types.FETCH_FULFILLED, 219 | payload 220 | }) 221 | } catch(err) { 222 | yield put({ 223 | type: duck.types.FETCH_FAILURE, 224 | err 225 | }) 226 | } 227 | } 228 | }, 229 | // Defining observer 230 | takes: (duck) => ([ 231 | takeEvery(duck.types.FETCH, duck.sagas.fetchData) 232 | ]) 233 | }) 234 | ``` 235 | 236 | ### Defining the Initial State 237 | 238 | Usually the initial state is declared within the the reducer declaration, just like bellow: 239 | 240 | ```js 241 | function myReducer(state = {someDefaultValue}, action) { 242 | // ... 243 | } 244 | ``` 245 | 246 | With extensible-duck you define it separately: 247 | 248 | ```js 249 | export default new Duck({ 250 | // ... 251 | initialState: {someDefaultValue} 252 | }) 253 | ``` 254 | 255 | If you need to access the [types](#defining-the-types) or [constants](#defining-the-constants), you can define this way: 256 | 257 | ```js 258 | export default new Duck({ 259 | // ... 260 | consts: { statuses: ['NEW'] }, 261 | initialState: ({ statuses }) => ({ status: statuses.NEW }) 262 | }) 263 | ``` 264 | 265 | ### Defining the Selectors 266 | 267 | Simple selectors: 268 | 269 | ```js 270 | export default new Duck({ 271 | // ... 272 | selectors: { 273 | shopItems: state => state.shop.items 274 | } 275 | }) 276 | 277 | ``` 278 | 279 | Composed selectors: 280 | 281 | ```js 282 | export default new Duck({ 283 | // ... 284 | selectors: { 285 | shopItems: state => state.shop.items, 286 | subtotal: new Duck.Selector(selectors => state => 287 | selectors.shopItems(state).reduce((acc, item) => acc + item.value, 0) 288 | ) 289 | } 290 | }) 291 | ``` 292 | 293 | Using with [Reselect](https://github.com/reactjs/reselect): 294 | 295 | ```js 296 | export default new Duck({ 297 | // ... 298 | selectors: { 299 | shopItems: state => state.shop.items, 300 | subtotal: new Duck.Selector(selectors => 301 | createSelector( 302 | selectors.shopItems, 303 | items => items.reduce((acc, item) => acc + item.value, 0) 304 | ) 305 | ) 306 | } 307 | }) 308 | ``` 309 | 310 | Selectors with duck reference: 311 | 312 | ```js 313 | export default new Duck({ 314 | // ... 315 | selectors: (duck) => ({ 316 | shopItems: state => state.shop.items, 317 | addedItems: new Duck.Selector(selectors => 318 | createSelector( 319 | selectors.shopItems, 320 | items => { 321 | const out = []; 322 | items.forEach(item => { 323 | if (-1 === duck.initialState.shop.items.indexOf(item)) { 324 | out.push(item); 325 | } 326 | }); 327 | return out; 328 | } 329 | ) 330 | ) 331 | }) 332 | }) 333 | ``` 334 | 335 | ### Defining the Types 336 | 337 | ```js 338 | export default new Duck({ 339 | namespace: 'my-app', store: 'widgets', 340 | // ... 341 | types: [ 342 | 'CREATE', // myDuck.types.CREATE = "my-app/widgets/CREATE" 343 | 'RETREIVE', // myDuck.types.RETREIVE = "my-app/widgets/RETREIVE" 344 | 'UPDATE', // myDuck.types.UPDATE = "my-app/widgets/UPDATE" 345 | 'DELETE', // myDuck.types.DELETE = "my-app/widgets/DELETE" 346 | ] 347 | } 348 | ``` 349 | 350 | ### Defining the Constants 351 | 352 | ```js 353 | export default new Duck({ 354 | // ... 355 | consts: { 356 | statuses: ['NEW'], // myDuck.statuses = { NEW: "NEW" } 357 | fooBar: [ 358 | 'FOO', // myDuck.fooBar.FOO = "FOO" 359 | 'BAR' // myDuck.fooBar.BAR = "BAR" 360 | ] 361 | } 362 | } 363 | ``` 364 | 365 | ## Creating Reusable Ducks 366 | 367 | This example uses [redux-promise-middleware](https://github.com/pburtchaell/redux-promise-middleware) 368 | and [axios](https://github.com/mzabriskie/axios). 369 | 370 | ```js 371 | // remoteObjDuck.js 372 | 373 | import Duck from 'extensible-duck' 374 | import axios from 'axios' 375 | 376 | export default function createDuck({ namespace, store, path, initialState={} }) { 377 | return new Duck({ 378 | namespace, store, 379 | 380 | consts: { statuses: [ 'NEW', 'LOADING', 'READY', 'SAVING', 'SAVED' ] }, 381 | 382 | types: [ 383 | 'UPDATE', 384 | 'FETCH', 'FETCH_PENDING', 'FETCH_FULFILLED', 385 | 'POST', 'POST_PENDING', 'POST_FULFILLED', 386 | ], 387 | 388 | reducer: (state, action, { types, statuses, initialState }) => { 389 | switch(action.type) { 390 | case types.UPDATE: 391 | return { ...state, obj: { ...state.obj, ...action.payload } } 392 | case types.FETCH_PENDING: 393 | return { ...state, status: statuses.LOADING } 394 | case types.FETCH_FULFILLED: 395 | return { ...state, obj: action.payload.data, status: statuses.READY } 396 | case types.POST_PENDING: 397 | case types.PATCH_PENDING: 398 | return { ...state, status: statuses.SAVING } 399 | case types.POST_FULFILLED: 400 | case types.PATCH_FULFILLED: 401 | return { ...state, status: statuses.SAVED } 402 | default: 403 | return state 404 | } 405 | }, 406 | 407 | creators: ({ types }) => ({ 408 | update: (fields) => ({ type: types.UPDATE, payload: fields }), 409 | get: (id) => ({ type: types.FETCH, payload: axios.get(`${path}/${id}`), 410 | post: () => ({ type: types.POST, payload: axios.post(path, obj) }), 411 | patch: () => ({ type: types.PATCH, payload: axios.patch(`${path}/${id}`, obj) }) 412 | }), 413 | 414 | initialState: ({ statuses }) => ({ obj: initialState || {}, status: statuses.NEW, entities: [] }) 415 | }) 416 | } 417 | ``` 418 | 419 | ```js 420 | // usersDuck.js 421 | 422 | import createDuck from './remoteObjDuck' 423 | 424 | export default createDuck({ namespace: 'my-app', store: 'user', path: '/users' }) 425 | ``` 426 | 427 | ```js 428 | // reducers.js 429 | 430 | import { combineReducers } from 'redux' 431 | import userDuck from './userDuck' 432 | 433 | export default combineReducers({ [userDuck.store]: userDuck.reducer }) 434 | ``` 435 | 436 | ## Extending Ducks 437 | 438 | This example is based on the previous one. 439 | 440 | ```js 441 | // usersDuck.js 442 | 443 | import createDuck from './remoteObjDuck.js' 444 | 445 | export default createDuck({ namespace: 'my-app',store: 'user', path: '/users' }).extend({ 446 | types: [ 'RESET' ], 447 | reducer: (state, action, { types, statuses, initialState }) => { 448 | switch(action.type) { 449 | case types.RESET: 450 | return { ...initialState, obj: { ...initialState.obj, ...action.payload } } 451 | default: 452 | return state 453 | }, 454 | creators: ({ types }) => ({ 455 | reset: (fields) => ({ type: types.RESET, payload: fields }), 456 | }) 457 | }) 458 | ``` 459 | 460 | ## Creating Reusable Duck Extensions 461 | 462 | This example is a refactor of the previous one. 463 | 464 | ```js 465 | // resetDuckExtension.js 466 | 467 | export default { 468 | types: [ 'RESET' ], 469 | reducer: (state, action, { types, statuses, initialState }) => { 470 | switch(action.type) { 471 | case types.RESET: 472 | return { ...initialState, obj: { ...initialState.obj, ...action.payload } } 473 | default: 474 | return state 475 | }, 476 | creators: ({ types }) => ({ 477 | reset: (fields) => ({ type: types.RESET, payload: fields }), 478 | }) 479 | } 480 | ``` 481 | 482 | ```js 483 | // userDuck.js 484 | 485 | import createDuck from './remoteObjDuck' 486 | import reset from './resetDuckExtension' 487 | 488 | export default createDuck({ namespace: 'my-app',store: 'user', path: '/users' }).extend(reset) 489 | ``` 490 | 491 | 492 | ## Creating Ducks with selectors 493 | 494 | Selectors help in providing performance optimisations when used with libraries such as React-Redux, Preact-Redux etc. 495 | 496 | ```js 497 | // Duck.js 498 | 499 | import Duck, { constructLocalized } from 'extensible-duck' 500 | 501 | export default new Duck({ 502 | store: 'fruits', 503 | initialState: { 504 | items: [ 505 | { name: 'apple', value: 1.2 }, 506 | { name: 'orange', value: 0.95 } 507 | ] 508 | }, 509 | reducer: (state, action, duck) => { 510 | switch(action.type) { 511 | // do reducer stuff 512 | default: return state 513 | } 514 | }, 515 | selectors: constructLocalized({ 516 | items: state => state.items, // gets the items from state 517 | subTotal: new Duck.Selector(selectors => state => 518 | // Get another derived state reusing previous selector. In this case items selector 519 | // Can compose multiple such selectors if using library like reselect. Recommended! 520 | // Note: The order of the selectors definitions matters 521 | selectors 522 | .items(state) 523 | .reduce((computedTotal, item) => computedTotal + item.value, 0) 524 | ) 525 | }) 526 | }) 527 | ``` 528 | 529 | ```js 530 | // reducers.js 531 | 532 | import { combineReducers } from 'redux' 533 | import Duck from './Duck' 534 | 535 | export default combineReducers({ [Duck.store]: Duck.reducer }) 536 | ``` 537 | 538 | ```js 539 | // HomeView.js 540 | import React from 'react' 541 | import Duck from './Duck' 542 | 543 | @connect(state => ({ 544 | items: Duck.selectors.items(state), 545 | subTotal: Duck.selectors.subTotal(state) 546 | })) 547 | export default class HomeView extends React.Component { 548 | render(){ 549 | // make use of sliced state here in props 550 | ... 551 | } 552 | } 553 | ``` 554 | -------------------------------------------------------------------------------- /dist/extensible-duck.js: -------------------------------------------------------------------------------- 1 | (function webpackUniversalModuleDefinition(root, factory) { 2 | if(typeof exports === 'object' && typeof module === 'object') 3 | module.exports = factory(); 4 | else if(typeof define === 'function' && define.amd) 5 | define([], factory); 6 | else if(typeof exports === 'object') 7 | exports["Duck"] = factory(); 8 | else 9 | root["Duck"] = factory(); 10 | })(this, function() { 11 | return /******/ (function(modules) { // webpackBootstrap 12 | /******/ // The module cache 13 | /******/ var installedModules = {}; 14 | /******/ 15 | /******/ // The require function 16 | /******/ function __webpack_require__(moduleId) { 17 | /******/ 18 | /******/ // Check if module is in cache 19 | /******/ if(installedModules[moduleId]) { 20 | /******/ return installedModules[moduleId].exports; 21 | /******/ } 22 | /******/ // Create a new module (and put it into the cache) 23 | /******/ var module = installedModules[moduleId] = { 24 | /******/ i: moduleId, 25 | /******/ l: false, 26 | /******/ exports: {} 27 | /******/ }; 28 | /******/ 29 | /******/ // Execute the module function 30 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 31 | /******/ 32 | /******/ // Flag the module as loaded 33 | /******/ module.l = true; 34 | /******/ 35 | /******/ // Return the exports of the module 36 | /******/ return module.exports; 37 | /******/ } 38 | /******/ 39 | /******/ 40 | /******/ // expose the modules object (__webpack_modules__) 41 | /******/ __webpack_require__.m = modules; 42 | /******/ 43 | /******/ // expose the module cache 44 | /******/ __webpack_require__.c = installedModules; 45 | /******/ 46 | /******/ // define getter function for harmony exports 47 | /******/ __webpack_require__.d = function(exports, name, getter) { 48 | /******/ if(!__webpack_require__.o(exports, name)) { 49 | /******/ Object.defineProperty(exports, name, { 50 | /******/ configurable: false, 51 | /******/ enumerable: true, 52 | /******/ get: getter 53 | /******/ }); 54 | /******/ } 55 | /******/ }; 56 | /******/ 57 | /******/ // getDefaultExport function for compatibility with non-harmony modules 58 | /******/ __webpack_require__.n = function(module) { 59 | /******/ var getter = module && module.__esModule ? 60 | /******/ function getDefault() { return module['default']; } : 61 | /******/ function getModuleExports() { return module; }; 62 | /******/ __webpack_require__.d(getter, 'a', getter); 63 | /******/ return getter; 64 | /******/ }; 65 | /******/ 66 | /******/ // Object.prototype.hasOwnProperty.call 67 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 68 | /******/ 69 | /******/ // __webpack_public_path__ 70 | /******/ __webpack_require__.p = ""; 71 | /******/ 72 | /******/ // Load entry module and return exports 73 | /******/ return __webpack_require__(__webpack_require__.s = 0); 74 | /******/ }) 75 | /************************************************************************/ 76 | /******/ ([ 77 | /* 0 */ 78 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 79 | 80 | "use strict"; 81 | Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); 82 | /* harmony export (immutable) */ __webpack_exports__["constructLocalized"] = constructLocalized; 83 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "constructLocalised", function() { return constructLocalized; }); 84 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Selector", function() { return Selector; }); 85 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 86 | 87 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 88 | 89 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; 90 | 91 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 92 | 93 | function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } 94 | 95 | function typeValue(namespace, store, type) { 96 | return namespace + '/' + store + '/' + type; 97 | } 98 | 99 | function zipObject(keys, values) { 100 | if (arguments.length == 1) { 101 | values = keys[1]; 102 | keys = keys[0]; 103 | } 104 | 105 | var result = {}; 106 | var i = 0; 107 | 108 | for (i; i < keys.length; i += 1) { 109 | result[keys[i]] = values[i]; 110 | } 111 | 112 | return result; 113 | } 114 | 115 | function buildTypes(namespace, store, types) { 116 | return zipObject(types, types.map(function (type) { 117 | return typeValue(namespace, store, type); 118 | })); 119 | } 120 | 121 | function isObject(obj) { 122 | return obj !== null && (typeof obj === 'undefined' ? 'undefined' : _typeof(obj)) === 'object'; 123 | } 124 | 125 | function isFunction(func) { 126 | return func !== null && typeof func === 'function'; 127 | } 128 | 129 | function isUndefined(value) { 130 | return typeof value === 'undefined' || value === undefined; 131 | } 132 | 133 | function isPlainObject(obj) { 134 | return isObject(obj) && (obj.constructor === Object || // obj = {} 135 | obj.constructor === undefined) // obj = Object.create(null) 136 | ; 137 | } 138 | 139 | function mergeDeep(target) { 140 | for (var _len = arguments.length, sources = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { 141 | sources[_key - 1] = arguments[_key]; 142 | } 143 | 144 | if (!sources.length) return target; 145 | var source = sources.shift(); 146 | 147 | if (Array.isArray(target)) { 148 | if (Array.isArray(source)) { 149 | var _target; 150 | 151 | (_target = target).push.apply(_target, _toConsumableArray(source)); 152 | } else { 153 | target.push(source); 154 | } 155 | } else if (isPlainObject(target)) { 156 | if (isPlainObject(source)) { 157 | var _iteratorNormalCompletion = true; 158 | var _didIteratorError = false; 159 | var _iteratorError = undefined; 160 | 161 | try { 162 | for (var _iterator = Object.keys(source)[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { 163 | var key = _step.value; 164 | 165 | if (!target[key]) { 166 | target[key] = source[key]; 167 | } else { 168 | mergeDeep(target[key], source[key]); 169 | } 170 | } 171 | } catch (err) { 172 | _didIteratorError = true; 173 | _iteratorError = err; 174 | } finally { 175 | try { 176 | if (!_iteratorNormalCompletion && _iterator.return) { 177 | _iterator.return(); 178 | } 179 | } finally { 180 | if (_didIteratorError) { 181 | throw _iteratorError; 182 | } 183 | } 184 | } 185 | } else { 186 | throw new Error('Cannot merge object with non-object'); 187 | } 188 | } else { 189 | target = source; 190 | } 191 | 192 | return mergeDeep.apply(undefined, [target].concat(sources)); 193 | } 194 | 195 | function assignDefaults(options) { 196 | return _extends({}, options, { 197 | consts: options.consts || {}, 198 | sagas: options.sagas || function () { 199 | return {}; 200 | }, 201 | takes: options.takes || function () { 202 | return []; 203 | }, 204 | creators: options.creators || function () { 205 | return {}; 206 | }, 207 | selectors: options.selectors || {}, 208 | types: options.types || [] 209 | }); 210 | } 211 | 212 | function injectDuck(input, duck) { 213 | if (input instanceof Function) { 214 | return input(duck); 215 | } else { 216 | return input; 217 | } 218 | } 219 | 220 | function getLocalizedState(globalState, duck) { 221 | var localizedState = void 0; 222 | 223 | if (duck.storePath) { 224 | var segments = [].concat(duck.storePath.split('.'), duck.store); 225 | localizedState = segments.reduce(function getSegment(acc, segment) { 226 | if (!acc[segment]) { 227 | throw Error('state does not contain reducer at storePath ' + segments.join('.')); 228 | } 229 | return acc[segment]; 230 | }, globalState); 231 | } else { 232 | localizedState = globalState[duck.store]; 233 | } 234 | 235 | return localizedState; 236 | } 237 | 238 | function constructLocalized(selectors) { 239 | var derivedSelectors = deriveSelectors(selectors); 240 | return function (duck) { 241 | var localizedSelectors = {}; 242 | Object.keys(derivedSelectors).forEach(function (key) { 243 | var selector = derivedSelectors[key]; 244 | localizedSelectors[key] = function (globalState) { 245 | return selector(getLocalizedState(globalState, duck), globalState); 246 | }; 247 | }); 248 | return localizedSelectors; 249 | }; 250 | } 251 | 252 | // An alias for those who do not use the above spelling. 253 | 254 | 255 | /** 256 | * Helper utility to assist in composing the selectors. 257 | * Previously defined selectors can be used to derive future selectors. 258 | * 259 | * @param {object} selectors 260 | * @returns 261 | */ 262 | function deriveSelectors(selectors) { 263 | var composedSelectors = {}; 264 | Object.keys(selectors).forEach(function (key) { 265 | var selector = selectors[key]; 266 | if (selector instanceof Selector) { 267 | composedSelectors[key] = function () { 268 | return (composedSelectors[key] = selector.extractFunction(composedSelectors)).apply(undefined, arguments); 269 | }; 270 | } else { 271 | composedSelectors[key] = selector; 272 | } 273 | }); 274 | return composedSelectors; 275 | } 276 | 277 | var Duck = function () { 278 | function Duck(options) { 279 | var _this = this; 280 | 281 | _classCallCheck(this, Duck); 282 | 283 | options = assignDefaults(options); 284 | var _options = options, 285 | namespace = _options.namespace, 286 | store = _options.store, 287 | storePath = _options.storePath, 288 | types = _options.types, 289 | consts = _options.consts, 290 | initialState = _options.initialState, 291 | creators = _options.creators, 292 | selectors = _options.selectors, 293 | sagas = _options.sagas, 294 | takes = _options.takes; 295 | 296 | this.options = options; 297 | Object.keys(consts).forEach(function (name) { 298 | _this[name] = zipObject(consts[name], consts[name]); 299 | }); 300 | 301 | this.store = store; 302 | this.storePath = storePath; 303 | this.types = buildTypes(namespace, store, types); 304 | this.initialState = isFunction(initialState) ? initialState(this) : initialState; 305 | this.reducer = this.reducer.bind(this); 306 | this.selectors = deriveSelectors(injectDuck(selectors, this)); 307 | this.creators = creators(this); 308 | this.sagas = sagas(this); 309 | this.takes = takes(this); 310 | } 311 | 312 | _createClass(Duck, [{ 313 | key: 'reducer', 314 | value: function reducer(state, action) { 315 | if (isUndefined(state)) { 316 | state = this.initialState; 317 | } 318 | return this.options.reducer(state, action, this); 319 | } 320 | }, { 321 | key: 'extend', 322 | value: function extend(options) { 323 | var _this2 = this; 324 | 325 | if (isFunction(options)) { 326 | options = options(this); 327 | } 328 | options = assignDefaults(options); 329 | var parent = this.options; 330 | var initialState = void 0; 331 | if (isFunction(options.initialState)) { 332 | initialState = function initialState(duck) { 333 | return options.initialState(duck, _this2.initialState); 334 | }; 335 | } else if (isUndefined(options.initialState)) { 336 | initialState = parent.initialState; 337 | } else { 338 | initialState = options.initialState; 339 | } 340 | return new Duck(_extends({}, parent, options, { 341 | initialState: initialState, 342 | consts: mergeDeep({}, parent.consts, options.consts), 343 | sagas: function sagas(duck) { 344 | var parentSagas = parent.sagas(duck); 345 | return _extends({}, parentSagas, options.sagas(duck, parentSagas)); 346 | }, 347 | takes: function takes(duck) { 348 | var parentTakes = parent.takes(duck); 349 | return [].concat(_toConsumableArray(parentTakes), _toConsumableArray(options.takes(duck, parentTakes))); 350 | }, 351 | creators: function creators(duck) { 352 | var parentCreators = parent.creators(duck); 353 | return _extends({}, parentCreators, options.creators(duck, parentCreators)); 354 | }, 355 | selectors: function selectors(duck) { 356 | return _extends({}, injectDuck(parent.selectors, duck), injectDuck(options.selectors, duck)); 357 | }, 358 | types: [].concat(_toConsumableArray(parent.types), _toConsumableArray(options.types)), 359 | reducer: function reducer(state, action, duck) { 360 | state = parent.reducer(state, action, duck); 361 | if (isUndefined(options.reducer)) { 362 | return state; 363 | } else { 364 | return options.reducer(state, action, duck); 365 | } 366 | } 367 | })); 368 | } 369 | }]); 370 | 371 | return Duck; 372 | }(); 373 | 374 | /* harmony default export */ __webpack_exports__["default"] = (Duck); 375 | 376 | 377 | var Selector = function () { 378 | function Selector(func) { 379 | _classCallCheck(this, Selector); 380 | 381 | this.func = func; 382 | } 383 | 384 | _createClass(Selector, [{ 385 | key: 'extractFunction', 386 | value: function extractFunction(selectors) { 387 | return this.func(selectors); 388 | } 389 | }]); 390 | 391 | return Selector; 392 | }(); 393 | 394 | Duck.Selector = Selector; 395 | 396 | /***/ }) 397 | /******/ ]); 398 | }); 399 | //# sourceMappingURL=extensible-duck.js.map -------------------------------------------------------------------------------- /dist/extensible-duck.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack:///webpack/universalModuleDefinition","webpack:///webpack/bootstrap 880a1df6fb2495bb3587","webpack:///./src/extensible-duck.js"],"names":["typeValue","namespace","store","type","zipObject","keys","values","arguments","length","result","i","buildTypes","types","map","isObject","obj","isFunction","func","isUndefined","value","undefined","isPlainObject","constructor","Object","mergeDeep","target","sources","source","shift","Array","isArray","push","key","Error","assignDefaults","options","consts","sagas","takes","creators","selectors","injectDuck","input","duck","Function","getLocalizedState","globalState","localizedState","storePath","segments","concat","split","reduce","getSegment","acc","segment","join","constructLocalized","derivedSelectors","deriveSelectors","localizedSelectors","forEach","selector","composedSelectors","Selector","extractFunction","Duck","initialState","name","reducer","bind","state","action","parent","parentSagas","parentTakes","parentCreators"],"mappings":"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","file":"extensible-duck.js","sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"Duck\"] = factory();\n\telse\n\t\troot[\"Duck\"] = factory();\n})(this, function() {\nreturn \n\n\n// WEBPACK FOOTER //\n// webpack/universalModuleDefinition"," \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, {\n \t\t\t\tconfigurable: false,\n \t\t\t\tenumerable: true,\n \t\t\t\tget: getter\n \t\t\t});\n \t\t}\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 0);\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap 880a1df6fb2495bb3587","function typeValue(namespace, store, type) {\n return `${namespace}/${store}/${type}`\n}\n\nfunction zipObject(keys, values) {\n if (arguments.length == 1) {\n values = keys[1]\n keys = keys[0]\n }\n\n var result = {}\n var i = 0\n\n for (i; i < keys.length; i += 1) {\n result[keys[i]] = values[i]\n }\n\n return result\n}\n\nfunction buildTypes(namespace, store, types) {\n return zipObject(types, types.map(type => typeValue(namespace, store, type)))\n}\n\nfunction isObject(obj) {\n return obj !== null && typeof obj === 'object'\n}\n\nfunction isFunction(func) {\n return func !== null && typeof func === 'function'\n}\n\nfunction isUndefined(value) {\n return typeof value === 'undefined' || value === undefined\n}\n\nfunction isPlainObject(obj) {\n return (\n isObject(obj) &&\n (obj.constructor === Object || // obj = {}\n obj.constructor === undefined) // obj = Object.create(null)\n )\n}\n\nfunction mergeDeep(target, ...sources) {\n if (!sources.length) return target\n const source = sources.shift()\n\n if (Array.isArray(target)) {\n if (Array.isArray(source)) {\n target.push(...source)\n } else {\n target.push(source)\n }\n } else if (isPlainObject(target)) {\n if (isPlainObject(source)) {\n for (let key of Object.keys(source)) {\n if (!target[key]) {\n target[key] = source[key]\n } else {\n mergeDeep(target[key], source[key])\n }\n }\n } else {\n throw new Error(`Cannot merge object with non-object`)\n }\n } else {\n target = source\n }\n\n return mergeDeep(target, ...sources)\n}\n\nfunction assignDefaults(options) {\n return {\n ...options,\n consts: options.consts || {},\n sagas: options.sagas || (() => ({})),\n takes: options.takes || (() => []),\n creators: options.creators || (() => ({})),\n selectors: options.selectors || {},\n types: options.types || [],\n }\n}\n\nfunction injectDuck(input, duck) {\n if (input instanceof Function) {\n return input(duck)\n } else {\n return input\n }\n}\n\nfunction getLocalizedState(globalState, duck) {\n let localizedState\n\n if (duck.storePath) {\n const segments = [].concat(duck.storePath.split('.'), duck.store)\n localizedState = segments.reduce(function getSegment(acc, segment) {\n if (!acc[segment]) {\n throw Error(\n `state does not contain reducer at storePath ${segments.join('.')}`\n )\n }\n return acc[segment]\n }, globalState)\n } else {\n localizedState = globalState[duck.store]\n }\n\n return localizedState\n}\n\nexport function constructLocalized(selectors) {\n const derivedSelectors = deriveSelectors(selectors)\n return duck => {\n const localizedSelectors = {}\n Object.keys(derivedSelectors).forEach(key => {\n const selector = derivedSelectors[key]\n localizedSelectors[key] = globalState =>\n selector(getLocalizedState(globalState, duck), globalState)\n })\n return localizedSelectors\n }\n}\n\n// An alias for those who do not use the above spelling.\nexport { constructLocalized as constructLocalised }\n\n/**\n * Helper utility to assist in composing the selectors.\n * Previously defined selectors can be used to derive future selectors.\n *\n * @param {object} selectors\n * @returns\n */\nfunction deriveSelectors(selectors) {\n const composedSelectors = {}\n Object.keys(selectors).forEach(key => {\n const selector = selectors[key]\n if (selector instanceof Selector) {\n composedSelectors[key] = (...args) =>\n (composedSelectors[key] = selector.extractFunction(composedSelectors))(\n ...args\n )\n } else {\n composedSelectors[key] = selector\n }\n })\n return composedSelectors\n}\n\nexport default class Duck {\n constructor(options) {\n options = assignDefaults(options)\n const {\n namespace,\n store,\n storePath,\n types,\n consts,\n initialState,\n creators,\n selectors,\n sagas,\n takes,\n } = options\n this.options = options\n Object.keys(consts).forEach(name => {\n this[name] = zipObject(consts[name], consts[name])\n })\n\n this.store = store\n this.storePath = storePath\n this.types = buildTypes(namespace, store, types)\n this.initialState = isFunction(initialState)\n ? initialState(this)\n : initialState\n this.reducer = this.reducer.bind(this)\n this.selectors = deriveSelectors(injectDuck(selectors, this))\n this.creators = creators(this)\n this.sagas = sagas(this)\n this.takes = takes(this)\n }\n reducer(state, action) {\n if (isUndefined(state)) {\n state = this.initialState\n }\n return this.options.reducer(state, action, this)\n }\n extend(options) {\n if (isFunction(options)) {\n options = options(this)\n }\n options = assignDefaults(options)\n const parent = this.options\n let initialState\n if (isFunction(options.initialState)) {\n initialState = duck => options.initialState(duck, this.initialState)\n } else if (isUndefined(options.initialState)) {\n initialState = parent.initialState\n } else {\n initialState = options.initialState\n }\n return new Duck({\n ...parent,\n ...options,\n initialState,\n consts: mergeDeep({}, parent.consts, options.consts),\n sagas: duck => {\n const parentSagas = parent.sagas(duck)\n return { ...parentSagas, ...options.sagas(duck, parentSagas) }\n },\n takes: duck => {\n const parentTakes = parent.takes(duck)\n return [...parentTakes, ...options.takes(duck, parentTakes)]\n },\n creators: duck => {\n const parentCreators = parent.creators(duck)\n return { ...parentCreators, ...options.creators(duck, parentCreators) }\n },\n selectors: duck => ({\n ...injectDuck(parent.selectors, duck),\n ...injectDuck(options.selectors, duck),\n }),\n types: [...parent.types, ...options.types],\n reducer: (state, action, duck) => {\n state = parent.reducer(state, action, duck)\n if (isUndefined(options.reducer)) {\n return state\n } else {\n return options.reducer(state, action, duck)\n }\n },\n })\n }\n}\n\nexport class Selector {\n constructor(func) {\n this.func = func\n }\n\n extractFunction(selectors) {\n return this.func(selectors)\n }\n}\n\nDuck.Selector = Selector\n\n\n\n// WEBPACK FOOTER //\n// ./src/extensible-duck.js"],"sourceRoot":""} -------------------------------------------------------------------------------- /dist/extensible-duck.min.js: -------------------------------------------------------------------------------- 1 | !function(t,r){"object"==typeof exports&&"object"==typeof module?module.exports=r():"function"==typeof define&&define.amd?define([],r):"object"==typeof exports?exports.Duck=r():t.Duck=r()}(this,function(){return function(e){var n={};function o(t){if(n[t])return n[t].exports;var r=n[t]={i:t,l:!1,exports:{}};return e[t].call(r.exports,r,r.exports,o),r.l=!0,r.exports}return o.m=e,o.c=n,o.d=function(t,r,e){o.o(t,r)||Object.defineProperty(t,r,{configurable:!1,enumerable:!0,get:e})},o.n=function(t){var r=t&&t.__esModule?function(){return t.default}:function(){return t};return o.d(r,"a",r),r},o.o=function(t,r){return Object.prototype.hasOwnProperty.call(t,r)},o.p="",o(o.s=0)}([function(t,r,e){"use strict";Object.defineProperty(r,"__esModule",{value:!0}),r.constructLocalized=u,e.d(r,"constructLocalised",function(){return u}),e.d(r,"Selector",function(){return s});var n=function(){function n(t,r){for(var e=0;e 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {\n sources[_key - 1] = arguments[_key];\n }\n\n if (!sources.length) return target;\n var source = sources.shift();\n\n if (Array.isArray(target)) {\n if (Array.isArray(source)) {\n var _target;\n\n (_target = target).push.apply(_target, _toConsumableArray(source));\n } else {\n target.push(source);\n }\n } else if (isPlainObject(target)) {\n if (isPlainObject(source)) {\n var _iteratorNormalCompletion = true;\n var _didIteratorError = false;\n var _iteratorError = undefined;\n\n try {\n for (var _iterator = Object.keys(source)[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {\n var key = _step.value;\n\n if (!target[key]) {\n target[key] = source[key];\n } else {\n mergeDeep(target[key], source[key]);\n }\n }\n } catch (err) {\n _didIteratorError = true;\n _iteratorError = err;\n } finally {\n try {\n if (!_iteratorNormalCompletion && _iterator.return) {\n _iterator.return();\n }\n } finally {\n if (_didIteratorError) {\n throw _iteratorError;\n }\n }\n }\n } else {\n throw new Error('Cannot merge object with non-object');\n }\n } else {\n target = source;\n }\n\n return mergeDeep.apply(undefined, [target].concat(sources));\n}\n\nfunction assignDefaults(options) {\n return _extends({}, options, {\n consts: options.consts || {},\n sagas: options.sagas || function () {\n return {};\n },\n takes: options.takes || function () {\n return [];\n },\n creators: options.creators || function () {\n return {};\n },\n selectors: options.selectors || {},\n types: options.types || []\n });\n}\n\nfunction injectDuck(input, duck) {\n if (input instanceof Function) {\n return input(duck);\n } else {\n return input;\n }\n}\n\nfunction getLocalizedState(globalState, duck) {\n var localizedState = void 0;\n\n if (duck.storePath) {\n var segments = [].concat(duck.storePath.split('.'), duck.store);\n localizedState = segments.reduce(function getSegment(acc, segment) {\n if (!acc[segment]) {\n throw Error('state does not contain reducer at storePath ' + segments.join('.'));\n }\n return acc[segment];\n }, globalState);\n } else {\n localizedState = globalState[duck.store];\n }\n\n return localizedState;\n}\n\nfunction constructLocalized(selectors) {\n var derivedSelectors = deriveSelectors(selectors);\n return function (duck) {\n var localizedSelectors = {};\n Object.keys(derivedSelectors).forEach(function (key) {\n var selector = derivedSelectors[key];\n localizedSelectors[key] = function (globalState) {\n return selector(getLocalizedState(globalState, duck), globalState);\n };\n });\n return localizedSelectors;\n };\n}\n\n// An alias for those who do not use the above spelling.\n\n\n/**\n * Helper utility to assist in composing the selectors.\n * Previously defined selectors can be used to derive future selectors.\n *\n * @param {object} selectors\n * @returns\n */\nfunction deriveSelectors(selectors) {\n var composedSelectors = {};\n Object.keys(selectors).forEach(function (key) {\n var selector = selectors[key];\n if (selector instanceof Selector) {\n composedSelectors[key] = function () {\n return (composedSelectors[key] = selector.extractFunction(composedSelectors)).apply(undefined, arguments);\n };\n } else {\n composedSelectors[key] = selector;\n }\n });\n return composedSelectors;\n}\n\nvar Duck = function () {\n function Duck(options) {\n var _this = this;\n\n _classCallCheck(this, Duck);\n\n options = assignDefaults(options);\n var _options = options,\n namespace = _options.namespace,\n store = _options.store,\n storePath = _options.storePath,\n types = _options.types,\n consts = _options.consts,\n initialState = _options.initialState,\n creators = _options.creators,\n selectors = _options.selectors,\n sagas = _options.sagas,\n takes = _options.takes;\n\n this.options = options;\n Object.keys(consts).forEach(function (name) {\n _this[name] = zipObject(consts[name], consts[name]);\n });\n\n this.store = store;\n this.storePath = storePath;\n this.types = buildTypes(namespace, store, types);\n this.initialState = isFunction(initialState) ? initialState(this) : initialState;\n this.reducer = this.reducer.bind(this);\n this.selectors = deriveSelectors(injectDuck(selectors, this));\n this.creators = creators(this);\n this.sagas = sagas(this);\n this.takes = takes(this);\n }\n\n _createClass(Duck, [{\n key: 'reducer',\n value: function reducer(state, action) {\n if (isUndefined(state)) {\n state = this.initialState;\n }\n return this.options.reducer(state, action, this);\n }\n }, {\n key: 'extend',\n value: function extend(options) {\n var _this2 = this;\n\n if (isFunction(options)) {\n options = options(this);\n }\n options = assignDefaults(options);\n var parent = this.options;\n var initialState = void 0;\n if (isFunction(options.initialState)) {\n initialState = function initialState(duck) {\n return options.initialState(duck, _this2.initialState);\n };\n } else if (isUndefined(options.initialState)) {\n initialState = parent.initialState;\n } else {\n initialState = options.initialState;\n }\n return new Duck(_extends({}, parent, options, {\n initialState: initialState,\n consts: mergeDeep({}, parent.consts, options.consts),\n sagas: function sagas(duck) {\n var parentSagas = parent.sagas(duck);\n return _extends({}, parentSagas, options.sagas(duck, parentSagas));\n },\n takes: function takes(duck) {\n var parentTakes = parent.takes(duck);\n return [].concat(_toConsumableArray(parentTakes), _toConsumableArray(options.takes(duck, parentTakes)));\n },\n creators: function creators(duck) {\n var parentCreators = parent.creators(duck);\n return _extends({}, parentCreators, options.creators(duck, parentCreators));\n },\n selectors: function selectors(duck) {\n return _extends({}, injectDuck(parent.selectors, duck), injectDuck(options.selectors, duck));\n },\n types: [].concat(_toConsumableArray(parent.types), _toConsumableArray(options.types)),\n reducer: function reducer(state, action, duck) {\n state = parent.reducer(state, action, duck);\n if (isUndefined(options.reducer)) {\n return state;\n } else {\n return options.reducer(state, action, duck);\n }\n }\n }));\n }\n }]);\n\n return Duck;\n}();\n\n/* harmony default export */ __webpack_exports__[\"default\"] = (Duck);\n\n\nvar Selector = function () {\n function Selector(func) {\n _classCallCheck(this, Selector);\n\n this.func = func;\n }\n\n _createClass(Selector, [{\n key: 'extractFunction',\n value: function extractFunction(selectors) {\n return this.func(selectors);\n }\n }]);\n\n return Selector;\n}();\n\nDuck.Selector = Selector;\n\n/***/ })\n/******/ ]);\n});\n//# sourceMappingURL=extensible-duck.js.map"]} -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp') 2 | const loadPlugins = require('gulp-load-plugins') 3 | const del = require('del') 4 | const glob = require('glob') 5 | const path = require('path') 6 | const isparta = require('isparta') 7 | const webpack = require('webpack') 8 | const webpackStream = require('webpack-stream') 9 | 10 | const Instrumenter = isparta.Instrumenter 11 | const mochaGlobals = require('./test/setup/.globals') 12 | const manifest = require('./package.json') 13 | 14 | // Load all of our Gulp plugins 15 | const $ = loadPlugins() 16 | 17 | // Gather the library data from `package.json` 18 | const config = manifest.babelBoilerplateOptions 19 | const mainFile = manifest.main 20 | const destinationFolder = path.dirname(mainFile) 21 | const exportFileName = path.basename(mainFile, path.extname(mainFile)) 22 | 23 | function cleanDist(done) { 24 | del([destinationFolder]).then(() => done()) 25 | } 26 | 27 | function cleanTmp(done) { 28 | del(['tmp']).then(() => done()) 29 | } 30 | 31 | // Lint a set of files 32 | function lint(files) { 33 | return gulp 34 | .src(files) 35 | .pipe($.eslint()) 36 | .pipe($.eslint.format()) 37 | .pipe($.eslint.failAfterError()) 38 | } 39 | 40 | function lintSrc() { 41 | return lint('src/**/*.js') 42 | } 43 | 44 | function lintTest() { 45 | return lint('test/**/*.js') 46 | } 47 | 48 | function lintGulpfile() { 49 | return lint('gulpfile.js') 50 | } 51 | 52 | function build() { 53 | return gulp 54 | .src(path.join('src', config.entryFileName)) 55 | .pipe( 56 | webpackStream( 57 | { 58 | output: { 59 | filename: `${exportFileName}.js`, 60 | libraryTarget: 'umd', 61 | library: config.mainVarName, 62 | }, 63 | // Add your own externals here. For instance, 64 | // { 65 | // jquery: true 66 | // } 67 | // would externalize the `jquery` module. 68 | externals: {}, 69 | module: { 70 | rules: [ 71 | { 72 | test: /\.js$/, 73 | exclude: /node_modules/, 74 | use: [ 75 | { 76 | loader: 'babel-loader', 77 | query: { 78 | babelrc: false, 79 | presets: [ 80 | [ 81 | 'es2015', 82 | { 83 | // Enable tree-shaking by disabling commonJS transformation 84 | modules: false, 85 | }, 86 | ], 87 | ], 88 | plugins: ['transform-object-rest-spread'], 89 | }, 90 | }, 91 | ], 92 | }, 93 | ], 94 | }, 95 | devtool: 'source-map', 96 | }, 97 | webpack 98 | ) 99 | ) 100 | .pipe(gulp.dest(destinationFolder)) 101 | .pipe($.filter(['**', '!**/*.js.map'])) 102 | .pipe($.rename(`${exportFileName}.min.js`)) 103 | .pipe($.sourcemaps.init({ loadMaps: true })) 104 | .pipe($.uglify()) 105 | .pipe($.sourcemaps.write('./')) 106 | .pipe(gulp.dest(destinationFolder)) 107 | } 108 | 109 | function _mocha() { 110 | return gulp 111 | .src(['test/setup/node.js', 'test/unit/**/*.js'], { read: false }) 112 | .pipe( 113 | $.mocha({ 114 | reporter: 'dot', 115 | globals: Object.keys(mochaGlobals.globals), 116 | ignoreLeaks: false, 117 | }) 118 | ) 119 | } 120 | 121 | function _registerBabel() { 122 | require('babel-register') 123 | require('babel-polyfill') 124 | } 125 | 126 | function test() { 127 | _registerBabel() 128 | return _mocha() 129 | } 130 | 131 | function coverage(done) { 132 | _registerBabel() 133 | gulp 134 | .src(['src/**/*.js']) 135 | .pipe( 136 | $.istanbul({ 137 | instrumenter: Instrumenter, 138 | includeUntested: true, 139 | }) 140 | ) 141 | .pipe($.istanbul.hookRequire()) 142 | .on('finish', () => { 143 | return test().pipe($.istanbul.writeReports()).on('end', done) 144 | }) 145 | } 146 | 147 | const watchFiles = ['src/**/*', 'test/**/*', 'package.json', '**/.eslintrc'] 148 | 149 | // Run the headless unit tests as you make changes. 150 | function watch() { 151 | gulp.watch(watchFiles, ['test']) 152 | } 153 | 154 | function testBrowser() { 155 | // Our testing bundle is made up of our unit tests, which 156 | // should individually load up pieces of our application. 157 | // We also include the browser setup file. 158 | const testFiles = glob.sync('./test/unit/**/*.js') 159 | const allFiles = ['./test/setup/browser.js'].concat(testFiles) 160 | 161 | // Lets us differentiate between the first build and subsequent builds 162 | var firstBuild = true 163 | 164 | // This empty stream might seem like a hack, but we need to specify all of our files through 165 | // the `entry` option of webpack. Otherwise, it ignores whatever file(s) are placed in here. 166 | return gulp 167 | .src('') 168 | .pipe($.plumber()) 169 | .pipe( 170 | webpackStream( 171 | { 172 | watch: true, 173 | entry: allFiles, 174 | output: { 175 | filename: '__spec-build.js', 176 | }, 177 | // Externals isn't necessary here since these are for tests. 178 | module: { 179 | // This is what allows us to author in future JavaScript 180 | rules: [ 181 | { 182 | test: /\.js$/, 183 | exclude: /node_modules/, 184 | use: [ 185 | { 186 | loader: 'babel-loader', 187 | query: { 188 | babelrc: false, 189 | presets: [ 190 | [ 191 | 'es2015', 192 | { 193 | modules: false, 194 | }, 195 | ], 196 | ], 197 | plugins: ['transform-object-rest-spread'], 198 | }, 199 | }, 200 | ], 201 | }, 202 | { 203 | test: /\.json$/, 204 | use: [ 205 | { 206 | loader: 'json-loader', 207 | }, 208 | ], 209 | }, 210 | ], 211 | }, 212 | plugins: [ 213 | // By default, webpack does `n=>n` compilation with entry files. This concatenates 214 | // them into a single chunk. 215 | new webpack.optimize.LimitChunkCountPlugin({ 216 | maxChunks: 1, 217 | }), 218 | ], 219 | devtool: 'inline-source-map', 220 | }, 221 | webpack, 222 | () => { 223 | if (firstBuild) { 224 | $.livereload.listen({ 225 | port: 35729, 226 | host: 'localhost', 227 | start: true, 228 | }) 229 | gulp.watch(watchFiles, ['lint']) 230 | } else { 231 | $.livereload.reload('./tmp/__spec-build.js') 232 | } 233 | firstBuild = false 234 | } 235 | ) 236 | ) 237 | .pipe(gulp.dest('./tmp')) 238 | } 239 | 240 | // Remove the built files 241 | gulp.task('clean', cleanDist) 242 | 243 | // Remove our temporary files 244 | gulp.task('clean-tmp', cleanTmp) 245 | 246 | // Lint our source code 247 | gulp.task('lint-src', lintSrc) 248 | 249 | // Lint our test code 250 | gulp.task('lint-test', lintTest) 251 | 252 | // Lint this file 253 | gulp.task('lint-gulpfile', lintGulpfile) 254 | 255 | // Lint everything 256 | gulp.task('lint', ['lint-src', 'lint-test', 'lint-gulpfile']) 257 | 258 | // Build two versions of the library 259 | gulp.task('build', ['lint', 'clean'], build) 260 | 261 | // Lint and run our tests 262 | gulp.task('test', ['lint'], test) 263 | 264 | // Set up coverage and run tests 265 | gulp.task('coverage', ['lint'], coverage) 266 | 267 | // Set up a livereload environment for our spec runner `test/runner.html` 268 | gulp.task('test-browser', ['lint', 'clean-tmp'], testBrowser) 269 | 270 | // Run the headless unit tests as you make changes. 271 | gulp.task('watch', watch) 272 | 273 | // An alias of test 274 | gulp.task('default', ['test']) 275 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "extensible-duck", 3 | "version": "1.6.0", 4 | "description": "Modular and Extensible Redux Reducer Bundles (ducks-modular-redux)", 5 | "main": "dist/extensible-duck.js", 6 | "scripts": { 7 | "test": "gulp", 8 | "lint": "gulp lint", 9 | "test-browser": "gulp test-browser", 10 | "watch": "gulp watch", 11 | "build": "gulp build", 12 | "precommit": "lint-staged", 13 | "prettier": "prettier --no-semi --single-quote --trailing-comma es5 --write '{src,test}/**/*.js'", 14 | "coverage": "gulp coverage" 15 | }, 16 | "lint-staged": { 17 | "**/{src,test}/**/*.js": [ 18 | "prettier --no-semi --single-quote --trailing-comma es5 --write", 19 | "git add" 20 | ] 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/investtools/extensible-duck.git" 25 | }, 26 | "keywords": [], 27 | "author": "Andre Aizim Kelmanosn ", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/investtools/extensible-duck/issues" 31 | }, 32 | "homepage": "https://github.com/investtools/extensible-duck", 33 | "devDependencies": { 34 | "babel-cli": "6.24.1", 35 | "babel-core": "6.25.0", 36 | "babel-eslint": "7.2.3", 37 | "babel-loader": "7.1.1", 38 | "babel-plugin-transform-object-rest-spread": "6.23.0", 39 | "babel-polyfill": "6.23.0", 40 | "babel-preset-es2015": "6.24.1", 41 | "babel-register": "6.24.1", 42 | "chai": "3.5.0", 43 | "del": "2.2.2", 44 | "glob": "7.0.6", 45 | "gulp": "3.9.1", 46 | "gulp-eslint": "3.0.1", 47 | "gulp-filter": "4.0.0", 48 | "gulp-istanbul": "1.1.1", 49 | "gulp-livereload": "3.8.1", 50 | "gulp-load-plugins": "1.2.4", 51 | "gulp-mocha": "3.0.1", 52 | "gulp-plumber": "1.1.0", 53 | "gulp-rename": "1.2.2", 54 | "gulp-sourcemaps": "2.6.0", 55 | "gulp-uglify": "3.0.0", 56 | "isparta": "4.0.0", 57 | "json-loader": "0.5.4", 58 | "lint-staged": "4.0.2", 59 | "mocha": "3.0.2", 60 | "prettier": "1.5.3", 61 | "reselect": "^3.0.1", 62 | "sinon": "1.17.5", 63 | "sinon-chai": "2.8.0", 64 | "webpack": "3.2.0", 65 | "webpack-stream": "3.2.0" 66 | }, 67 | "babelBoilerplateOptions": { 68 | "entryFileName": "extensible-duck.js", 69 | "mainVarName": "Duck" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/extensible-duck.js: -------------------------------------------------------------------------------- 1 | function typeValue(namespace, store, type) { 2 | return `${namespace}/${store}/${type}` 3 | } 4 | 5 | function zipObject(keys, values) { 6 | if (arguments.length == 1) { 7 | values = keys[1] 8 | keys = keys[0] 9 | } 10 | 11 | var result = {} 12 | var i = 0 13 | 14 | for (i; i < keys.length; i += 1) { 15 | result[keys[i]] = values[i] 16 | } 17 | 18 | return result 19 | } 20 | 21 | function buildTypes(namespace, store, types) { 22 | return zipObject(types, types.map(type => typeValue(namespace, store, type))) 23 | } 24 | 25 | function isObject(obj) { 26 | return obj !== null && typeof obj === 'object' 27 | } 28 | 29 | function isFunction(func) { 30 | return func !== null && typeof func === 'function' 31 | } 32 | 33 | function isUndefined(value) { 34 | return typeof value === 'undefined' || value === undefined 35 | } 36 | 37 | function isPlainObject(obj) { 38 | return ( 39 | isObject(obj) && 40 | (obj.constructor === Object || // obj = {} 41 | obj.constructor === undefined) // obj = Object.create(null) 42 | ) 43 | } 44 | 45 | function mergeDeep(target, ...sources) { 46 | if (!sources.length) return target 47 | const source = sources.shift() 48 | 49 | if (Array.isArray(target)) { 50 | if (Array.isArray(source)) { 51 | target.push(...source) 52 | } else { 53 | target.push(source) 54 | } 55 | } else if (isPlainObject(target)) { 56 | if (isPlainObject(source)) { 57 | for (let key of Object.keys(source)) { 58 | if (!target[key]) { 59 | target[key] = source[key] 60 | } else { 61 | mergeDeep(target[key], source[key]) 62 | } 63 | } 64 | } else { 65 | throw new Error(`Cannot merge object with non-object`) 66 | } 67 | } else { 68 | target = source 69 | } 70 | 71 | return mergeDeep(target, ...sources) 72 | } 73 | 74 | function assignDefaults(options) { 75 | return { 76 | ...options, 77 | consts: options.consts || {}, 78 | sagas: options.sagas || (() => ({})), 79 | takes: options.takes || (() => []), 80 | creators: options.creators || (() => ({})), 81 | selectors: options.selectors || {}, 82 | types: options.types || [], 83 | } 84 | } 85 | 86 | function injectDuck(input, duck) { 87 | if (input instanceof Function) { 88 | return input(duck) 89 | } else { 90 | return input 91 | } 92 | } 93 | 94 | function getLocalizedState(globalState, duck) { 95 | let localizedState 96 | 97 | if (duck.storePath) { 98 | const segments = [].concat(duck.storePath.split('.'), duck.store) 99 | localizedState = segments.reduce(function getSegment(acc, segment) { 100 | if (!acc[segment]) { 101 | throw Error( 102 | `state does not contain reducer at storePath ${segments.join('.')}` 103 | ) 104 | } 105 | return acc[segment] 106 | }, globalState) 107 | } else { 108 | localizedState = globalState[duck.store] 109 | } 110 | 111 | return localizedState 112 | } 113 | 114 | export function constructLocalized(selectors) { 115 | const derivedSelectors = deriveSelectors(selectors) 116 | return duck => { 117 | const localizedSelectors = {} 118 | Object.keys(derivedSelectors).forEach(key => { 119 | const selector = derivedSelectors[key] 120 | localizedSelectors[key] = globalState => 121 | selector(getLocalizedState(globalState, duck), globalState) 122 | }) 123 | return localizedSelectors 124 | } 125 | } 126 | 127 | // An alias for those who do not use the above spelling. 128 | export { constructLocalized as constructLocalised } 129 | 130 | /** 131 | * Helper utility to assist in composing the selectors. 132 | * Previously defined selectors can be used to derive future selectors. 133 | * 134 | * @param {object} selectors 135 | * @returns 136 | */ 137 | function deriveSelectors(selectors) { 138 | const composedSelectors = {} 139 | Object.keys(selectors).forEach(key => { 140 | const selector = selectors[key] 141 | if (selector instanceof Selector) { 142 | composedSelectors[key] = (...args) => 143 | (composedSelectors[key] = selector.extractFunction(composedSelectors))( 144 | ...args 145 | ) 146 | } else { 147 | composedSelectors[key] = selector 148 | } 149 | }) 150 | return composedSelectors 151 | } 152 | 153 | export default class Duck { 154 | constructor(options) { 155 | options = assignDefaults(options) 156 | const { 157 | namespace, 158 | store, 159 | storePath, 160 | types, 161 | consts, 162 | initialState, 163 | creators, 164 | selectors, 165 | sagas, 166 | takes, 167 | } = options 168 | this.options = options 169 | Object.keys(consts).forEach(name => { 170 | this[name] = zipObject(consts[name], consts[name]) 171 | }) 172 | 173 | this.store = store 174 | this.storePath = storePath 175 | this.types = buildTypes(namespace, store, types) 176 | this.initialState = isFunction(initialState) 177 | ? initialState(this) 178 | : initialState 179 | this.reducer = this.reducer.bind(this) 180 | this.selectors = deriveSelectors(injectDuck(selectors, this)) 181 | this.creators = creators(this) 182 | this.sagas = sagas(this) 183 | this.takes = takes(this) 184 | } 185 | reducer(state, action) { 186 | if (isUndefined(state)) { 187 | state = this.initialState 188 | } 189 | return this.options.reducer(state, action, this) 190 | } 191 | extend(options) { 192 | if (isFunction(options)) { 193 | options = options(this) 194 | } 195 | options = assignDefaults(options) 196 | const parent = this.options 197 | let initialState 198 | if (isFunction(options.initialState)) { 199 | initialState = duck => options.initialState(duck, this.initialState) 200 | } else if (isUndefined(options.initialState)) { 201 | initialState = parent.initialState 202 | } else { 203 | initialState = options.initialState 204 | } 205 | return new Duck({ 206 | ...parent, 207 | ...options, 208 | initialState, 209 | consts: mergeDeep({}, parent.consts, options.consts), 210 | sagas: duck => { 211 | const parentSagas = parent.sagas(duck) 212 | return { ...parentSagas, ...options.sagas(duck, parentSagas) } 213 | }, 214 | takes: duck => { 215 | const parentTakes = parent.takes(duck) 216 | return [...parentTakes, ...options.takes(duck, parentTakes)] 217 | }, 218 | creators: duck => { 219 | const parentCreators = parent.creators(duck) 220 | return { ...parentCreators, ...options.creators(duck, parentCreators) } 221 | }, 222 | selectors: duck => ({ 223 | ...injectDuck(parent.selectors, duck), 224 | ...injectDuck(options.selectors, duck), 225 | }), 226 | types: [...parent.types, ...options.types], 227 | reducer: (state, action, duck) => { 228 | state = parent.reducer(state, action, duck) 229 | if (isUndefined(options.reducer)) { 230 | return state 231 | } else { 232 | return options.reducer(state, action, duck) 233 | } 234 | }, 235 | }) 236 | } 237 | } 238 | 239 | export class Selector { 240 | constructor(func) { 241 | this.func = func 242 | } 243 | 244 | extractFunction(selectors) { 245 | return this.func(selectors) 246 | } 247 | } 248 | 249 | Duck.Selector = Selector 250 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./setup/.globals.json", 3 | "parserOptions": { 4 | "ecmaVersion": 6, 5 | "sourceType": "module", 6 | "ecmaFeatures": { 7 | "experimentalObjectRestSpread": true 8 | } 9 | }, 10 | "rules": { 11 | "strict": 0, 12 | "quotes": [2, "single"], 13 | "no-unused-expressions": 0 14 | }, 15 | "env": { 16 | "browser": true, 17 | "node": true, 18 | "mocha": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Tests 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | -------------------------------------------------------------------------------- /test/setup/.globals.json: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "expect": true, 4 | "mock": true, 5 | "sandbox": true, 6 | "spy": true, 7 | "stub": true, 8 | "useFakeServer": true, 9 | "useFakeTimers": true, 10 | "useFakeXMLHttpRequest": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/setup/browser.js: -------------------------------------------------------------------------------- 1 | var mochaGlobals = require('./.globals.json').globals 2 | 3 | window.mocha.setup('bdd') 4 | window.onload = function() { 5 | window.mocha.checkLeaks() 6 | window.mocha.globals(Object.keys(mochaGlobals)) 7 | window.mocha.run() 8 | require('./setup')(window) 9 | } 10 | -------------------------------------------------------------------------------- /test/setup/node.js: -------------------------------------------------------------------------------- 1 | global.chai = require('chai') 2 | global.sinon = require('sinon') 3 | global.chai.use(require('sinon-chai')) 4 | 5 | require('babel-core/register') 6 | require('./setup')() 7 | 8 | /* 9 | Uncomment the following if your library uses features of the DOM, 10 | for example if writing a jQuery extension, and 11 | add 'simple-jsdom' to the `devDependencies` of your package.json 12 | 13 | Note that JSDom doesn't implement the entire DOM API. If you're using 14 | more advanced or experimental features, you may need to switch to 15 | PhantomJS. Setting that up is currently outside of the scope of this 16 | boilerplate. 17 | */ 18 | // import simpleJSDom from 'simple-jsdom'; 19 | // simpleJSDom.install(); 20 | -------------------------------------------------------------------------------- /test/setup/setup.js: -------------------------------------------------------------------------------- 1 | module.exports = function(root) { 2 | root = root ? root : global 3 | root.expect = root.chai.expect 4 | 5 | beforeEach(() => { 6 | // Using these globally-available Sinon features is preferrable, as they're 7 | // automatically restored for you in the subsequent `afterEach` 8 | root.sandbox = root.sinon.sandbox.create() 9 | root.stub = root.sandbox.stub.bind(root.sandbox) 10 | root.spy = root.sandbox.spy.bind(root.sandbox) 11 | root.mock = root.sandbox.mock.bind(root.sandbox) 12 | root.useFakeTimers = root.sandbox.useFakeTimers.bind(root.sandbox) 13 | root.useFakeXMLHttpRequest = root.sandbox.useFakeXMLHttpRequest.bind( 14 | root.sandbox 15 | ) 16 | root.useFakeServer = root.sandbox.useFakeServer.bind(root.sandbox) 17 | }) 18 | 19 | afterEach(() => { 20 | delete root.stub 21 | delete root.spy 22 | root.sandbox.restore() 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /test/unit/extensible-duck.js: -------------------------------------------------------------------------------- 1 | import Duck, { constructLocalized } from '../../src/extensible-duck' 2 | import _ from 'lodash' 3 | import { createSelector } from 'reselect' 4 | 5 | describe('Duck', () => { 6 | describe('constructor', () => { 7 | it('transforms types in object with prefix', () => { 8 | expect( 9 | new Duck({ 10 | namespace: 'app', 11 | store: 'users', 12 | types: ['FETCH'], 13 | }).types 14 | ).to.eql({ FETCH: 'app/users/FETCH' }) 15 | }) 16 | it('lets the creators reference the duck instance', () => { 17 | const duck = new Duck({ 18 | types: ['FETCH'], 19 | creators: ({ types }) => ({ 20 | get: id => ({ type: types.FETCH, id }), 21 | }), 22 | }) 23 | expect(duck.creators.get(15)).to.eql({ 24 | type: duck.types.FETCH, 25 | id: 15, 26 | }) 27 | }) 28 | it('lets the selectors compose themselves and reference the duck instance', () => { 29 | const duck = new Duck({ 30 | initialState: { 31 | items: [ 32 | { name: 'apple', value: 1.2 }, 33 | { name: 'orange', value: 0.95 }, 34 | ], 35 | }, 36 | selectors: { 37 | items: state => state.items, // gets the items from complete state 38 | subTotal: new Duck.Selector(selectors => state => 39 | // Get another derived state reusing previous items selector. 40 | // Can be composed multiple such states if using library like reselect. 41 | selectors 42 | .items(state) 43 | .reduce((computedTotal, item) => computedTotal + item.value, 0) 44 | ), 45 | }, 46 | }) 47 | expect(duck.selectors.items(duck.initialState)).to.eql([ 48 | { name: 'apple', value: 1.2 }, 49 | { name: 'orange', value: 0.95 }, 50 | ]) 51 | expect(duck.selectors.subTotal(duck.initialState)).to.eql(2.15) 52 | }) 53 | it('generates the selector function once per selector', () => { 54 | let passes = 0 55 | const duck = new Duck({ 56 | selectors: { 57 | myFunc: new Duck.Selector(selectors => { 58 | passes++ 59 | return () => {} 60 | }), 61 | }, 62 | }) 63 | duck.selectors.myFunc() 64 | duck.selectors.myFunc() 65 | expect(passes).to.eql(1) 66 | }) 67 | it('lets the selectors access the duck instance', () => { 68 | const planetsState = { 69 | planets: ['mercury', 'wenus', 'earth', 'mars'], 70 | } 71 | const duck = new Duck({ 72 | store: 'box', 73 | initialState: { 74 | items: ['chocolate', 'muffin', 'candy'], 75 | }, 76 | selectors: constructLocalized({ 77 | countSweets: localState => localState.items.length, 78 | countPlanets: (localState, globalState) => globalState.planets.length, 79 | countObjects: new Duck.Selector(selectors => 80 | createSelector( 81 | selectors.countSweets, 82 | selectors.countPlanets, 83 | (countSweets, countPlanets) => countSweets + countPlanets 84 | ) 85 | ), 86 | }), 87 | }) 88 | const store = { 89 | ...planetsState, 90 | [duck.store]: duck.initialState, 91 | } 92 | expect(duck.selectors.countSweets(store)).to.eql(3) 93 | expect(duck.selectors.countPlanets(store)).to.eql(4) 94 | expect(duck.selectors.countObjects(store)).to.eql(7) 95 | }) 96 | it('can construct localized state of deep nested duck reference', () => { 97 | const planetsState = { 98 | planets: ['mercury', 'wenus', 'earth', 'mars'], 99 | } 100 | const duck = new Duck({ 101 | store: 'box', 102 | storePath: 'foo.bar', 103 | initialState: { 104 | items: ['chocolate', 'muffin', 'candy'], 105 | }, 106 | selectors: constructLocalized({ 107 | countSweets: localState => localState.items.length, 108 | countPlanets: (localState, globalState) => globalState.planets.length, 109 | }), 110 | }) 111 | const store = { 112 | ...planetsState, 113 | foo: { 114 | bar: { 115 | [duck.store]: duck.initialState, 116 | }, 117 | }, 118 | } 119 | expect(duck.selectors.countSweets(store)).to.eql(3) 120 | expect(duck.selectors.countPlanets(store)).to.eql(4) 121 | }) 122 | it('works with reselect', () => { 123 | const duck = new Duck({ 124 | selectors: { 125 | test1: state => state.test1, 126 | test2: new Duck.Selector(selectors => 127 | createSelector(selectors.test1, test1 => test1) 128 | ), 129 | test3: new Duck.Selector(selectors => 130 | createSelector(selectors.test2, test2 => test2) 131 | ), 132 | }, 133 | }) 134 | expect(duck.selectors.test3({ test1: 'it works' })).to.eql('it works') 135 | }) 136 | it('lets the initialState reference the duck instance', () => { 137 | const duck = new Duck({ 138 | consts: { statuses: ['NEW'] }, 139 | initialState: ({ statuses }) => ({ status: statuses.NEW }), 140 | }) 141 | expect(duck.initialState).to.eql({ status: 'NEW' }) 142 | }) 143 | it('accepts the initialState as an object', () => { 144 | const duck = new Duck({ 145 | initialState: { obj: {} }, 146 | }) 147 | expect(duck.initialState).to.eql({ obj: {} }) 148 | }) 149 | it('creates the constant objects', () => { 150 | const duck = new Duck({ 151 | consts: { statuses: ['READY', 'ERROR'] }, 152 | }) 153 | expect(duck.statuses).to.eql({ READY: 'READY', ERROR: 'ERROR' }) 154 | }) 155 | it('lets the creators access the selectors', () => { 156 | const duck = new Duck({ 157 | selectors: { 158 | sum: numbers => numbers.reduce((sum, n) => sum + n, 0), 159 | }, 160 | creators: ({ selectors }) => ({ 161 | calculate: () => dispatch => { 162 | dispatch({ type: 'CALCULATE', payload: selectors.sum([1, 2, 3]) }) 163 | }, 164 | }), 165 | }) 166 | const dispatch = sinon.spy() 167 | duck.creators.calculate()(dispatch) 168 | expect(dispatch).to.have.been.calledWith({ 169 | type: 'CALCULATE', 170 | payload: 6, 171 | }) 172 | }) 173 | }) 174 | describe('reducer', () => { 175 | it('lets the original reducer reference the duck instance', () => { 176 | const duck = new Duck({ 177 | types: ['FETCH'], 178 | reducer: (state, action, { types }) => { 179 | switch (action.type) { 180 | case types.FETCH: 181 | return { worked: true } 182 | default: 183 | return state 184 | } 185 | }, 186 | }) 187 | expect(duck.reducer({}, { type: duck.types.FETCH })).to.eql({ 188 | worked: true, 189 | }) 190 | }) 191 | it('passes the initialState to the original reducer when state is undefined', () => { 192 | const duck = new Duck({ 193 | initialState: { obj: {} }, 194 | reducer: (state, action) => { 195 | return state 196 | }, 197 | }) 198 | expect(duck.reducer(undefined, { type: duck.types.FETCH })).to.eql({ 199 | obj: {}, 200 | }) 201 | }) 202 | }) 203 | describe('extend', () => { 204 | it('creates a new Duck', () => { 205 | expect(new Duck({}).extend({}).constructor.name).to.eql('Duck') 206 | }) 207 | it('copies the attributes to the new Duck', () => { 208 | const duck = new Duck({ initialState: { obj: null } }) 209 | expect(duck.extend({}).initialState).to.eql({ obj: null }) 210 | }) 211 | it('copies the original consts', () => { 212 | const duck = new Duck({ consts: { statuses: ['NEW'] } }) 213 | expect(duck.extend({}).statuses).to.eql({ NEW: 'NEW' }) 214 | }) 215 | it('overrides the types', () => { 216 | const duck = new Duck({ 217 | namespace: 'ns', 218 | store: 'x', 219 | types: ['FETCH'], 220 | }) 221 | expect(duck.extend({ namespace: 'ns2', store: 'y' }).types).to.eql({ 222 | FETCH: 'ns2/y/FETCH', 223 | }) 224 | }) 225 | it('merges the consts', () => { 226 | const duck = new Duck({ consts: { statuses: ['READY'] } }) 227 | expect( 228 | duck.extend({ consts: { statuses: ['FAILED'] } }).statuses 229 | ).to.eql({ 230 | READY: 'READY', 231 | FAILED: 'FAILED', 232 | }) 233 | }) 234 | it('merges the takes', () => { 235 | const duck = new Duck({ takes: () => ['first'] }) 236 | expect(duck.extend({ takes: () => ['second'] }).takes).to.eql([ 237 | 'first', 238 | 'second', 239 | ]) 240 | }) 241 | it('merges the sagas', () => { 242 | const duck = new Duck({ 243 | sagas: () => ({ 244 | first: function*() { 245 | yield 1 246 | }, 247 | }), 248 | }) 249 | const childDuck = duck.extend({ 250 | sagas: () => ({ 251 | second: function*() { 252 | yield 2 253 | }, 254 | }), 255 | }) 256 | expect(_.keys(childDuck.sagas)).to.eql(['first', 'second']) 257 | }) 258 | it('appends new types', () => { 259 | expect( 260 | new Duck({}).extend({ 261 | namespace: 'ns2', 262 | store: 'y', 263 | types: ['RESET'], 264 | }).types 265 | ).to.eql({ RESET: 'ns2/y/RESET' }) 266 | }) 267 | it('appends the new reducers', () => { 268 | const duck = new Duck({ 269 | creators: () => ({ 270 | get: () => ({ type: 'GET' }), 271 | }), 272 | }) 273 | const childDuck = duck.extend({ 274 | creators: () => ({ 275 | delete: () => ({ type: 'DELETE' }), 276 | }), 277 | }) 278 | expect(_.keys(childDuck.creators)).to.eql(['get', 'delete']) 279 | }) 280 | it('lets the reducers access the parents', () => { 281 | const d1 = new Duck({ 282 | creators: () => ({ 283 | get: () => ({ d1: true }), 284 | }), 285 | }) 286 | const d2 = d1.extend({ 287 | creators: (duck, parent) => ({ 288 | get: () => ({ ...parent.get(duck), d2: true }), 289 | }), 290 | }) 291 | const d3 = d2.extend({ 292 | creators: (duck, parent) => ({ 293 | get: () => ({ ...parent.get(duck), d3: true }), 294 | }), 295 | }) 296 | expect(d3.creators.get()).to.eql({ d1: true, d2: true, d3: true }) 297 | }) 298 | context('when a function is passed', () => { 299 | it('passes the duck instance as argument', () => { 300 | const duck = new Duck({ foo: 2 }) 301 | const childDuck = duck.extend(parent => ({ 302 | bar: parent.options.foo * 2, 303 | })) 304 | expect(childDuck.options.bar).to.eql(4) 305 | }) 306 | }) 307 | it('updates the old creators with the new properties', () => { 308 | const duck = new Duck({ 309 | namespace: 'a', 310 | store: 'x', 311 | types: ['GET'], 312 | creators: ({ types }) => ({ 313 | get: () => ({ type: types.GET }), 314 | }), 315 | }) 316 | const childDuck = duck.extend({ namespace: 'b', store: 'y' }) 317 | expect(childDuck.creators.get()).to.eql({ type: 'b/y/GET' }) 318 | }) 319 | it('updates the old selectors with the new properties', () => { 320 | const duck = new Duck({ 321 | namespace: 'a', 322 | store: 'x', 323 | initialState: { 324 | items: [ 325 | { name: 'apple', value: 1.2 }, 326 | { name: 'orange', value: 0.95 }, 327 | ], 328 | }, 329 | selectors: { 330 | items: state => state.items, // gets the items from complete state 331 | }, 332 | }) 333 | const childDuck = duck.extend({ 334 | namespace: 'b', 335 | store: 'y', 336 | selectors: { 337 | subTotal: new Duck.Selector(selectors => state => 338 | // Get another derived state reusing previous items selector. 339 | // Can be composed multiple such states if using library like reselect. 340 | selectors 341 | .items(state) 342 | .reduce((computedTotal, item) => computedTotal + item.value, 0) 343 | ), 344 | }, 345 | }) 346 | expect(childDuck.selectors.items(duck.initialState)).to.eql([ 347 | { name: 'apple', value: 1.2 }, 348 | { name: 'orange', value: 0.95 }, 349 | ]) 350 | expect(childDuck.selectors.subTotal(duck.initialState)).to.eql(2.15) 351 | }) 352 | it('adds the new reducer keeping the old ones', () => { 353 | const parentDuck = new Duck({ 354 | reducer: (state, action) => { 355 | switch (action.type) { 356 | case 'FETCH': 357 | return { ...state, parentDuck: true } 358 | default: 359 | return state 360 | } 361 | }, 362 | }) 363 | const duck = parentDuck.extend({ 364 | reducer: (state, action) => { 365 | switch (action.type) { 366 | case 'FETCH': 367 | return { ...state, duck: true } 368 | default: 369 | return state 370 | } 371 | }, 372 | }) 373 | expect(duck.reducer({}, { type: 'FETCH' })).to.eql({ 374 | parentDuck: true, 375 | duck: true, 376 | }) 377 | }) 378 | it('does not affect the original duck', () => { 379 | const parentDuck = new Duck({ 380 | reducer: (state, action) => { 381 | switch (action.type) { 382 | case 'FETCH': 383 | return { ...state, parentDuck: true } 384 | default: 385 | return state 386 | } 387 | }, 388 | }) 389 | const duck = parentDuck.extend({ 390 | reducer: (state, action) => { 391 | switch (action.type) { 392 | case 'FETCH': 393 | return { ...state, duck: true } 394 | default: 395 | return state 396 | } 397 | }, 398 | }) 399 | expect(parentDuck.reducer({}, { type: 'FETCH' })).to.eql({ 400 | parentDuck: true, 401 | }) 402 | }) 403 | it('passes the parent initialState to the child', () => { 404 | const parentDuck = new Duck({ initialState: { parent: true } }) 405 | const duck = parentDuck.extend({ 406 | initialState: (duck, parentState) => { 407 | return { ...parentState, child: true } 408 | }, 409 | }) 410 | expect(duck.initialState).to.eql({ parent: true, child: true }) 411 | }) 412 | }) 413 | }) 414 | --------------------------------------------------------------------------------