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(
    @@ -26,9 +22,9 @@ var RecipeAdder = React.createClass({
    ); - }, + } - 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

    - +
    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"