PAYATHON 2026

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 的编辑场景,使用临时副本更清楚,也更稳定。

备注:内容仅供参考。