├── .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 |
--------------------------------------------------------------------------------