Skip to content

Commit

Permalink
Implements connectToStores().
Browse files Browse the repository at this point in the history
  • Loading branch information
cpsubrian authored and goatslacker committed Apr 19, 2015
1 parent 20c8382 commit a568bfb
Show file tree
Hide file tree
Showing 6 changed files with 358 additions and 23 deletions.
116 changes: 113 additions & 3 deletions dist/alt-with-addons.js
Original file line number Diff line number Diff line change
Expand Up @@ -1165,6 +1165,8 @@ var DispatcherRecorder = _interopRequire(require("../utils/DispatcherRecorder"))

var atomicTransactions = _interopRequire(require("../utils/atomicTransactions"));

var connectToStores = _interopRequire(require("../utils/connectToStores"));

var chromeDebug = _interopRequire(require("../utils/chromeDebug"));

var makeFinalStore = _interopRequire(require("../utils/makeFinalStore"));
Expand All @@ -1180,12 +1182,13 @@ Alt.addons = {
DispatcherRecorder: DispatcherRecorder,
atomicTransactions: atomicTransactions,
chromeDebug: chromeDebug,
connectToStores: connectToStores,
makeFinalStore: makeFinalStore,
withAltContext: withAltContext };

module.exports = Alt;

},{"../../AltContainer":1,"../utils/ActionListeners":23,"../utils/AltManager":24,"../utils/DispatcherRecorder":25,"../utils/atomicTransactions":26,"../utils/chromeDebug":27,"../utils/makeFinalStore":28,"../utils/withAltContext":29,"./":14}],14:[function(require,module,exports){
},{"../../AltContainer":1,"../utils/ActionListeners":23,"../utils/AltManager":24,"../utils/DispatcherRecorder":25,"../utils/atomicTransactions":26,"../utils/chromeDebug":27,"../utils/connectToStores":28,"../utils/makeFinalStore":29,"../utils/withAltContext":30,"./":14}],14:[function(require,module,exports){
"use strict";

var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; };
Expand Down Expand Up @@ -2335,7 +2338,7 @@ function atomicTransactions(alt) {
};
}

},{"./makeFinalStore":28}],27:[function(require,module,exports){
},{"./makeFinalStore":29}],27:[function(require,module,exports){
/*global window*/
"use strict";

Expand All @@ -2346,6 +2349,113 @@ function chromeDebug(alt) {
}

},{}],28:[function(require,module,exports){
(function (global){
"use strict";

var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; };

/**
* 'Higher Order Component' that controls the props of a wrapped
* component via stores.
*
* Expects the Component to have two static methods:
* - getStores(): Should return an array of stores.
* - getPropsFromStores(props): Should return the props from the stores.
*
* Example using old React.createClass() style:
*
* const MyComponent = React.createClass({
* statics: {
* getStores() {
* return [myStore]
* },
* getPropsFromStores(props) {
* return myStore.getState()
* }
* },
* render() {
* // Use this.props like normal ...
* }
* })
* MyComponent = connectToStores(MyComponent)
*
*
* Example using ES6 Class:
*
* class MyComponent extends React.Component {
* static getStores() {
* return [myStore]
* }
* static getPropsFromStores(props) {
* return myStore.getState()
* }
* render() {
* // Use this.props like normal ...
* }
* }
* MyComponent = connectToStores(MyComponent)
*
* A great explanation of the merits of higher order components can be found at
* http://bit.ly/1abPkrP
*/

var React = _interopRequire((typeof window !== "undefined" ? window.React : typeof global !== "undefined" ? global.React : null));

var assign = _interopRequire(require("object-assign"));

function connectToStores(Component) {

// Check for required static methods.
if (typeof Component.getStores !== "function") {
throw new Error("connectToStores() expects the wrapped component to have a static getStores() method");
}
if (typeof Component.getPropsFromStores !== "function") {
throw new Error("connectToStores() expects the wrapped component to have a static getPropsFromStores() method");
}

// Cache stores.
var stores = Component.getStores();

// Wrapper Component.
var StoreConnection = React.createClass({
displayName: "StoreConnection",

getInitialState: function getInitialState() {
return Component.getPropsFromStores(this.props);
},

componentDidMount: function componentDidMount() {
var _this = this;

stores.forEach(function (store) {
store.listen(_this.onChange);
});
},

componentWillUnmount: function componentWillUnmount() {
var _this = this;

stores.forEach(function (store) {
store.unlisten(_this.onChange);
});
},

onChange: function onChange() {
this.setState(Component.getPropsFromStores(this.props));
},

render: function render() {
return React.createElement(Component, assign({}, this.props, this.state));
}
});

return StoreConnection;
}

module.exports = connectToStores;

}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"object-assign":10}],29:[function(require,module,exports){
"use strict";

module.exports = makeFinalStore;
Expand Down Expand Up @@ -2391,7 +2501,7 @@ function makeFinalStore(alt) {
return alt.createUnsavedStore(FinalStore);
}

},{}],29:[function(require,module,exports){
},{}],30:[function(require,module,exports){
(function (global){
"use strict";

Expand Down
2 changes: 1 addition & 1 deletion guides/getting-started/view.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ componentWillUnmount() {
},
```

A few [mixins](https://github.com/goatslacker/alt/tree/master/mixins) are available to make this boilerplate go away.
A few [mixins](https://github.com/goatslacker/alt/tree/master/mixins) or a ["higher-order-component"](https://github.com/goatslacker/alt/tree/master/utils/connectToStores.js) are available to make this boilerplate go away.

---

Expand Down
2 changes: 2 additions & 0 deletions src/alt/addons.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import AltManager from '../utils/AltManager'
import DispatcherRecorder from '../utils/DispatcherRecorder'

import atomicTransactions from '../utils/atomicTransactions'
import connectToStores from '../utils/connectToStores'
import chromeDebug from '../utils/chromeDebug'
import makeFinalStore from '../utils/makeFinalStore'
import withAltContext from '../utils/withAltContext'
Expand All @@ -18,6 +19,7 @@ Alt.addons = {
DispatcherRecorder,
atomicTransactions,
chromeDebug,
connectToStores,
makeFinalStore,
withAltContext,
}
Expand Down
109 changes: 109 additions & 0 deletions test/connect-to-stores.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import Alt from '../dist/alt-with-runtime'
import React from 'react'
import ReactComponent from './helpers/ReactComponent'
import connectToStores from '../utils/connectToStores'
import { assert } from 'chai'

const alt = new Alt();

const testActions = alt.createActions(
class TestActions {
updateFoo(newValue) {
this.dispatch(newValue)
}
}
)

const testStore = alt.createStore(
class TestStore {
constructor() {
this.bindAction(testActions.updateFoo, this.onChangeFoo)
this.foo = 'Bar'
}
onChangeFoo(newValue) {
this.foo = newValue
}
}
)

const BadComponentOne = React.createClass({
render() {
return React.createElement('div', null, 'Bad');
}
});
const BadComponentTwo = React.createClass({
statics: {
getStores() {
return [testStore]
}
},
render() {
return React.createElement('div', null, 'Bad');
}
})

const LegacyComponent = React.createClass({
statics: {
getStores() {
return [testStore]
},
getPropsFromStores(props) {
return testStore.getState()
}
},
render() {
return React.createElement('div', null, `Foo${this.props.delim}${this.props.foo}`)
}
})

class ClassComponent extends ReactComponent {
static getStores() {
return [testStore]
}
static getPropsFromStores(props) {
return testStore.getState()
}
render() {
// Will never get called due to mocked wrapper.
}
}

export default {
'connectToStores wrapper': {

'missing the static getStores() method should throw'() {
assert.throws(() => connectToStores(BadComponentOne), 'expects the wrapped component to have a static getStores() method')
},

'missing the static getPropsFromStores() method should throw'() {
assert.throws(() => connectToStores(BadComponentTwo), 'expects the wrapped component to have a static getPropsFromStores() method')
},

'createClass() component can get props from stores'() {
connectToStores.createClass = React.createClass
const WrappedComponent = connectToStores(LegacyComponent)
const element = React.createElement(WrappedComponent, {delim: ': '})
const output = React.renderToStaticMarkup(element)
assert.include(output, 'Foo: Bar')
},

'ES6 class component responds to store events'() {
let renderCalled = false
connectToStores.createClass = (spec) => {
class FakeComponent extends ReactComponent {}
Object.assign(FakeComponent.prototype, spec)
FakeComponent.prototype.render = function() {
assert.strictEqual(this.state.foo, 'Baz', 'wrapped component did not receive store changes')
renderCalled = true
}
return ReactComponent.prepare(FakeComponent)
}
const WrappedComponent = connectToStores(ClassComponent)
ReactComponent.test(WrappedComponent, () => {
testActions.updateFoo('Baz')
assert(renderCalled === true, 'render was never called')
}, {delim: ' - '})
}

}
}
49 changes: 30 additions & 19 deletions test/helpers/ReactComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,44 +4,55 @@ class ReactComponent {
this.render()
}

// A not at all react spec compliant way to test fake react components
static test(Component, testFn, props) {
// Create the mocked component.
static prepare(Component) {
const builtInProto = Object.getOwnPropertyNames(Component.prototype)

// A trolol way of doing the react <0.13 auto-binding.
function AutoBoundComponent() {
Component.call(this)

builtInProto.forEach((method) => {
if (method !== 'constructor') {
if (typeof this[method] === 'function' && method !== 'constructor') {
this[method] = this[method].bind(this)
}
})
}
AutoBoundComponent.prototype = Component.prototype

// initialize the component
const component = new AutoBoundComponent()

Component.mixins.forEach((mixin) => {
// transfer over the mixins.
Object.keys(mixin).forEach((method) => {
component[method] = mixin[method].bind(component)
// Move over the statics.
if (Component.statics) {
Object.keys(Component.statics).forEach((stat) => {
Component[stat] = Component.statics[stat]
})
}

return AutoBoundComponent
}

// move over the statics
if (Component.statics) {
Object.keys(Component.statics).forEach((stat) => {
Component[stat] = Component.statics[stat]
// A not at all react spec compliant way to test fake react components.
static test(Component, testFn, props = {}) {
// Prepare the class.
const PreparedComponent = ReactComponent.prepare(Component)

// Initialize the component.
const component = new PreparedComponent()

// Transfer over the mixins.
if (Component.mixins) {
Component.mixins.forEach((mixin) => {
Object.keys(mixin).forEach((method) => {
component[method] = mixin[method].bind(component)
})
}
})
})
}

// call the lifecycle methods and the test function
// Call the lifecycle methods and the test function.
try {
component.props = component.getDefaultProps
let defaultProps = component.getDefaultProps
? component.getDefaultProps()
: {}
component.props = Object.assign({}, defaultProps, props)

component.state = component.getInitialState
? component.getInitialState()
Expand All @@ -54,7 +65,7 @@ class ReactComponent {
} catch (e) {
throw e;
} finally {
// end with last lifecycle method
// End with last lifecycle method.
component.componentWillUnmount && component.componentWillUnmount();
}
}
Expand Down
Loading

0 comments on commit a568bfb

Please sign in to comment.