diff --git a/bower.json b/bower.json
index 86621a9..3168606 100644
--- a/bower.json
+++ b/bower.json
@@ -1,6 +1,6 @@
{
"name": "fluxxor",
- "version": "1.5.4",
+ "version": "1.6.0-alpha1",
"main": "build/fluxxor.min.js",
"description": "Flux architecture tools for React",
"homepage": "",
diff --git a/examples/react-router/app/app.jsx b/examples/react-router/app/app.jsx
index 05bda26..14122b2 100644
--- a/examples/react-router/app/app.jsx
+++ b/examples/react-router/app/app.jsx
@@ -3,14 +3,12 @@ var React = require("react"),
Fluxxor = require("../../../");
var actions = require("./actions.jsx"),
- routes = require("./routes.jsx"),
+ router = require("./router.jsx"),
RecipeStore = require("./stores/recipe_store.jsx");
RouteStore = require("./stores/route_store.jsx");
require("./style.less");
-var router = Router.create({routes: routes});
-
var stores = {
recipe: new RecipeStore(),
route: new RouteStore({router: router})
diff --git a/examples/react-router/app/components/empty_view.jsx b/examples/react-router/app/components/empty_view.jsx
index 4ea0e31..5dd663a 100644
--- a/examples/react-router/app/components/empty_view.jsx
+++ b/examples/react-router/app/components/empty_view.jsx
@@ -2,8 +2,10 @@ var React = require("react"),
Router = require("react-router"),
RouteHandler = Router.RouteHandler;
-module.exports = React.createClass({
- render: function() {
+class EmptyView extends React.Component {
+ render() {
return ;
}
-});
+}
+
+module.exports = EmptyView;
diff --git a/examples/react-router/app/components/recipe.jsx b/examples/react-router/app/components/recipe.jsx
index 07ec265..7b1483b 100644
--- a/examples/react-router/app/components/recipe.jsx
+++ b/examples/react-router/app/components/recipe.jsx
@@ -1,34 +1,17 @@
var React = require("react"),
Router = require("react-router"),
- Link = Router.Link,
- Fluxxor = require("../../../../");
+ Link = Router.Link;
var RecipeStore = require("../stores/recipe_store.jsx");
-var Recipe = React.createClass({
- mixins: [
- Fluxxor.FluxMixin(React),
- Fluxxor.StoreWatchMixin("recipe")
- ],
-
- contextTypes: {
- router: React.PropTypes.func
- },
-
- getStateFromFlux: function() {
- var params = this.context.router.getCurrentParams();
-
- return {
- recipe: this.getFlux().store("recipe").getRecipe(params.id)
- };
- },
-
- componentWillReceiveProps: function(nextProps) {
- this.setState(this.getStateFromFlux());
- },
+class Recipe extends React.Component {
+ constructor() {
+ super();
+ this.deleteRecipe = this.deleteRecipe.bind(this);
+ }
- render: function() {
- var recipe = this.state.recipe;
+ render() {
+ var recipe = this.props.recipe;
if (recipe === RecipeStore.NOT_FOUND_TOKEN) {
return this.renderNotFound();
@@ -52,23 +35,23 @@ var Recipe = React.createClass({
);
- },
+ }
- renderIngredient: function(ingredient, idx) {
+ renderIngredient(ingredient, idx) {
return (
{ingredient.quantity} {ingredient.item}
);
- },
+ }
- renderNotFound: function() {
+ renderNotFound() {
return this.renderWithLayout(
That recipe was not found.
);
- },
+ }
- renderWithLayout: function(content) {
+ renderWithLayout(content) {
return (
{content}
@@ -77,15 +60,24 @@ var Recipe = React.createClass({
{" | "}Add New Recipe
);
- },
+ }
- deleteRecipe: function(e) {
+ deleteRecipe(e) {
if (confirm("Really delete this recipe?")) {
- this.getFlux().actions.recipes.remove(this.state.recipe.id);
+ this.props.onDeleteRecipe(this.props.recipe.id);
} else {
e.preventDefault();
}
}
-});
+}
+
+Recipe.propTypes = {
+ recipe: React.PropTypes.object.isRequired,
+ onDeleteRecipe: React.PropTypes.func.isRequired
+};
+
+Recipe.contextTypes = {
+ router: React.PropTypes.func
+};
module.exports = Recipe;
diff --git a/examples/react-router/app/components/recipe_adder.jsx b/examples/react-router/app/components/recipe_adder.jsx
index d313cf8..47a7456 100644
--- a/examples/react-router/app/components/recipe_adder.jsx
+++ b/examples/react-router/app/components/recipe_adder.jsx
@@ -2,22 +2,18 @@ var t = require("tcomb-form"),
React = require("react"),
Router = require("react-router"),
RouteHandler = Router.RouteHandler,
- Link = Router.Link,
- Fluxxor = require("../../../../");
+ Link = Router.Link;
var Recipe = require("../schemas/recipe.jsx"),
RecipeForm = require("../forms/recipe_form.jsx");
-var RecipeAdder = React.createClass({
- mixins: [
- Fluxxor.FluxMixin(React)
- ],
-
- contextTypes: {
- router: React.PropTypes.func
- },
+class RecipeAdder extends React.Component {
+ constructor(props) {
+ super(props);
+ this.onSubmit = this.onSubmit.bind(this);
+ }
- render: function() {
+ render() {
return this.renderWithLayout(
);
- },
+ }
- renderWithLayout: function(content) {
+ renderWithLayout(content) {
return (
{content}
@@ -37,29 +33,30 @@ var RecipeAdder = React.createClass({
{" | "}Add New Recipe
);
- },
+ }
- onSubmit: function(e) {
+ onSubmit(e) {
e.preventDefault();
var newRecipe = this.refs.form.getValue();
if (newRecipe) {
- this.getFlux().actions.recipes.add(
+ this.props.onAddRecipe(
newRecipe.name,
newRecipe.description,
newRecipe.ingredients,
newRecipe.directions
);
}
- },
-
- deleteRecipe: function(e) {
- if (confirm("Really delete this recipe?")) {
- this.getFlux().actions.recipes.remove(this.state.recipe.id);
- } else {
- e.preventDefault();
- }
}
-});
+};
+
+RecipeAdder.propTypes = {
+ onAddRecipe: React.PropTypes.func.isRequired
+};
+
+RecipeAdder.contextTypes = {
+ router: React.PropTypes.func
+};
+
module.exports = RecipeAdder;
diff --git a/examples/react-router/app/components/recipe_editor.jsx b/examples/react-router/app/components/recipe_editor.jsx
index 0817d0e..d68a903 100644
--- a/examples/react-router/app/components/recipe_editor.jsx
+++ b/examples/react-router/app/components/recipe_editor.jsx
@@ -2,37 +2,21 @@ var t = require("tcomb-form"),
React = require("react"),
Router = require("react-router"),
RouteHandler = Router.RouteHandler,
- Link = Router.Link,
- Fluxxor = require("../../../../");
+ Link = Router.Link;
var Recipe = require("../schemas/recipe.jsx"),
RecipeForm = require("../forms/recipe_form.jsx"),
RecipeStore = require("../stores/recipe_store.jsx");
-var RecipeEditor = React.createClass({
- mixins: [
- Fluxxor.FluxMixin(React),
- Fluxxor.StoreWatchMixin("recipe")
- ],
-
- contextTypes: {
- router: React.PropTypes.func
- },
-
- getStateFromFlux: function() {
- var params = this.context.router.getCurrentParams();
-
- return {
- recipe: this.getFlux().store("recipe").getRecipe(params.id)
- };
- },
-
- componentWillReceiveProps: function(nextProps) {
- this.setState(this.getStateFromFlux());
- },
+class RecipeEditor extends React.Component {
+ constructor() {
+ super();
+ this.onSubmit = this.onSubmit.bind(this);
+ this.deleteRecipe = this.deleteRecipe.bind(this);
+ }
- render: function() {
- var recipe = this.state.recipe;
+ render() {
+ var recipe = this.props.recipe;
if (recipe === RecipeStore.NOT_FOUND_TOKEN) {
return this.renderNotFound();
@@ -50,15 +34,15 @@ var RecipeEditor = React.createClass({
);
- },
+ }
- renderNotFound: function() {
+ renderNotFound() {
return this.renderWithLayout(
That recipe was not found.
);
- },
+ }
- renderWithLayout: function(content) {
+ renderWithLayout(content) {
return (
{content}
@@ -67,32 +51,42 @@ var RecipeEditor = React.createClass({
{" | "}Add New Recipe
);
- },
+ }
- onSubmit: function(e) {
+ onSubmit(e) {
e.preventDefault();
var newRecipe = this.refs.form.getValue();
if (newRecipe) {
- this.getFlux().actions.recipes.edit(
- this.state.recipe.id,
+ this.props.onEditRecipe(
+ this.props.recipe.id,
newRecipe.name,
newRecipe.description,
newRecipe.ingredients,
newRecipe.directions
);
- this.context.router.transitionTo("recipe", {id: this.state.recipe.id});
+ this.context.router.transitionTo("recipe", {id: this.props.recipe.id});
}
- },
+ }
- deleteRecipe: function(e) {
+ deleteRecipe(e) {
if (confirm("Really delete this recipe?")) {
- this.getFlux().actions.recipes.remove(this.state.recipe.id);
+ this.props.onDeleteRecipe(this.props.recipe.id);
} else {
e.preventDefault();
}
}
-});
+};
+
+RecipeEditor.propTypes = {
+ recipe: React.PropTypes.object.isRequired,
+ onEditRecipe: React.PropTypes.func.isRequired,
+ onDeleteRecipe: React.PropTypes.func.isRequired
+};
+
+RecipeEditor.contextTypes = {
+ router: React.PropTypes.func
+};
module.exports = RecipeEditor;
diff --git a/examples/react-router/app/components/recipe_list.jsx b/examples/react-router/app/components/recipe_list.jsx
index 697070c..381398e 100644
--- a/examples/react-router/app/components/recipe_list.jsx
+++ b/examples/react-router/app/components/recipe_list.jsx
@@ -1,37 +1,32 @@
var React = require("react"),
Router = require("react-router"),
RouteHandler = Router.RouteHandler,
- Link = Router.Link,
- Fluxxor = require("../../../../");
+ Link = Router.Link;
-var RecipeList = React.createClass({
- mixins: [Fluxxor.FluxMixin(React), Fluxxor.StoreWatchMixin("recipe")],
-
- getStateFromFlux: function() {
- return {
- recipes: this.getFlux().store("recipe").getRecipes()
- };
- },
-
- render: function() {
+class RecipeList extends React.Component {
+ render() {
return (
Recipes
-
{this.state.recipes.map(this.renderRecipeLink)}
+
{this.props.recipes.map(this.renderRecipeLink)}
Add New Recipe
);
- },
+ }
- renderRecipeLink: function(recipe) {
+ renderRecipeLink(recipe) {
return (
{recipe.name}
);
}
-});
+}
+
+RecipeList.propTypes = {
+ recipes: React.PropTypes.arrayOf(React.PropTypes.object).isRequired
+};
module.exports = RecipeList;
diff --git a/examples/react-router/app/forms/recipe_form.jsx b/examples/react-router/app/forms/recipe_form.jsx
index c832843..ccdf89c 100644
--- a/examples/react-router/app/forms/recipe_form.jsx
+++ b/examples/react-router/app/forms/recipe_form.jsx
@@ -85,7 +85,7 @@ var list = function(locals) {
};
module.exports = {
- auto: 'none',
+ auto: 'placeholders',
templates: {
struct: struct,
list: list
diff --git a/examples/react-router/app/router.jsx b/examples/react-router/app/router.jsx
new file mode 100644
index 0000000..32dca9e
--- /dev/null
+++ b/examples/react-router/app/router.jsx
@@ -0,0 +1,124 @@
+var React = require("react"),
+ Router = require("react-router"),
+ Route = Router.Route,
+ DefaultRoute = Router.DefaultRoute;
+
+var EmptyView = require("./components/empty_view.jsx"),
+ Recipe = require("./components/recipe.jsx"),
+ RecipeEditor = require("./components/recipe_editor.jsx"),
+ RecipeAdder = require("./components/recipe_adder.jsx"),
+ RecipeList = require("./components/recipe_list.jsx");
+
+var Fluxxor = require("../../..");
+var FluxController = Fluxxor.FluxController(React);
+
+var router;
+
+var { wrap } = FluxController;
+
+// The basic format for `FluxController.wrap` is:
+//
+// wrap(component, storesToWatch, propsFunction, extraContext)
+//
+// Where the return value of `propsFunction` should be an object that
+// will get passed to the wrapped component as properties, and `extraContext`
+// is an object or a function that returns an object that will be passed as
+// the last argument to `propsFunction`.
+//
+// `propsFunction` has the signature:
+//
+// (flux, props, extraContext) => { return obj; }
+//
+// where `flux` is the `Fluxxor.Flux` instance, `props` is the props
+// originally passed to the component, and `extraContext` is as
+// described above.
+
+
+// This example is the most basic; `RecipeList` is wrapped by a
+// component that re-renders it anytime the "recipe" store is changed,
+// passing in `flux.store("recipe").getRecipes()` as its `recipes` prop.
+RecipeListWrapped = wrap(RecipeList, ["recipe"], (flux) => {
+ return {
+ recipes: flux.store("recipe").getRecipes()
+ };
+});
+
+// This example is similar, except no stores are watched. The purpose
+// of wrapping the component is to provide a flux action creator
+// as one of its properties.
+RecipeAdderWrapped = wrap(RecipeAdder, [], (flux) => {
+ return {
+ onAddRecipe: (name, desc, ingredients, directions) => {
+ flux.actions.recipes.add(name, desc, ingredients, directions);
+ }
+ };
+});
+
+// This example shows how you could provide extra context to
+// the props function (in this case, we want access to the router.
+RecipeWrapped = wrap(Recipe, ["recipe"], (flux, props, extraContext) => {
+ var params = extraContext.router.getCurrentParams();
+
+ return {
+ recipe: flux.store("recipe").getRecipe(params.id),
+ onDeleteRecipe: (recipeId) => {
+ flux.actions.recipes.remove(recipeId);
+ }
+ };
+}, () => ({router: router})); // we make extraContext a function and
+ // it will be called, and the result passed
+ // as the last arg to the props function
+
+// This example shows how to completely customize the rendering
+// of a child component by utilizing `FluxController` directly
+// in a custom `render` function (passed as a prop to
+// `FluxController`).
+class RecipeEditorWrapped extends React.Component {
+ getChildProps(flux, props, extraCtx) {
+ var params = extraCtx.router.getCurrentParams();
+ return {
+ recipe: flux.store("recipe").getRecipe(params.id),
+ onEditRecipe: (id, name, desc, ingredients, directions) => {
+ flux.actions.recipes.edit(id, name, desc, ingredients, directions);
+ },
+ onDeleteRecipe: (recipeId) => {
+ flux.actions.recipes.remove(recipeId);
+ }
+ };
+ }
+
+ render() {
+ return ;
+ }
+
+ renderChild(fProps) {
+ return ;
+ }
+}
+
+RecipeEditorWrapped.contextTypes = {
+ router: React.PropTypes.func
+};
+
+
+var routes = (
+
+
+
+
+
+
+
+
+
+
+);
+
+router = Router.create({routes: routes});
+
+module.exports = router;
diff --git a/examples/react-router/app/routes.jsx b/examples/react-router/app/routes.jsx
deleted file mode 100644
index f80aa31..0000000
--- a/examples/react-router/app/routes.jsx
+++ /dev/null
@@ -1,25 +0,0 @@
-var React = require("react"),
- Router = require("react-router"),
- Route = Router.Route,
- DefaultRoute = Router.DefaultRoute;
-
-var EmptyView = require("./components/empty_view.jsx"),
- Recipe = require("./components/recipe.jsx"),
- RecipeEditor = require("./components/recipe_editor.jsx"),
- RecipeAdder = require("./components/recipe_adder.jsx"),
- RecipeList = require("./components/recipe_list.jsx");
-
-var routes = (
-
-
-
-
-
-
-
-
-
-
-);
-
-module.exports = routes;
diff --git a/examples/react-router/webpack.config.js b/examples/react-router/webpack.config.js
index 10735cd..cf4808c 100644
--- a/examples/react-router/webpack.config.js
+++ b/examples/react-router/webpack.config.js
@@ -11,7 +11,7 @@ module.exports = {
module: {
loaders: [
{ test: /\.less$/, loader: "style!css!less" },
- { test: /\.jsx$/, loader: "jsx-loader" },
+ { test: /\.jsx$/, loader: "jsx-loader?harmony" },
{ test: /\.json$/, loader: "json" }
]
}
diff --git a/index.js b/index.js
index 1f08008..15cb609 100644
--- a/index.js
+++ b/index.js
@@ -12,7 +12,9 @@ var Fluxxor = {
FluxChildMixin: FluxChildMixin,
StoreWatchMixin: StoreWatchMixin,
createStore: createStore,
- version: require("./version")
+ version: require("./version"),
+
+ FluxController: require("./lib/flux_controller")
};
module.exports = Fluxxor;
diff --git a/lib/flux_controller.js b/lib/flux_controller.js
new file mode 100644
index 0000000..13f8c56
--- /dev/null
+++ b/lib/flux_controller.js
@@ -0,0 +1,124 @@
+var _each = require("lodash/collection/forEach"),
+ _extend = require("lodash/object/extend"),
+ _isFunction = require("lodash/lang/isFunction");
+
+var FluxMixin = require("./flux_mixin");
+
+module.exports = function(React) {
+ var FluxController = React.createClass({
+ mixins: [FluxMixin(React)],
+
+ propTypes: {
+ fluxxorStores: React.PropTypes.arrayOf(
+ React.PropTypes.string
+ ),
+ fluxxorProps: React.PropTypes.func,
+ fluxxorExtraContext: React.PropTypes.oneOfType([
+ React.PropTypes.object,
+ React.PropTypes.func
+ ])
+ },
+
+ getDefaultProps: function() {
+ return {
+ fluxxorStores: [],
+ fluxxorProps: function() {},
+ fluxxorExtraContext: {}
+ };
+ },
+
+ getStateFromFlux: function(props) {
+ props = props || this.props;
+ var flux = this.getFlux();
+ var extraContext;
+ if (_isFunction(this.props.fluxxorExtraContext)) {
+ extraContext = this.props.fluxxorExtraContext(flux, this);
+ } else {
+ extraContext = this.props.fluxxorExtraContext;
+ }
+ var newState = { fluxProps: this.props.fluxxorProps(flux, props, extraContext) };
+ return newState;
+ },
+
+ componentDidMount: function() {
+ var flux = this.getFlux();
+ _each(this.props.fluxxorStores, function(store) {
+ flux.store(store).on("change", this._setStateFromFlux);
+ }, this);
+ },
+
+ componentWillUnmount: function() {
+ var flux = this.getFlux();
+ _each(this.props.fluxxorStores, function(store) {
+ flux.store(store).removeListener("change", this._setStateFromFlux);
+ }, this);
+ },
+
+ componentWillReceiveProps: function(nextProps) {
+ this.setState(this.getStateFromFlux(nextProps));
+ },
+
+ _setStateFromFlux: function() {
+ if(this.isMounted()) {
+ this.setState(this.getStateFromFlux());
+ }
+ },
+
+ getInitialState: function() {
+ return this.getStateFromFlux();
+ },
+
+ render: function() {
+ var fluxProps = {};
+ if (this.state.fluxProps) {
+ Object.keys(this.state.fluxProps).forEach(function(key) {
+ fluxProps[key] = this.state.fluxProps[key];
+ }.bind(this));
+ }
+ if (this.props) {
+ Object.keys(this.props).forEach(function(key) {
+ fluxProps[key] = this.props[key];
+ }.bind(this));
+ }
+
+ delete fluxProps.fluxxorStores;
+ delete fluxProps.fluxxorProps;
+ delete fluxProps.fluxxorExtraContext;
+
+ if (this.props.render) {
+ return this.props.render(fluxProps);
+ }
+
+ return React.createElement("div", {}, React.Children.map(this.props.children, function(child) {
+ return React.cloneElement(child, fluxProps);
+ }, this));
+ }
+ });
+
+ FluxController.wrap = function(comp, stores, fluxProps, extraContext) {
+ return React.createClass({
+ render: function() {
+ var props = _extend({}, this.props, {
+ fluxxorStores: stores,
+ fluxxorProps: fluxProps,
+ fluxxorExtraContext: extraContext,
+ render: function(fProps) {
+ return React.createElement(comp, fProps);
+ }
+ });
+ return React.createElement(FluxController, props);
+ }
+ });
+ };
+
+ FluxController.wrapStatic = function(comp, extraContext) {
+ return FluxController.wrap(
+ comp,
+ comp.fluxxorStores,
+ comp.fluxxorProps,
+ extraContext
+ );
+ };
+
+ return FluxController;
+};
diff --git a/package.json b/package.json
index 1966859..d812ee4 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "fluxxor",
- "version": "1.5.4",
+ "version": "1.6.0-alpha1",
"description": "Flux architecture tools for React",
"repository": {
"type": "git",
diff --git a/version.js b/version.js
index 1bc458e..c6516da 100644
--- a/version.js
+++ b/version.js
@@ -1 +1 @@
-module.exports = "1.5.4"
\ No newline at end of file
+module.exports = "1.6.0-alpha1"