├── .gitignore ├── .npmignore ├── ConnectBase.js ├── FluxContext.js ├── README.md ├── connect.js ├── package.json ├── src ├── ConnectBase.js ├── FluxContext.js ├── connect.js ├── index.js └── supplyFluxContext.js ├── supplyFluxContext.js └── test ├── babel └── index.js └── connect-to-stores-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | -------------------------------------------------------------------------------- /ConnectBase.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/ConnectBase') 2 | -------------------------------------------------------------------------------- /FluxContext.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/FluxContext') 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # alt-react 2 | 3 | * Connect your react component to your flux store. 4 | * Automatically pass the flux instance to your component. 5 | * Has hooks for shouldComponentUpdate, didMount, willMount, etc. 6 | * Can be extended to create your own connectors. 7 | 8 | Example 9 | 10 | ```js 11 | import { connect } from 'alt-react' 12 | import React from 'react' 13 | import UserStore from '../stores/UserStore' 14 | 15 | class MyComponent extends React.Component { 16 | render() { 17 | return
Hello, {this.props.userName}!
18 | } 19 | } 20 | 21 | connect(MyComponent, { 22 | listenTo() { 23 | return [UserStore] 24 | }, 25 | 26 | getProps() { 27 | return { 28 | userName: UserStore.getUserName(), 29 | } 30 | }, 31 | }) 32 | ``` 33 | 34 | and providing the flux context at your root component 35 | 36 | ```js 37 | import { supplyFluxContext } from 'alt-react' 38 | 39 | export default supplyFluxContext(alt)(Root) 40 | ``` 41 | -------------------------------------------------------------------------------- /connect.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/connect.js') 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alt-react", 3 | "version": "0.0.1", 4 | "description": "Connect flux to react", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "npm run clean && npm run transpile", 8 | "clean": "rimraf lib", 9 | "pretest": "npm run clean && npm run transpile", 10 | "test": "mocha -u exports -R nyan --require ./test/babel test", 11 | "transpile": "babel src --out-dir lib --stage 0" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git@github.com:altjs/react.git" 16 | }, 17 | "keywords": [ 18 | "context", 19 | "instance", 20 | "flux", 21 | "isomorphic", 22 | "universal", 23 | "react", 24 | "connect", 25 | "stores", 26 | "alt" 27 | ], 28 | "author": "Josh Perez ", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/altjs/react/issues" 32 | }, 33 | "homepage": "https://github.com/altjs/react", 34 | "devDependencies": { 35 | "alt": "0.17.4", 36 | "babel": "5.8.23", 37 | "babel-core": "5.8.25", 38 | "chai": "3.3.0", 39 | "jsdom": "7.0.1", 40 | "mocha": "2.3.3", 41 | "react": "0.14.0", 42 | "react-addons-test-utils": "0.14.0", 43 | "react-dom": "0.14.0", 44 | "rimraf": "2.4.3", 45 | "sinon": "1.17.1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/ConnectBase.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | 3 | export default class Connect extends React.Component { 4 | static contextTypes = { 5 | flux: PropTypes.object, 6 | } 7 | 8 | setConnections(props, context, config = {}) { 9 | this.flux = props.flux || context.flux 10 | this.stores = this.flux ? this.flux.stores : {} 11 | this.config = typeof config === 'function' 12 | ? config(this.stores, this.flux) 13 | : config 14 | } 15 | 16 | componentWillMount() { 17 | if (this.config.willMount) this.call(this.config.willMount) 18 | } 19 | 20 | componentDidMount() { 21 | const stores = this.config.listenTo ? this.call(this.config.listenTo) : [] 22 | this.storeListeners = stores.map((store) => { 23 | return store.listen(() => this.forceUpdate()) 24 | }) 25 | 26 | if (this.config.didMount) this.call(this.config.didMount) 27 | } 28 | 29 | componentWillUnmount() { 30 | this.storeListeners.forEach(unlisten => unlisten()) 31 | if (this.config.willUnmount) this.call(this.config.willUnmount) 32 | } 33 | 34 | componentWillReceiveProps(nextProps) { 35 | if (this.config.willReceiveProps) this.call(this.config.willReceiveProps) 36 | } 37 | 38 | shouldComponentUpdate(nextProps) { 39 | return this.config.shouldComponentUpdate 40 | ? this.call(this.config.shouldComponentUpdate, nextProps) 41 | : true 42 | } 43 | 44 | getNextProps(nextProps = this.props) { 45 | return this.config.getProps 46 | ? this.call(this.config.getProps, nextProps) 47 | : nextProps 48 | } 49 | 50 | call(f, props = this.props) { 51 | return f(props, this.context, this.flux) 52 | } 53 | 54 | render() { 55 | throw new Error('Render should be defined in your own class') 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/FluxContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default React.createClass({ 4 | childContextTypes: { 5 | flux: React.PropTypes.object, 6 | }, 7 | 8 | getChildContext() { 9 | return { flux: this.props.flux } 10 | }, 11 | 12 | render() { 13 | return this.props.children 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /src/connect.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ConnectBase from './ConnectBase' 3 | 4 | export default (Component, config = {}) => { 5 | return class extends ConnectBase { 6 | static displayName = `Stateful${Component.displayName || Component.name}Container` 7 | static contextTypes = Component.contextTypes || config.contextTypes || {} 8 | 9 | constructor(props, context) { 10 | super(props, context) 11 | this.setConnections(props, context, config) 12 | } 13 | 14 | render() { 15 | return ( 16 | 21 | ) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import connect from './connect' 2 | import supplyFluxContext from './supplyFluxContext' 3 | 4 | export default { connect, supplyFluxContext } 5 | -------------------------------------------------------------------------------- /src/supplyFluxContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default flux => Component => React.createClass({ 4 | childContextTypes: { 5 | flux: React.PropTypes.object, 6 | }, 7 | 8 | getChildContext() { 9 | return { flux } 10 | }, 11 | 12 | render() { 13 | return 14 | }, 15 | }) 16 | -------------------------------------------------------------------------------- /supplyFluxContext.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/supplyFluxContext') 2 | -------------------------------------------------------------------------------- /test/babel/index.js: -------------------------------------------------------------------------------- 1 | require('babel-core/external-helpers') 2 | require('babel/register-without-polyfill')({ 3 | stage: 0 4 | }) 5 | -------------------------------------------------------------------------------- /test/connect-to-stores-test.js: -------------------------------------------------------------------------------- 1 | import { jsdom } from 'jsdom' 2 | import Alt from 'alt' 3 | import React from 'react' 4 | import ReactDom from 'react-dom' 5 | import ReactDomServer from 'react-dom/server' 6 | import connectToStores from '../' 7 | import { assert } from 'chai' 8 | import sinon from 'sinon' 9 | import TestUtils from 'react-addons-test-utils' 10 | 11 | const alt = new Alt() 12 | 13 | const testActions = alt.generateActions('updateFoo') 14 | 15 | const testStore = alt.createStore( 16 | class TestStore { 17 | constructor() { 18 | this.bindAction(testActions.updateFoo, this.onChangeFoo) 19 | this.foo = 'Bar' 20 | } 21 | onChangeFoo(newValue) { 22 | this.foo = newValue 23 | } 24 | } 25 | ) 26 | 27 | export default { 28 | 'connectToStores wrapper': { 29 | beforeEach() { 30 | global.document = jsdom('') 31 | global.window = global.document.defaultView 32 | 33 | alt.recycle() 34 | }, 35 | 36 | afterEach() { 37 | delete global.document 38 | delete global.window 39 | }, 40 | 41 | 'resolve props on re-render'() { 42 | const FooStore = alt.createStore(function () { 43 | this.x = 1 44 | }, 'FooStore') 45 | 46 | const getProps = sinon.stub().returns(FooStore.getState()) 47 | 48 | const Child = connectToStores(React.createClass({ 49 | render() { 50 | return {this.props.x + this.props.y} 51 | } 52 | }), { 53 | listenTo(props) { 54 | return [FooStore] 55 | }, 56 | 57 | getProps 58 | }) 59 | 60 | const Parent = React.createClass({ 61 | getInitialState() { 62 | return { y: 0 } 63 | }, 64 | componentDidMount() { 65 | this.setState({ y: 1 }) 66 | }, 67 | render() { 68 | return 69 | } 70 | }) 71 | 72 | const node = TestUtils.renderIntoDocument( 73 | 74 | ) 75 | 76 | assert(getProps.callCount === 2, 'getProps called twice') 77 | 78 | const span = TestUtils.findRenderedDOMComponentWithTag(node, 'span') 79 | assert(span.innerHTML === '2', 'prop passed in is correct') 80 | }, 81 | 82 | 'element mounts and unmounts'() { 83 | const div = document.createElement('div') 84 | 85 | const LegacyComponent = connectToStores(React.createClass({ 86 | render() { 87 | return React.createElement('div', null, `Foo${this.props.delim}${this.props.foo}`) 88 | } 89 | }), { 90 | listenTo() { 91 | return [testStore] 92 | }, 93 | getProps(props) { 94 | return testStore.getState() 95 | } 96 | }) 97 | 98 | ReactDom.render( 99 | 100 | , div) 101 | 102 | ReactDom.unmountComponentAtNode(div) 103 | }, 104 | 105 | 'createClass() component can get props from stores'() { 106 | const LegacyComponent = React.createClass({ 107 | render() { 108 | return React.createElement('div', null, `Foo${this.props.delim}${this.props.foo}`) 109 | } 110 | }) 111 | 112 | const WrappedComponent = connectToStores(LegacyComponent, { 113 | listenTo() { 114 | return [testStore] 115 | }, 116 | getProps(props) { 117 | return testStore.getState() 118 | }, 119 | }) 120 | const element = React.createElement(WrappedComponent, {delim: ': '}) 121 | const output = ReactDomServer.renderToStaticMarkup(element) 122 | assert.include(output, 'Foo: Bar') 123 | }, 124 | 125 | 'component statics can see context properties'() { 126 | const Child = connectToStores(React.createClass({ 127 | contextTypes: { 128 | store: React.PropTypes.object 129 | }, 130 | render() { 131 | return Foo: {this.props.foo} 132 | } 133 | }), { 134 | listenTo(props, context) { 135 | return [context.store] 136 | }, 137 | getProps(props, context) { 138 | return context.store.getState() 139 | }, 140 | }) 141 | 142 | const ContextComponent = React.createClass({ 143 | getChildContext() { 144 | return { store: testStore } 145 | }, 146 | childContextTypes: { 147 | store: React.PropTypes.object 148 | }, 149 | render() { 150 | return 151 | } 152 | }) 153 | const element = React.createElement(ContextComponent) 154 | const output = ReactDomServer.renderToStaticMarkup(element) 155 | assert.include(output, 'Foo: Bar') 156 | }, 157 | 158 | 'component can get use stores from props'() { 159 | const LegacyComponent = React.createClass({ 160 | render() { 161 | return React.createElement('div', null, `Foo${this.props.delim}${this.props.foo}`) 162 | } 163 | }) 164 | 165 | const WrappedComponent = connectToStores(LegacyComponent, { 166 | listenTo(props) { 167 | return [props.store] 168 | }, 169 | getProps(props) { 170 | return props.store.getState() 171 | }, 172 | }) 173 | const element = React.createElement(WrappedComponent, {delim: ': ', store: testStore}) 174 | const output = ReactDomServer.renderToStaticMarkup(element) 175 | assert.include(output, 'Foo: Bar') 176 | }, 177 | 178 | 'ES6 class component responds to store events'() { 179 | class ClassComponent1 extends React.Component { 180 | render() { 181 | return {this.props.foo} 182 | } 183 | } 184 | 185 | const WrappedComponent = connectToStores(ClassComponent1, { 186 | listenTo() { 187 | return [testStore] 188 | }, 189 | getProps(props) { 190 | return testStore.getState() 191 | } 192 | }) 193 | 194 | const node = TestUtils.renderIntoDocument( 195 | 196 | ) 197 | 198 | testActions.updateFoo('Baz') 199 | 200 | const span = TestUtils.findRenderedDOMComponentWithTag(node, 'span') 201 | 202 | assert(span.innerHTML === 'Baz') 203 | }, 204 | 205 | 'componentDidConnect hook is called '() { 206 | let componentDidConnect = false 207 | class ClassComponent2 extends React.Component { 208 | render() { 209 | return 210 | } 211 | } 212 | const WrappedComponent = connectToStores(ClassComponent2, { 213 | listenTo() { 214 | return [testStore] 215 | }, 216 | getProps(props) { 217 | return testStore.getState() 218 | }, 219 | didMount() { 220 | componentDidConnect = true 221 | } 222 | }) 223 | const node = TestUtils.renderIntoDocument( 224 | 225 | ) 226 | assert(componentDidConnect === true) 227 | }, 228 | 229 | 'Component receives all updates'(done) { 230 | let componentDidConnect = false 231 | class ClassComponent3 extends React.Component { 232 | componentDidUpdate() { 233 | componentDidConnect = true 234 | assert(this.props.foo === 'Baz') 235 | done() 236 | } 237 | render() { 238 | return 239 | } 240 | } 241 | 242 | const WrappedComponent = connectToStores(ClassComponent3, { 243 | listenTo() { 244 | return [testStore] 245 | }, 246 | getProps(props) { 247 | return testStore.getState() 248 | }, 249 | didMount() { 250 | testActions.updateFoo('Baz') 251 | }, 252 | }) 253 | 254 | let node = TestUtils.renderIntoDocument( 255 | 256 | ) 257 | 258 | const span = TestUtils.findRenderedDOMComponentWithTag(node, 'span') 259 | assert(componentDidConnect === true) 260 | }, 261 | 262 | 263 | } 264 | } 265 | --------------------------------------------------------------------------------