使用Flux构建一个编辑表单,谁实际将数据POST到服务器:操作,存储,视图?

时间:2015-07-22 21:08:04

标签: javascript reactjs reactjs-flux flux

我已经找到了很多关于如何获取React和Flux数据的资源,博客和意见,但更不用说将数据写入服务器了。有人可以在构建一个持久更改RESTful Web API的简单编辑表单的上下文中为“首选”方法提供基本原理和一些示例代码吗?

具体来说,哪个Flux框应该调用$.post,调用ActionCreator.receiveItem()在哪里(以及它做了什么),以及商店注册方法中的内容是什么?

相关链接:

2 个答案:

答案 0 :(得分:4)

简短回答

  • 您的表单组件应从Store检索其状态,对用户输入创建“更新”操作,并在表单提交上调用“保存”操作。
  • 操作创建者将执行POST请求,并根据请求结果触发“save_success”操作或“save_error”操作。

通过实施示例的长答案

apiUtils / BarAPI.js

var Request = require('./Request'); //it's a custom module that handles request via superagent wrapped in Promise
var BarActionCreators = require('../actions/BarActionCreators');

var _endpoint = 'http://localhost:8888/api/bars/';

module.exports = {

    post: function(barData) {
        BarActionCreators.savePending();
        Request.post(_endpoint, barData).then (function(res) {
            if (res.badRequest) { //i.e response returns code 400 due to validation errors for example
                BarActionCreators.saveInvalidated(res.body);
            }
            BarActionCreators.savedSuccess(res.body);
        }).catch( function(err) { //server errors
            BarActionCreators.savedError(err);
        });
    },

    //other helpers out of topic for this answer

};

动作/ BarActionCreators.js

var AppDispatcher = require('../dispatcher/AppDispatcher');
var ActionTypes = require('../constants/BarConstants').ActionTypes;
var BarAPI = require('../apiUtils/VoucherAPI');

module.exports = {

    save: function(bar) {
        BarAPI.save(bar.toJSON());
    },

    saveSucceed: function(response) {
        AppDispatcher.dispatch({
            type: ActionTypes.BAR_SAVE_SUCCEED,
            response: response
        });
    },

    saveInvalidated: function(barData) {
        AppDispatcher.dispatch({
            type: ActionTypes.BAR_SAVE_INVALIDATED,
            response: response
        })
    },

    saveFailed: function(err) {
        AppDispatcher.dispatch({
            type: ActionTypes.BAR_SAVE_FAILED,
            err: err
        });
    },

    savePending: function(bar) {
        AppDispatcher.dispatch({
            type: ActionTypes.BAR_SAVE_PENDING,
            bar: bar
        });
    }

    rehydrate: function(barId, field, value) {
        AppDispatcher.dispatch({
            type: ActionTypes.BAR_REHYDRATED,
            barId: barId,
            field: field,
            value: value
        });
    },

};

存储/ BarStore.js

var assign = require('object-assign');
var EventEmitter = require('events').EventEmitter;
var Immutable = require('immutable');

var AppDispatcher = require('../dispatcher/AppDispatcher');
var ActionTypes = require('../constants/BarConstants').ActionTypes;
var BarAPI = require('../apiUtils/BarAPI')
var CHANGE_EVENT = 'change';

var _bars = Immutable.OrderedMap();

class Bar extends Immutable.Record({
    'id': undefined,
    'name': undefined,
    'description': undefined,
    'save_status': "not saved" //better to use constants here
}) {

    isReady() {
        return this.id != undefined //usefull to know if we can display a spinner when the Bar is loading or the Bar's data if it is ready.
    }

    getBar() {
        return BarStore.get(this.bar_id);
    }
}

function _rehydrate(barId, field, value) {
    //Since _bars is an Immutable, we need to return the new Immutable map. Immutable.js is smart, if we update with the save values, the same reference is returned.
    _bars = _bars.updateIn([barId, field], function() {
        return value;
    });
}


var BarStore = assign({}, EventEmitter.prototype, {

    get: function(id) {
        if (!_bars.has(id)) {
            BarAPI.get(id); //not defined is this example
            return new Bar(); //we return an empty Bar record for consistency
        }
        return _bars.get(id)
    },

    getAll: function() {
        return _bars.toList() //we want to get rid of keys and just keep the values
    },

    Bar: Bar,

    emitChange: function() {
        this.emit(CHANGE_EVENT);
    },

    addChangeListener: function(callback) {
        this.on(CHANGE_EVENT, callback);
    },

    removeChangeListener: function(callback) {
        this.removeListener(CHANGE_EVENT, callback);
    },

});

var _setBar = function(barData) {
    _bars = _bars.set(barData.id, new Bar(barData));
};

