AngularJS dropdown 未保存却修改 $scope 值
结论
不要把 dropdown 的 ng-model 直接绑定到列表对象的 invoice.paymentStatus。选项一旦改变,AngularJS 的双向绑定就会立即修改对象,不会等到用户单击 Save。
编辑时应创建一份临时副本:
- dropdown 绑定临时对象。
- 单击 Save 后提交临时对象,接口成功时再更新原对象。
- 单击 Cancel 时直接丢弃临时对象,原值不会受到影响。
原因
如果页面直接这样绑定:
<select ng-model="invoice.paymentStatus">
用户选择新状态后,AngularJS 会立即执行类似的赋值:
invoice.paymentStatus = selectedValue;
当 invoice 是 $scope.clientPaymentDetails 数组中的对象时,数组里的原对象也会随之改变。Save 按钮只负责调用接口,无法控制 ng-model 写入 $scope 的时机。
下面的赋值同样不会创建独立副本:
$scope.editInvoice = invoice;
此时两个变量指向同一个对象。修改 $scope.editInvoice.paymentStatus,也会修改 invoice.paymentStatus。
使用临时编辑副本
进入编辑状态时,通过 angular.copy() 创建副本:
$scope.startEdit = function (invoice) {
invoice.editing = true;
invoice.editModel = angular.copy(invoice);
};
dropdown 和输入框都绑定到 invoice.editModel:
<tr ng-repeat="invoice in clientPayment.invoices">
<td>{{ invoice.invoiceId }}</td>
<td>{{ invoice.invoiceNo }}</td>
<td ng-if="!invoice.editing">
{{ invoice.paymentStatus }}
</td>
<td ng-if="invoice.editing">
<select
ng-model="invoice.editModel.paymentStatus"
ng-options="status.type as status.type for status in paymentStatusTypes">
<option value="">Select payment status</option>
</select>
</td>
<td ng-if="!invoice.editing">
{{ invoice.amountReceived }}
</td>
<td ng-if="invoice.editing">
<input
type="number"
ng-model="invoice.editModel.amountReceived">
</td>
<td>
<button
type="button"
ng-if="!invoice.editing"
ng-click="startEdit(invoice)">
Edit
</button>
<button
type="button"
ng-if="invoice.editing"
ng-click="saveInvoice(invoice)">
Save
</button>
<button
type="button"
ng-if="invoice.editing"
ng-click="cancelEdit(invoice)">
Cancel
</button>
</td>
</tr>
状态列表可以这样定义:
$scope.paymentStatusTypes = [
{ type: 'ALL' },
{ type: 'PAID' },
{ type: 'UNPAID' },
{ type: 'PARTIALLY_PAID' }
];
如果 ALL 只用于筛选,不能作为 invoice 的实际付款状态,就要分别定义筛选状态和可编辑状态:
$scope.filterStatusTypes = [
'ALL',
'PAID',
'UNPAID',
'PARTIALLY_PAID'
];
$scope.invoiceStatusTypes = [
'PAID',
'UNPAID',
'PARTIALLY_PAID'
];
invoice 编辑 dropdown 对应写成:
<select
ng-model="invoice.editModel.paymentStatus"
ng-options="status for status in invoiceStatusTypes">
</select>
Save 的实现
将临时对象发送给服务,等服务端更新成功后再覆盖原对象:
$scope.saveInvoice = function (invoice) {
var editedInvoice = invoice.editModel;
if (
editedInvoice.amountReceived === null ||
editedInvoice.amountReceived === undefined ||
editedInvoice.amountReceived === '' ||
editedInvoice.paymentStatus === null ||
editedInvoice.paymentStatus === undefined ||
editedInvoice.paymentStatus === ''
) {
noty({
type: 'error',
text: 'Please enter Amount Received and Payment Status.'
});
return;
}
rpmDashboardService.updateInvoiceDetails(editedInvoice)
.then(function (response) {
if (response.status !== 200) {
noty({
type: 'error',
text: 'Some error occurred.'
});
return;
}
if (!response.data) {
noty({
type: 'error',
text: 'Invoice could not be updated.'
});
return;
}
// 如果接口返回完整的最新 invoice,优先使用 response.data。
var savedInvoice = angular.isObject(response.data)
? response.data
: editedInvoice;
// 避免把界面专用字段复制到业务对象。
delete savedInvoice.editing;
delete savedInvoice.editModel;
angular.extend(invoice, angular.copy(savedInvoice));
invoice.editing = false;
delete invoice.editModel;
noty({
type: 'success',
text: 'Invoice details updated successfully.'
});
})
.catch(function () {
noty({
type: 'error',
text: 'Some error occurred.'
});
});
};
如果接口的 response.data 只返回 true,直接使用临时对象即可:
angular.extend(invoice, angular.copy(editedInvoice));
具体使用哪个值,要看接口实际返回的数据结构。不能只因为 response.data 为真,就把它当作完整的 invoice。
Cancel 的实现
Cancel 无需将旧值重新写入原对象,丢弃临时副本即可:
$scope.cancelEdit = function (invoice) {
invoice.editing = false;
delete invoice.editModel;
};
用户修改的始终是 invoice.editModel,原来的 invoice.paymentStatus 没有发生变化,因此取消编辑后不会保留新状态。
筛选 dropdown 也应使用独立变量
如果页面顶部的 dropdown 只用于筛选 invoices,就不应绑定任何 invoice 的 paymentStatus,而要使用单独的筛选模型:
<select
ng-model="filters.paymentStatus"
ng-options="status for status in filterStatusTypes">
</select>
<tr ng-repeat="invoice in invoices | filter:paymentStatusFilter">
<!-- invoice content -->
</tr>
$scope.filters = {
paymentStatus: 'ALL'
};
$scope.paymentStatusFilter = function (invoice) {
var status = $scope.filters.paymentStatus;
return status === 'ALL' ||
invoice.paymentStatus === status;
};
如果筛选条件也需要 Save 和 Cancel,可以分别保存已应用值和临时值:
$scope.appliedFilters = {
paymentStatus: 'ALL'
};
$scope.pendingFilters = angular.copy($scope.appliedFilters);
$scope.saveFilters = function () {
$scope.appliedFilters = angular.copy($scope.pendingFilters);
};
$scope.cancelFilters = function () {
$scope.pendingFilters = angular.copy($scope.appliedFilters);
};
dropdown 绑定待确认的值:
<select
ng-model="pendingFilters.paymentStatus"
ng-options="status for status in filterStatusTypes">
</select>
<button type="button" ng-click="saveFilters()">Save</button>
<button type="button" ng-click="cancelFilters()">Cancel</button>
筛选函数读取已经应用的值:
$scope.paymentStatusFilter = function (invoice) {
var status = $scope.appliedFilters.paymentStatus;
return status === 'ALL' ||
invoice.paymentStatus === status;
};
注意事项
angular.copy() 很重要,因为 invoice 中可能有嵌套对象。如果只做浅复制:
$scope.editInvoice = angular.extend({}, invoice);
虽然第一层属性已经隔离,但嵌套对象仍可能共享引用。当对象中包含项目、客户或付款明细等嵌套数据时,应优先使用:
$scope.editInvoice = angular.copy(invoice);
也不要依赖表单的 $rollbackViewValue() 完成业务层面的取消操作。它主要用于处理输入控件尚未提交的视图值。常规双向绑定更新模型后,它无法可靠地还原业务对象。对于需要 Save 和 Cancel 的编辑场景,使用临时副本更清楚,也更稳定。
备注:内容仅供参考。
