使用Flux构建一个编辑表单,它实际向服务器发送数据:动作、存储、视图
Using Flux to build an edit form, who actually POSTs data to the server: actions, stores, views?
我找到了很多关于如何为React和Flux获取数据的资源、博客和意见,但很少有关于向服务器写入数据的。有人能提供一个"首选"方法的基本原理和一些示例代码吗?在构建一个简单的编辑表单的背景下,将更改持久化到RESTful web API?
具体来说,哪个Flux盒子应该调用$.post
,在哪里调用ActionCreator.receiveItem()
(它做什么),以及在商店的注册方法中有什么?
相关链接:
- 当使用React + Flux时,动作或存储应该负责转换数据吗?
- 通量存储或动作(或两者)应该接触外部服务吗? 在Flux应用中ajax请求应该在哪里?
简短回答
- 你的表单组件应该从Store中获取它的状态,在用户输入时创建"update"动作,在表单提交时调用"save"动作。
- 操作创建者将执行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项时,底层的Bar
记录将在本地保持最新状态,当提交表单时,我们尝试将其保存在服务器上。就这样吧:)
- 组件/视图用于显示数据和事件
- action与事件(onClick, onChange…)绑定,用于在承诺解决或失败后与资源通信和调度事件。确保至少有两个事件,一个成功,一个ajax失败。
- store被订阅到调度程序正在调度的事件。一旦接收到数据,存储就会更新存储的值并发出变化。
- 组件/视图被订阅到store,一旦发生更改就重新渲染。
flux store,或者action(或者两者)应该接触外部服务吗?方法对我来说是很自然的。
也有一些情况下,当你需要触发一些操作作为其他操作被触发的结果,这是你可以从一个相关的存储触发操作,结果存储和视图被更新。
相关文章:
- Angularjs UI视图和Rails服务器端模板路由
- 重用服务器端的主干应用程序以在服务器端呈现视图
- Angular:将对象存储在服务中以在视图中显示和将crud函数存储到服务器的最佳方式是什么
- 根据服务器的初始标记填充knockoutJS视图模型对象
- 如果外部应用程序更改了持久模型(服务器数据库),AngularJS 是否可以自动更新视图
- 尝试使用 javascript 从 Tableau 服务器渲染视图时出错
- 如何使用环回保护服务器视图和客户端视图
- 更改路由并更新对节点服务器的 ajax 请求的视图
- 如何在没有持续服务器请求的情况下在 AngularJS 中进行视图重定向
- Backbone.js和服务器端视图
- Dojo DataGrid如何通知视图服务器上的数据已更改
- 一次将网格视图数据从客户端发送到服务器端
- 我的Angular视图没有用数据表单服务器更新
- 在服务器上呈现部分视图或在客户端上发送json数据并呈现模板
- 我如何传递node.js服务器变量到我的angular/html视图
- 如何仅在服务器上的集合发生更改时更新主干视图
- AngularJS来自服务器的绑定响应在视图中没有更新
- Angular区分服务器模型和客户端视图模型的方式
- 如何在Razor视图中分配JavaScript变量值给服务器端变量
- 我如何在剃刀视图中编写服务器端代码之间的JavaScript