BarStore.dispatchToken = AppDispatcher.register(function(action) {
    switch (action.type)
    {   

        case ActionTypes.BAR_REHYDRATED:
            _rehydrate(
                action.barId,
                action.field,
                action.value
            );
            BarStore.emitChange();
            break;

        case ActionTypes.BAR_SAVE_PENDING:
            _bars = _bars.updateIn([action.bar.id, "save_status"], function() {
                return "saving";
            });
            BarStore.emitChange();
            break;

        case ActionTypes.BAR_SAVE_SUCCEED:
            _bars = _bars.updateIn([action.bar.id, "save_status"], function() {
                return "saved";
            });
            BarStore.emitChange();
            break;

        case ActionTypes.BAR_SAVE_INVALIDATED:
            _bars = _bars.updateIn([action.bar.id, "save_status"], function() {
                return "invalid";
            });
            BarStore.emitChange();
            break;

        case ActionTypes.BAR_SAVE_FAILED:
            _bars = _bars.updateIn([action.bar.id, "save_status"], function() {
                return "failed";
            });
            BarStore.emitChange();
            break;

        //many other actions outside the scope of this answer

        default:
            break;
    }
});

module.exports = BarStore;

组件/ BarList.react.js

var React = require('react/addons');
var Immutable = require('immutable');

var BarListItem = require('./BarListItem.react');
var BarStore = require('../stores/BarStore');

function getStateFromStore() {
    return {
        barList: BarStore.getAll(),
    };
}

module.exports = React.createClass({

    getInitialState: function() {
        return getStateFromStore();
    },

    componentDidMount: function() {
        BarStore.addChangeListener(this._onChange);
    },

    componentWillUnmount: function() {
        BarStore.removeChangeListener(this._onChange);
    },

    render: function() {
        var barItems = this.state.barList.toJS().map(function (bar) {
            // We could pass the entire Bar object here
            // but I tend to keep the component not tightly coupled
            // with store data, the BarItem can be seen as a standalone
            // component that only need specific data
            return <BarItem
                        key={bar.get('id')}
                        id={bar.get('id')}
                        name={bar.get('name')}
                        description={bar.get('description')}/>
        });

        if (barItems.length == 0) {
            return (
                <p>Loading...</p>
            )
        }

        return (
            <div>
                {barItems}
            </div>
        )

    },

    _onChange: function() {
        this.setState(getStateFromStore();
    }

});

组件/ BarListItem.react.js

var React = require('react/addons');
var ImmutableRenderMixin = require('react-immutable-render-mixin')
var Immutable = require('immutable');

module.exports = React.createClass({

    mixins: [ImmutableRenderMixin],

    // I use propTypes to explicitly telling
    // what data this component need. This 
    // component is a standalone component
    // and we could have passed an entire
    // object such as {id: ..., name, ..., description, ...}
    // since we use all the datas (and when we use all the data it's
    // a better approach since we don't want to write dozens of propTypes)
    // but let's do that for the example's sake 
    propTypes: {
        id: React.PropTypes.number.isRequired,
        name: React.PropTypes.string.isRequired,
        description: React.PropTypes.string.isRequired
    }

    render: function() {

        return (
            <li> //we should wrapped the following p's in a Link to the editing page of the Bar record with id = this.props.id. Let's assume that's what we did and when we click on this <li> we are redirected to edit page which renders a BarDetail component
                <p>{this.props.id}</p>
                <p>{this.props.name}</p>
                <p>{this.props.description}</p>
            </li>
        )

    }

});

组件/ BarDetail.react.js

var React = require('react/addons');
var ImmutableRenderMixin = require('react-immutable-render-mixin')
var Immutable = require('immutable');

var BarActionCreators = require('../actions/BarActionCreators');

module.exports = React.createClass({

    mixins: [ImmutableRenderMixin],

    propTypes: {
        id: React.PropTypes.number.isRequired,
        name: React.PropTypes.string.isRequired,
        description: React.PropTypes.string.isRequired
    },

    handleSubmit: function(event) {
        //Since we keep the Bar data up to date with user input
        //we can simply save the actual object in Store.
        //If the user goes back without saving, we could display a 
        //"Warning : item not saved" 
        BarActionCreators.save(this.props.id);
    },

    handleChange: function(event) {
        BarActionCreators.rehydrate(
            this.props.id,
            event.target.name, //the field we want to rehydrate
            event.target.value //the updated value
        );
    },

    render: function() {

        return (
            <form onSubmit={this.handleSumit}>
                <input
                    type="text"
                    name="name"
                    value={this.props.name}
                    onChange={this.handleChange}/>
                <textarea
                    name="description"
                    value={this.props.description}
                    onChange={this.handleChange}/>
                <input
                    type="submit"
                    defaultValue="Submit"/>
            </form>
        )

    },

});

使用此基本示例,只要用户通过BarDetail组件中的表单编辑了条形项,基础Bar记录将在本地保持最新,并且在提交表单时我们尝试将其保存在服务器上。就是这样:))

答案 1 :(得分:1)

  1. 组件/视图用于显示数据和火灾事件
  2. 操作与事件绑定(onClick,onChange ...),用于在承诺解决或失败后与资源通信并调度事件。确保您至少有两个事件,一个用于成功,另一个用于ajax失败。
  3. 商店订阅事件调度程序正在调度。一旦收到数据,商店就会更新存储的值并发出变化。
  4. 组件/视图订阅商店,并在更改发生后重新呈现。
  5. Should flux stores, or actions (or both) touch external services?方法对我来说似乎很自然。

    此外,在某些情况下,由于某些其他操作被触发,您需要触发某些操作,您可以在此处触发相关商店的操作,从而导致存储和视图更新。