├── .babelrc ├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── src ├── Connector.js ├── Provider.js ├── index.js └── storeShape.js └── test ├── Connector-spec.js └── setup.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "es2015", "react" ], 3 | "plugins": [ 4 | "transform-function-bind", 5 | "transform-es2015-modules-commonjs", 6 | "transform-object-rest-spread" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "plugins": [ 4 | "react" 5 | ], 6 | "rules": { 7 | // Enforces getter/setter pairs in objects 8 | "accessor-pairs": 0, 9 | // treat var statements as if they were block scoped 10 | "block-scoped-var": 2, 11 | // specify the maximum cyclomatic complexity allowed in a program 12 | "complexity": [0, 11], 13 | // require return statements to either always or never specify values 14 | "consistent-return": 0, 15 | // specify curly brace conventions for all control statements 16 | "curly": [2, "multi-line"], 17 | // require default case in switch statements 18 | "default-case": 2, 19 | // encourages use of dot notation whenever possible 20 | "dot-notation": [2, { 21 | "allowKeywords": true 22 | }], 23 | // enforces consistent newlines before or after dots 24 | "dot-location": 0, 25 | // require the use of === and !== 26 | "eqeqeq": 2, 27 | // make sure for-in loops have an if statement 28 | "guard-for-in": 2, 29 | // disabled use of an undefined variable 30 | "no-undef": 2, 31 | // disallow the use of console 32 | "no-console": 0, 33 | // disallow the use of alert, confirm, and prompt 34 | "no-alert": 0, 35 | // disallow use of arguments.caller or arguments.callee 36 | "no-caller": 2, 37 | // disallow division operators explicitly at beginning of regular expression 38 | "no-div-regex": 0, 39 | // disallow else after a return in an if 40 | "no-else-return": 0, 41 | // disallow comparisons to null without a type-checking operator 42 | "no-eq-null": 2, 43 | // disallow use of eval() 44 | "no-eval": 2, 45 | // disallow adding to native types 46 | "no-extend-native": 2, 47 | // disallow unnecessary function binding 48 | "no-extra-bind": 2, 49 | // disallow fallthrough of case statements 50 | "no-fallthrough": 2, 51 | // disallow the use of leading or trailing decimal points in numeric literals 52 | "no-floating-decimal": 2, 53 | // disallow the type conversions with shorter notations 54 | "no-implicit-coercion": 0, 55 | // disallow use of eval()-like methods 56 | "no-implied-eval": 2, 57 | // disallow this keywords outside of classes or class-like objects 58 | "no-invalid-this": 0, 59 | // disallow usage of __iterator__ property 60 | "no-iterator": 2, 61 | // disallow use of labeled statements 62 | "no-labels": 2, 63 | // disallow unnecessary nested blocks 64 | "no-lone-blocks": 2, 65 | // disallow creation of functions within loops 66 | "no-loop-func": 2, 67 | // disallow use of multiple spaces 68 | "no-multi-spaces": 2, 69 | // disallow use of multiline strings 70 | "no-multi-str": 2, 71 | // disallow reassignments of native objects 72 | "no-native-reassign": 2, 73 | // disallow use of new operator when not part of the assignment or comparison 74 | "no-new": 2, 75 | // disallow use of new operator for Function object 76 | "no-new-func": 2, 77 | // disallows creating new instances of String,Number, and Boolean 78 | "no-new-wrappers": 2, 79 | // disallow use of (old style) octal literals 80 | "no-octal": 2, 81 | // disallow use of octal escape sequences in string literals, such as 82 | // var foo = "Copyright \251"; 83 | "no-octal-escape": 2, 84 | // disallow reassignment of function parameters 85 | "no-param-reassign": 0, 86 | // disallow use of process.env 87 | "no-process-env": 0, 88 | // disallow usage of __proto__ property 89 | "no-proto": 2, 90 | // disallow declaring the same variable more then once 91 | "no-redeclare": 2, 92 | // disallow use of assignment in return statement 93 | "no-return-assign": 2, 94 | // disallow use of `javascript:` urls. 95 | "no-script-url": 2, 96 | // disallow comparisons where both sides are exactly the same 97 | "no-self-compare": 2, 98 | // disallow use of comma operator 99 | "no-sequences": 2, 100 | // restrict what can be thrown as an exception 101 | "no-throw-literal": 2, 102 | // disallow usage of expressions in statement position 103 | "no-unused-expressions": 2, 104 | // disallow unused variables/imports 105 | "no-unused-vars": [2, { "vars": "all", "args": "none" }], 106 | // disallow unnecessary .call() and .apply() 107 | "no-useless-call": 0, 108 | // disallow use of void operator 109 | "no-void": 0, 110 | // disallow usage of configurable warning terms in comments: e.g. todo 111 | "no-warning-comments": [0, { 112 | "terms": ["todo", "fixme", "xxx"], 113 | "location": "start" 114 | }], 115 | // disallow use of the with statement 116 | "no-with": 2, 117 | // require use of the second argument for parseInt() 118 | "radix": 2, 119 | // requires to declare all vars on top of their containing scope 120 | "vars-on-top": 2, 121 | // require immediate function invocation to be wrapped in parentheses 122 | "wrap-iife": [2, "any"], 123 | // require or disallow Yoda conditions 124 | "yoda": 2, 125 | // enforce spacing inside array brackets 126 | "array-bracket-spacing": 2, 127 | // enforce one true brace style 128 | "brace-style": [2, "1tbs", { 129 | "allowSingleLine": true 130 | }], 131 | // require camel case names 132 | "camelcase": [2, { 133 | "properties": "never" 134 | }], 135 | // enforce spacing before and after comma 136 | "comma-spacing": [2, { 137 | "before": false, 138 | "after": true 139 | }], 140 | // enforce one true comma style 141 | "comma-style": [2, "last"], 142 | // require or disallow padding inside computed properties 143 | "computed-property-spacing": 2, 144 | // enforces consistent naming when capturing the current execution context 145 | "consistent-this": 0, 146 | // enforce newline at the end of file, with no multiple empty lines 147 | "eol-last": 2, 148 | // require function expressions to have a name 149 | "func-names": 0, 150 | // enforces use of function declarations or expressions 151 | "func-style": 0, 152 | // this option enforces minimum and maximum identifier lengths (variable names, property names etc.) 153 | "id-length": 0, 154 | // this option sets a specific tab width for your code 155 | "indent": [2, 2, { "SwitchCase": 1 }], 156 | // enforces spacing between keys and values in object literal properties 157 | "key-spacing": [2, { 158 | "beforeColon": false, 159 | "afterColon": true 160 | }], 161 | "keyword-spacing": 2, 162 | // enforces empty lines around comments 163 | "lines-around-comment": 0, 164 | // disallow mixed "LF" and "CRLF" as linebreaks 165 | "linebreak-style": [2, "unix"], 166 | // specify the maximum depth callbacks can be nested 167 | "max-nested-callbacks": 0, 168 | // require a capital letter for constructors 169 | "new-cap": [2, { 170 | "newIsCap": true 171 | }], 172 | // disallow the omission of parentheses when invoking a constructor with no arguments 173 | "new-parens": 2, 174 | // allow/disallow an empty newline after var statement 175 | "newline-after-var": 0, 176 | // disallow use of the Array constructor 177 | "no-array-constructor": 0, 178 | // disallow use of the continue statement 179 | "no-continue": 0, 180 | // disallow comments inline after code 181 | "no-inline-comments": 0, 182 | // disallow if as the only statement in an else block 183 | "no-lonely-if": 0, 184 | // disallow mixed spaces and tabs for indentation 185 | "no-mixed-spaces-and-tabs": 2, 186 | // disallow multiple empty lines 187 | "no-multiple-empty-lines": [2, { 188 | "max": 2 189 | }], 190 | // disallow nested ternary expressions 191 | "no-nested-ternary": 2, 192 | // disallow use of the Object constructor 193 | "no-new-object": 2, 194 | // disallow space between function identifier and application 195 | "no-spaced-func": 2, 196 | // disallow the use of ternary operators 197 | "no-ternary": 0, 198 | // disallow trailing whitespace at the end of lines 199 | "no-trailing-spaces": 2, 200 | // disallow dangling underscores in identifiers 201 | "no-underscore-dangle": 0, 202 | // disallow the use of Boolean literals in conditional expressions 203 | "no-unneeded-ternary": 2, 204 | // require or disallow padding inside curly braces 205 | "object-curly-spacing": [2, "always"], 206 | // allow just one var statement per function 207 | "one-var": [2, "never"], 208 | // require assignment operator shorthand where possible or prohibit it entirely 209 | "operator-assignment": 0, 210 | // enforce operators to be placed before or after line breaks 211 | "operator-linebreak": 0, 212 | // enforce padding within blocks 213 | "padded-blocks": [2, "never"], 214 | // require quotes around object literal property names 215 | "quote-props": [2, "as-needed"], 216 | // specify whether double or single quotes should be used 217 | "quotes": [2, "single", "avoid-escape"], 218 | // require identifiers to match the provided regular expression 219 | "id-match": 0, 220 | // enforce spacing before and after semicolons 221 | "semi-spacing": [2, { 222 | "before": false, 223 | "after": true 224 | }], 225 | // require or disallow use of semicolons instead of ASI 226 | "semi": [2, "always"], 227 | // sort variables within the same declaration block 228 | "sort-vars": 0, 229 | // require or disallow space before blocks 230 | "space-before-blocks": 2, 231 | // require or disallow space before function opening parenthesis 232 | "space-before-function-paren": [0, { "anonymous": "always", "named": "never" }], 233 | // require or disallow spaces inside parentheses 234 | "space-in-parens": 0, 235 | // require spaces around operators 236 | "space-infix-ops": 2, 237 | // Require or disallow spaces before/after unary operators 238 | "space-unary-ops": 2, 239 | 240 | /* ES6+ */ 241 | // disallow using `var`. Must use `let` or `const` 242 | "no-var": 2, 243 | "no-class-assign": 2, 244 | "no-const-assign": 2, 245 | "no-dupe-class-members": 2, 246 | "no-this-before-super": 2, 247 | "prefer-const": 0, 248 | "prefer-spread": 2, 249 | // require object literal shorthand 250 | "object-shorthand": [2, "always"], 251 | "arrow-spacing": 2, 252 | "prefer-arrow-callback": 2, 253 | "arrow-parens": [0, "as-needed"], 254 | 255 | "react/jsx-uses-vars": 2, 256 | "react/jsx-uses-react": 2 257 | }, 258 | "env": { 259 | "browser": true, 260 | "es6": true, 261 | "mocha": true 262 | }, 263 | "globals": { 264 | "require": true 265 | }, 266 | "ecmaFeatures": { 267 | "jsx": true 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | temp 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Markus Coetzee 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 | # react-udeo (experimental) 2 | React bindings for [Udeo](https://github.com/mcoetzee/udeo) 3 | 4 | Provides: 5 | 6 | - A Connector to connect React components to the Udeo store 7 | - A Provider (ala Redux) to provide the Udeo store to the connected components 8 | 9 | ## Install 10 | 11 | NOTE: This has peer dependencies of `rxjs@5.0.*` and React 0.14 or later 12 | 13 | ```sh 14 | npm install --save react-udeo 15 | ``` 16 | ## Connector 17 | ### Basic Usage 18 | Subscribing to a single state stream: 19 | ```js 20 | import { Connector } from 'react-udeo'; 21 | import React from 'react'; 22 | 23 | class Finder extends React.Component { 24 | render() { 25 | ... 26 | } 27 | } 28 | 29 | export default new Connector(Finder) 30 | // Subscribe to single module's state stream 31 | .withStateFrom('finder') 32 | // By default will clear module's state on unmount 33 | .build(); 34 | ``` 35 | Only mapping the store's dispatch to props (no state stream subscription): 36 | ```js 37 | export default new Connector(SearchBox) 38 | // Map the store's dispatch to props 39 | .mapDispatchTo( 40 | dispatch => ({ 41 | onSearch(query) { 42 | dispatch({ type: 'SEARCH', payload: query }); 43 | } 44 | }) 45 | ) 46 | .build(); 47 | ``` 48 | 49 | ### More Advanced Usage 50 | Subscribing to multiple state streams and selectively clearing state on unmount: 51 | ```js 52 | export default new Connector(Finder) 53 | // Subscribe to multiple state streams 54 | .withStateFrom('finder', 'shoppingCart') 55 | // Map the state from multiple modules to props 56 | .mapStateTo( 57 | (finderState, cartState) => ({ 58 | ...finderState, 59 | totalCost: cartState.totalCost, 60 | }) 61 | ) 62 | // Map the store's dispatch to props 63 | .mapDispatchTo( 64 | dispatch => ({ 65 | onSearch(query) { 66 | dispatch({ type: 'SEARCH', payload: query }); 67 | } 68 | }) 69 | ) 70 | // Specify what to do with the state when component unmounts 71 | .clearStateOnUnmount({ 72 | finder: true, 73 | shoppingCart: false, 74 | }) 75 | .build(); 76 | ``` 77 | Hydrating the state stream with computed props: 78 | ```js 79 | export default new Connector(AvailabilityChecker) 80 | .withStateFrom('availability') 81 | .hydrateWith( 82 | props => ({ 83 | availability: { 84 | arrival: props.vacationStartDate, 85 | departure: new Moment(props.vacationStartDate).add(3, 'days') 86 | } 87 | }) 88 | ) 89 | .build(); 90 | ``` 91 | ## Provider 92 | ```js 93 | import React from 'react'; 94 | import { render } from 'react-dom'; 95 | import App from './components/App'; 96 | import { createStore } from 'udeo'; 97 | import { Provider } from 'react-udeo'; 98 | ... 99 | 100 | const store = createStore(...); 101 | 102 | render( 103 | 104 | 105 | , 106 | document.getElementById('root') 107 | ); 108 | ``` 109 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-udeo", 3 | "version": "0.0.0-alpha.2", 4 | "description": "React bindings for Udeo", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "lint": "eslint src && eslint test", 8 | "build": "npm run lint && rm -rf lib && babel src -d lib", 9 | "build_tests": "rm -rf temp && babel test -d temp", 10 | "clean": "rimraf ./lib; rimraf ./temp;", 11 | "test": "npm run build && npm run build_tests && mocha --require ./temp/setup.js temp", 12 | "prepublish": "npm test" 13 | }, 14 | "files": [ 15 | "lib", 16 | "README.md", 17 | "LICENSE" 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/mcoetzee/react-udeo.git" 22 | }, 23 | "keywords": [ 24 | "Rx", 25 | "RxJS", 26 | "state", 27 | "streams", 28 | "unidirectional", 29 | "data", 30 | "flow", 31 | "observable", 32 | "reactive", 33 | "programming" 34 | ], 35 | "author": "Markus Coetzee ", 36 | "license": "MIT", 37 | "bugs": { 38 | "url": "https://github.com/mcoetzee/react-udeo/issues" 39 | }, 40 | "homepage": "https://github.com/mcoetzee/react-udeo#readme", 41 | "peerDependencies": { 42 | "rxjs": "^5.0.0-beta.6", 43 | "react": "^0.14.0 || ^15.0.0-0" 44 | }, 45 | "dependencies": { 46 | "invariant": "^2.0.0" 47 | }, 48 | "devDependencies": { 49 | "babel-cli": "^6.7.5", 50 | "babel-eslint": "^6.0.3", 51 | "babel-plugin-transform-es2015-modules-commonjs": "^6.7.4", 52 | "babel-plugin-transform-function-bind": "^6.5.2", 53 | "babel-plugin-transform-object-rest-spread": "^6.6.5", 54 | "babel-polyfill": "^6.7.4", 55 | "babel-preset-es2015": "^6.6.0", 56 | "babel-preset-react": "6.5.0", 57 | "babel-register": "^6.7.2", 58 | "chai": "^3.5.0", 59 | "enzyme": "2.3.0", 60 | "eslint": "^2.10.2", 61 | "eslint-plugin-react": "^3.6.3", 62 | "jsdom": "9.2.0", 63 | "mocha": "^2.4.5", 64 | "react": "^0.14.0", 65 | "react-addons-test-utils": "^0.14.0", 66 | "react-dom": "^0.14.0", 67 | "rimraf": "^2.5.2", 68 | "rxjs": "^5.0.0-beta.6", 69 | "udeo": "^0.0.0-alpha.2" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Connector.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Observable } from 'rxjs'; 3 | import invariant from 'invariant'; 4 | import { storeShape } from './storeShape'; 5 | 6 | /** 7 | * Connects the provided component to the Udeo store. 8 | * @param Component - The React component to be connected to the store 9 | * @param [store] - The Udeo store. Optionally provided in order to use the Connector without 10 | * the Provider 11 | */ 12 | export function Connector(Component, store) { 13 | this.Component = Component; 14 | this.store = store; 15 | this.moduleNames = []; 16 | this.mapper = state => state; 17 | this.dispatchMapper = undefined; 18 | this.clearOnUnmount = true; 19 | } 20 | 21 | Connector.prototype.withStateFrom = function(...moduleNames) { 22 | this.moduleNames = moduleNames; 23 | return this; 24 | }; 25 | 26 | Connector.prototype.hydrateWith = function(hydrator) { 27 | this.hydrator = hydrator; 28 | return this; 29 | }; 30 | 31 | Connector.prototype.mapStateTo = function(mapper) { 32 | this.mapper = mapper; 33 | return this; 34 | }; 35 | 36 | Connector.prototype.mapDispatchTo = function(dispatchMapper) { 37 | this.dispatchMapper = dispatchMapper; 38 | return this; 39 | }; 40 | 41 | Connector.prototype.clearStateOnUnmount = function(clearOnUnmount) { 42 | this.clearOnUnmount = clearOnUnmount; 43 | return this; 44 | }; 45 | 46 | function getSingeState$(store, builder, hdrt) { 47 | let state$ = store.getState$(builder.moduleNames[0]); 48 | if (hdrt) { 49 | state$ = state$.filter(state => state.hydrated); 50 | } 51 | return state$.map(builder.mapper); 52 | } 53 | 54 | function getCombinedState$(store, builder, hdrt) { 55 | const state$s = builder.moduleNames.map(moduleName => { 56 | let state$ = store.getState$(moduleName); 57 | if (hdrt && hdrt[moduleName]) { 58 | state$ = state$.filter(state => state.hydrated); 59 | } 60 | return state$; 61 | }); 62 | return Observable.combineLatest(...state$s, builder.mapper); 63 | } 64 | 65 | Connector.prototype.build = function() { 66 | const builder = this; 67 | const connectorDisplayName = 'Connector(' + (builder.Component.displayName || builder.Component.name || 'Unknown') + ')'; 68 | 69 | class Connected extends React.Component { 70 | constructor(props, context) { 71 | super(props, context); 72 | this.store = builder.store || context.store; 73 | invariant(this.store, 74 | `The "store" was neither provided to the Connector or found in the context of "${connectorDisplayName}". ` + 75 | 'Either wrap the root component in a , or explicitly pass the "store" to the Connector.' 76 | ); 77 | } 78 | 79 | componentWillMount() { 80 | this.dispatchProps = builder.dispatchMapper ? builder.dispatchMapper(this.store.dispatch) : {}; 81 | 82 | if (!builder.moduleNames.length) { 83 | return; 84 | } 85 | 86 | const hdrt = builder.hydrator ? builder.hydrator(this.props) : undefined; 87 | 88 | const state$ = builder.moduleNames.length === 1 89 | ? getSingeState$(this.store, builder, hdrt) 90 | : getCombinedState$(this.store, builder, hdrt); 91 | 92 | this.subscription = state$.subscribe(state => this.setState(state)); 93 | 94 | if (hdrt) { 95 | builder.moduleNames.forEach(moduleName => { 96 | if (hdrt[moduleName]) { 97 | this.store.hydrate(moduleName, hdrt[moduleName]); 98 | } 99 | }); 100 | } 101 | } 102 | 103 | componentWillUnmount() { 104 | if (!builder.moduleNames.length) { 105 | return; 106 | } 107 | 108 | if (builder.clearOnUnmount) { 109 | const defined = typeof builder.clearOnUnmount === 'object'; 110 | builder.moduleNames.forEach(moduleName => { 111 | if (defined && !builder.clearOnUnmount[moduleName]) { 112 | return; 113 | } 114 | this.store.clearState(moduleName); 115 | }); 116 | } 117 | this.subscription.unsubscribe(); 118 | } 119 | 120 | render() { 121 | const { Component } = builder; 122 | return ; 123 | } 124 | } 125 | Connected.displayName = connectorDisplayName; 126 | Connected.contextTypes = { 127 | store: storeShape 128 | }; 129 | return Connected; 130 | }; 131 | -------------------------------------------------------------------------------- /src/Provider.js: -------------------------------------------------------------------------------- 1 | import { Component, PropTypes, Children } from 'react'; 2 | import { storeShape } from './storeShape'; 3 | 4 | export class Provider extends Component { 5 | getChildContext() { 6 | return { store: this.store }; 7 | } 8 | 9 | constructor(props, context) { 10 | super(props, context); 11 | this.store = props.store; 12 | } 13 | 14 | render() { 15 | return Children.only(this.props.children); 16 | } 17 | } 18 | 19 | Provider.propTypes = { 20 | store: storeShape.isRequired, 21 | children: PropTypes.element.isRequired 22 | }; 23 | 24 | Provider.childContextTypes = { 25 | store: storeShape.isRequired 26 | }; 27 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { Connector } from './Connector'; 2 | export { Provider } from './Provider'; 3 | -------------------------------------------------------------------------------- /src/storeShape.js: -------------------------------------------------------------------------------- 1 | import { PropTypes } from 'react'; 2 | 3 | export const storeShape = PropTypes.shape({ 4 | getState$: PropTypes.func.isRequired, 5 | dispatch: PropTypes.func.isRequired, 6 | hydrate: PropTypes.func.isRequired, 7 | clearState: PropTypes.func.isRequired, 8 | }); 9 | -------------------------------------------------------------------------------- /test/Connector-spec.js: -------------------------------------------------------------------------------- 1 | /* globals describe it */ 2 | import React from 'react'; 3 | import { expect } from 'chai'; 4 | import { createStore } from 'udeo'; 5 | import { Connector, Provider } from '../'; 6 | import { mount } from 'enzyme'; 7 | 8 | const FOO = '@test/FOO'; 9 | const BAR = '@test/BAR'; 10 | 11 | describe('Connector', () => { 12 | describe('with state from a single module', () => { 13 | it('receives state updates', () => { 14 | const initialState = { foos: ['Tommy'] }; 15 | const fooModule = { 16 | flow(dispatch$) { 17 | return [ 18 | dispatch$.filterAction(FOO) 19 | ]; 20 | }, 21 | reducer(state = initialState, action) { 22 | switch (action.type) { 23 | case FOO: 24 | return { 25 | ...state, 26 | foos: state.foos.concat(action.payload) 27 | }; 28 | default: 29 | return state; 30 | } 31 | } 32 | }; 33 | 34 | const store = createStore({ fooModule }); 35 | 36 | const Component = ({ foos }) => ( 37 |
38 | {foos.map((f, i) =>

-{f}-

)} 39 |
40 | ); 41 | 42 | const Connected = new Connector(Component) 43 | .withStateFrom('fooModule') 44 | .build(); 45 | 46 | const mounted = mount( 47 | 48 | 49 | 50 | ); 51 | 52 | expect(mounted.text()).to.eq('-Tommy-'); 53 | 54 | store.dispatch({ type: FOO, payload: 'Shelby' }); 55 | expect(mounted.text()).to.eq('-Tommy--Shelby-'); 56 | 57 | store.dispatch({ type: BAR, payload: 'Arthur' }); 58 | expect(mounted.text()).to.eq('-Tommy--Shelby-'); 59 | }); 60 | 61 | it('hydrates with computed props', () => { 62 | const initialState = { value: 0 }; 63 | const fooModule = { 64 | flow(dispatch$) { 65 | return [ 66 | dispatch$.filterAction(FOO) 67 | ]; 68 | }, 69 | reducer(state = initialState, action) { 70 | return state; 71 | } 72 | }; 73 | 74 | const store = createStore({ fooModule }); 75 | 76 | const Component = ({ value }) => ( 77 |
78 | The answer is {value} 79 |
80 | ); 81 | 82 | const Connected = new Connector(Component) 83 | .withStateFrom('fooModule') 84 | .hydrateWith( 85 | props => ({ 86 | fooModule: { value: props.initialValue + 10 } 87 | }) 88 | ) 89 | .build(); 90 | 91 | const mounted = mount( 92 | 93 | 94 | 95 | ); 96 | 97 | expect(mounted.text()).to.eq('The answer is 42'); 98 | }); 99 | 100 | it('maps state', () => { 101 | const initialState = { valueA: 21, valueB: 42 }; 102 | const fooModule = { 103 | flow(dispatch$) { 104 | return [ 105 | dispatch$.filterAction(FOO) 106 | ]; 107 | }, 108 | reducer(state = initialState, action) { 109 | return state; 110 | } 111 | }; 112 | 113 | const store = createStore({ fooModule }); 114 | 115 | const Component = ({ valueA }) => ( 116 |
117 | Value A is {valueA} 118 |
119 | ); 120 | 121 | const Connected = new Connector(Component) 122 | .withStateFrom('fooModule') 123 | .mapStateTo( 124 | fooState => ({ 125 | valueA: fooState.valueA, 126 | }) 127 | ) 128 | .build(); 129 | 130 | const mounted = mount( 131 | 132 | 133 | 134 | ); 135 | 136 | expect(mounted.find(Component).props()).to.have.all.keys(['valueA']); 137 | expect(mounted.find(Component).text()).to.eq('Value A is 21'); 138 | }); 139 | 140 | it('maps dispatch', () => { 141 | const initialState = { valueA: 20, valueB: 42 }; 142 | const fooModule = { 143 | flow(dispatch$) { 144 | return [ 145 | dispatch$.filterAction(FOO) 146 | ]; 147 | }, 148 | reducer(state = initialState, action) { 149 | switch (action.type) { 150 | case FOO: 151 | return { 152 | ...state, 153 | valueA: state.valueA * 2, 154 | valueB: state.valueB * 2, 155 | }; 156 | default: 157 | return state; 158 | } 159 | } 160 | }; 161 | 162 | const store = createStore({ fooModule }); 163 | 164 | const Component = ({ valueA, valueB, onDubble }) => ( 165 |
166 | A: {valueA} B: {valueB} 167 | 168 |
169 | ); 170 | 171 | const Connected = new Connector(Component) 172 | .withStateFrom('fooModule') 173 | .mapDispatchTo( 174 | dispatch => ({ 175 | onDubble() { 176 | dispatch({ type: FOO }); 177 | } 178 | }) 179 | ) 180 | .build(); 181 | 182 | const mounted = mount( 183 | 184 | 185 | 186 | ); 187 | 188 | expect(mounted.find(Component).props()).to.have.all.keys(['valueA', 'valueB', 'onDubble']); 189 | expect(mounted.find('.values').text()).to.eq('A: 20 B: 42'); 190 | 191 | mounted.find('#dubble').simulate('click'); 192 | expect(mounted.find('.values').text()).to.eq('A: 40 B: 84'); 193 | 194 | mounted.find('#dubble').simulate('click'); 195 | expect(mounted.find('.values').text()).to.eq('A: 80 B: 168'); 196 | }); 197 | 198 | it('clears state on unmount', () => { 199 | const initialState = { valueA: 20, valueB: 42 }; 200 | const fooModule = { 201 | flow(dispatch$) { 202 | return [ 203 | dispatch$.filterAction(FOO) 204 | ]; 205 | }, 206 | reducer(state = initialState, action) { 207 | switch (action.type) { 208 | case FOO: 209 | return { 210 | ...state, 211 | valueA: state.valueA * 2, 212 | valueB: state.valueB * 2, 213 | }; 214 | default: 215 | return state; 216 | } 217 | } 218 | }; 219 | 220 | const store = createStore({ fooModule }); 221 | 222 | const Component = ({ onDubble }) => ( 223 | 224 | ); 225 | 226 | const Connected = new Connector(Component) 227 | .withStateFrom('fooModule') 228 | .mapDispatchTo( 229 | dispatch => ({ 230 | onDubble() { 231 | dispatch({ type: FOO }); 232 | } 233 | }) 234 | ) 235 | .build(); 236 | 237 | const mounted = mount( 238 | 239 | 240 | 241 | ); 242 | 243 | let fooState; 244 | store.getState$('fooModule').subscribe(state => { 245 | fooState = state; 246 | }); 247 | 248 | mounted.find('#dubble').simulate('click'); 249 | mounted.find('#dubble').simulate('click'); 250 | 251 | expect(fooState).to.deep.eq({ valueA: 80, valueB: 168 }); 252 | 253 | mounted.unmount(); 254 | expect(fooState).to.deep.eq({ valueA: 20, valueB: 42 }); 255 | }); 256 | }); 257 | }); 258 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | /* globals global */ 2 | import { jsdom } from 'jsdom'; 3 | 4 | global.document = jsdom(''); 5 | global.window = document.defaultView; 6 | global.navigator = global.window.navigator; 7 | --------------------------------------------------------------------------------