Skip to content

Commit

Permalink
Adds AltContainer
Browse files Browse the repository at this point in the history
  • Loading branch information
goatslacker committed Mar 31, 2015
1 parent 07745f5 commit efbd652
Show file tree
Hide file tree
Showing 8 changed files with 664 additions and 5 deletions.
147 changes: 147 additions & 0 deletions components/AltContainer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/**
* AltContainer.
*
* There are many ways to use AltContainer.
*
* Using the `stores` prop.
*
* <AltContainer stores={{ FooStore: FooStore }}>
* children get this.props.FootStore.storeData
* </AltContainer>
*
* You can also pass in functions.
*
* <AltContainer stores={{ FooStore: function () { return { storeData: true } } }}>
* children get this.props.FootStore.storeData
* </AltContainer>
*
* Using the `store` prop.
*
* <AltContainer store={FooStore}>
* children get this.props.storeData
* </AltContainer>
*
* Passing in `flux` because you're using alt instances
*
* <AltContainer flux={flux}>
* children get this.props.flux
* </AltContainer>
*
* Using a custom render function.
*
* <AltContainer
* render={function (props) {
* return <div />;
* }}
* />
*
* Full docs available at http://goatslacker.github.io/alt/
*/
var React = require('react/addons')
var Subscribe = require('../mixins/Subscribe')
var assign = require('object-assign')

var cloneWithProps = React.addons.cloneWithProps

function getState(store, props) {
return typeof store === 'function' ? store(props) : store.getState()
}

var AltContainer = React.createClass({
getInitialState: function () {
if (this.props.stores && this.props.store) {
throw new ReferenceError('Cannot define both store and stores')
}

return this.getStateFromStores() || {}
},

componentDidMount: function () {
Subscribe.create(this)

if (this.props.store) {
this.addSubscription(this.props.store)
} else if (this.props.stores) {
var stores = this.props.stores

if (Array.isArray(stores)) {
stores.forEach(function (store) {
this.addSubscription(store)
}, this)
} else {
Object.keys(stores).forEach(function (formatter) {
this.addSubscription(stores[formatter])
}, this)
}
}
},

componentWillUnmount: function () {
Subscribe.destroy(this)
},

getStateFromStores: function () {
if (this.props.store) {
return getState(this.props.store, this.props)
} else if (this.props.stores) {
var stores = this.props.stores

// If you pass in an array of stores the state is merged together.
if (Array.isArray(stores)) {
return stores.reduce(function (obj, store) {
return assign(obj, getState(store, this.props))
}.bind(this), {})

// if it is an object then the state is added to the key specified
} else {
return Object.keys(stores).reduce(function (obj, key) {
obj[key] = getState(stores[key], this.props)
return obj
}.bind(this), {})
}
} else {
return {}
}
},

addSubscription(store) {
if (typeof store === 'object') {
Subscribe.add(this, store, this.altSetState)
}
},

altSetState: function () {
this.setState(this.getStateFromStores())
},

getProps: function () {
return assign(
this.context.flux || this.props.flux
? { flux: this.context.flux || this.props.flux }
: {},
this.state
)
},

render: function () {
var children = this.props.children

// Custom rendering function
if (typeof this.props.render === 'function') {
return this.props.render(this.getProps())
}

// Does not wrap child in a div if we don't have to.
if (Array.isArray(children)) {
return React.createElement('div', null, children.map(function (child, i) {
return cloneWithProps(child, assign({ key: i }, this.getProps()))
}, this))
} else if (children) {
return cloneWithProps(children, this.getProps())
} else {
return React.createElement('div', this.getProps())
}
}
})

module.exports = AltContainer
11 changes: 10 additions & 1 deletion dist/alt-browser-with-addons.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Alt = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
"use strict";

/**
* This mixin lets you setup your listeners. It is similar to Fluxible's mixin.
*
Expand Down Expand Up @@ -79,6 +80,7 @@ module.exports = FluxyMixin;

},{"./Subscribe":4}],2:[function(require,module,exports){
"use strict";

var Subscribe = require("./Subscribe");

var ListenerMixin = {
Expand Down Expand Up @@ -109,6 +111,7 @@ module.exports = ListenerMixin;

},{"./Subscribe":4}],3:[function(require,module,exports){
"use strict";

/**
* This mixin automatically sets the state for you based on the key you provide
*
Expand Down Expand Up @@ -190,6 +193,7 @@ module.exports = ReactStateMagicMixin;

},{"./Subscribe":4}],4:[function(require,module,exports){
"use strict";

var Symbol = require("es-symbol");
var MIXIN_REGISTRY = Symbol("alt store listeners");

Expand Down Expand Up @@ -1438,7 +1442,9 @@ var Alt = (function () {
}

return this.createActions(function () {
this.generateActions.apply(this, actionNames);
var _ref;

(_ref = this).generateActions.apply(_ref, actionNames);
});
}
},
Expand Down Expand Up @@ -1598,6 +1604,7 @@ module.exports = Alt;

},{"es-symbol":5,"eventemitter3":6,"flux":7,"object-assign":10}],13:[function(require,module,exports){
"use strict";

/**
* ActionListeners(alt: AltInstance): ActionListenersInstance
*
Expand Down Expand Up @@ -1660,6 +1667,7 @@ ActionListeners.prototype.removeAllActionListeners = function () {

},{"es-symbol":5}],14:[function(require,module,exports){
"use strict";

/**
* DispatcherRecorder(alt: AltInstance): DispatcherInstance
*
Expand Down Expand Up @@ -1795,6 +1803,7 @@ DispatcherRecorder.prototype.loadEvents = function (events) {

},{"es-symbol":5}],15:[function(require,module,exports){
"use strict";

/**
* makeFinalStore(alt: AltInstance): AltStore
*
Expand Down
4 changes: 3 additions & 1 deletion dist/alt-browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -1186,7 +1186,9 @@ var Alt = (function () {
}

return this.createActions(function () {
this.generateActions.apply(this, actionNames);
var _ref;

(_ref = this).generateActions.apply(_ref, actionNames);
});
}
},
Expand Down
122 changes: 122 additions & 0 deletions docs/components/altContainer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
---
layout: docs
title: AltContainer
description: A Store Controller component for better composition
permalink: /docs/components/altContainer
---

# AltContainer

AltContainer is not an idea exclusive to alt. For more information on why you should be using container components check out React.js Conf 2015 talk [Making your app fast with high-performance components](https://youtu.be/KYzlpRvWZ6c?t=22m48s).

The basic idea is that you have a container that wraps your component, the duty of this container component is to handle all the data fetching and communication with the stores, it then renders the corresponding children. The sub-components just render markup and are data agnostic thus making them highly reusable.

## `stores`

You can pass in an object to `stores` where the keys correspond to the prop that the children will receive and their value the store we should retrieve the state from.

For example:

```js
<AltContainer
stores={{
BlogPosts: BlogStore,
Comments: CommentsStore,
Shares: ShareStore
}}
>
<div />
</AltContainer>
```

Will pass the state from `FooStore` (`FooStore.getState()`) into the `<div />` as `this.props.FooStore`. `BarStore` will be available in `this.props.BarStore`, etc.

You can pass in a custom function as the value in order to control what each prop will represent. Say you have multiple getters on a store and only want to pass a subset of the state rather than the whole state.

```js
<AltContainer
stores={{
post: function (props) {
return BlogStore.getPostFor(props.blogId);
},
comments: function (props) {
return CommentsStore.getCommentsFor(props.blogId)
},
shares: function (props) {
return ShareStore.getSharesFor(props.blogId)
}
}}
>
<BlogPost />
</AltContainer>
```

## `store`

If you only have a single store you can use `store` to bind it. The state will then be passed as props straight through.

```js
var PostStore = alt.createStore({
displayName: 'PostStore',
state: {
id: 1,
title: 'Hello, World!'
}
});

<AltContainer store={PostStore}>
<BlogPost />
</AltContainer>
```

`BlogPost` in this case will receive all the state of `BlogStore` as props. `this.props.id` and `this.props.title` will be passed through to `BlogPost`.

Just like `stores`, you can define your own custom function to use with `store`.

```js

function blogStoreFetcher(props) {
return BlogStore.getPostFor(props.blogId);
}

<AltContainer store={blogStoreFetcher}>
<BlogPost />
</AltContainer>
```

Children will get the properties you define.

```js
<AltContainer store={BlogStore}>
<BlogPost className="my-awesome-post" />
</AltContainer>
```

## `render`

But you can also specify a special `render` function which will render whatever your heart desires.

```js
<AltContainer
store={BlogStore}
render={function (props) {
return <BlogPost className="my-awesome-post" id={props.id} title="Overriding the title" />
}}
/>
```

## `flux`

`AltContainer` also works with Alt instances. If you're passing the alt instance around as a `flux` prop, it'll bind it to all its children.

```js
const flux = new Flux();

<AltContainer flux={flux}>
<Header />
<Body />
<Footer />
</AltContainer>
```

Header, Body, and Footer will have the `flux` context passed down.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@
"object-assign": "^2.0.0"
},
"devDependencies": {
"babel": "^4.7.13",
"babel": "^4.7.16",
"babelify": "^5.0.4",
"browserify": "^9.0.3",
"chai": "^2.1.0",
"coveralls": "^2.11.2",
"eslint": "^0.17.1",
"iso": "^4.0.2",
"istanbul": "^0.3.5",
"jsdom": "^3.1.2",
"mocha": "^2.1.0",
"react": "^0.13.1",
"sinon": "^1.14.0"
Expand Down
4 changes: 2 additions & 2 deletions test/isomorphic-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ const reactRender = React.render
export default {
'Isomorphic Flux rendering util': {
afterEach() {
global.window = undefined
global.document = undefined
delete global.window
delete global.document
React.render = reactRender
},

Expand Down
Loading

0 comments on commit efbd652

Please sign in to comment.