PAYATHON 2026

Kendo UI AngularJs grid directive 为 undefined 如何处理

支付老李

结论

不要用固定时长的 $timeout 来判断 Kendo Grid 是否初始化完成。500ms 只是暂时掩盖了 AngularJS 子作用域、Kendo directive 初始化和 AJAX 返回之间的竞态条件。

更稳妥的方式是分别记录“ticket 数据已就绪”和“两个 Grid 已就绪”,等这些条件全部满足后再绑定 DataSource。在旧版 Kendo UI AngularJS 集成中,可以:

  • 用 $watch 监听 kendo-grid 暴露的 widget 变量;
  • 如果当前 Kendo UI 版本支持,也可以监听 kendoWidgetCreated;
  • 保留 autoBind: false,避免 Grid 初始化时过早读取数据。

undefined 是怎么出现的

对于下面的绑定:

<div
  kendo-grid="ticketLineGrid"
  k-options="ticketLineGridOptions">
</div>

AngularJS controller 创建时,ticketLineGrid 并不会立即存在。它要经过以下过程才会得到赋值:

  1. AngularJS 编译并链接 directive;
  2. Kendo directive 创建 Grid widget;
  3. directive 将 widget 实例赋给当前作用域中的 ticketLineGrid。

与此同时,ticketService.getTicket() 返回的是 Promise。网络响应、directive 初始化以及 controller 注册事件监听器的顺序并不固定。

父 controller 广播 PosReady 时,可能遇到以下情况:

  • 子 controller 已经收到事件,但 ticketLineGrid 还没有赋值;
  • 子 controller 或事件监听器尚未创建,导致广播直接丢失。

angular.element(document).ready() 只能说明 DOM 已经可以访问,不能说明 AngularJS directive 和 Kendo widget 已经初始化完成,因此无法解决这个问题。

用就绪状态统一协调

可以让 Grid 所在的子作用域监听 widget 实例。实例初始化完成后,再更新父级的共享状态。

先在父 controller 中建立一个对象。不要直接依赖原始类型的作用域继承,否则子作用域可能创建同名属性,遮蔽父作用域中的属性:

$scope.ticketControl = {
    lineGridReady: false,
    paymentGridReady: false,
    ticketReady: false,
    initialized: false
};

在明细 Grid 所在的 child scope 中监听 ticketLineGrid:

var unwatchLineGrid = $scope.$watch('ticketLineGrid', function (grid) {
    if (angular.isDefined(grid)) {
        $scope.ticketControl.lineGridReady = true;
        unwatchLineGrid();
    }
});

在付款 Grid 所在的 child scope 中监听 ticketPaymentGrid:

var unwatchPaymentGrid = $scope.$watch('ticketPaymentGrid', function (grid) {
    if (angular.isDefined(grid)) {
        $scope.ticketControl.paymentGridReady = true;
        unwatchPaymentGrid();
    }
});

父 controller 负责获取 ticket,并在状态变化时检查是否可以开始初始化:

function tryInitialize() {
    var control = $scope.ticketControl;

    if (control.initialized ||
        !control.ticketReady ||
        !control.lineGridReady ||
        !control.paymentGridReady) {
        return;
    }

    control.initialized = true;
    $scope.initPos($scope.ticket);
}

$scope.$watchGroup([
    'ticketControl.ticketReady',
    'ticketControl.lineGridReady',
    'ticketControl.paymentGridReady'
], tryInitialize);

ticketService.getTicket(ticketId).then(
    function (ticket) {
        $scope.ticket = ticket;
        $scope.ticketControl.ticketReady = true;
    },
    function (error) {
        // 按页面的错误处理策略显示提示或记录错误
    }
);

如果项目使用的 AngularJS 版本没有 $watchGroup,可以分别注册三个 $watch,统一调用 tryInitialize():

$scope.$watch('ticketControl.ticketReady', tryInitialize);
$scope.$watch('ticketControl.lineGridReady', tryInitialize);
$scope.$watch('ticketControl.paymentGridReady', tryInitialize);

ticket 请求和 Grid 初始化可以并行进行,无需为了等待 Grid 而延迟网络请求。不管哪一方先完成,tryInitialize() 都会等三个条件全部满足后执行初始化,而且只执行一次。

初始化 Grid 数据

Grid 配置中应保留 autoBind: false,防止 widget 创建后立即读取尚未准备好的数据:

$scope.ticketLineGridOptions = {
    autoBind: false,
    pageable: false,
    columns: [
        {
            field: "itemCode",
            title: "Item"
        },
        {
            field: "amount",
            title: "Amount",
            format: "{0:n2}",
            attributes: {
                style: "text-align:right"
            }
        }
    ]
};

等所有条件满足后,再设置数据源:

$scope.initPos = function (ticket) {
    var lineDataSource = getLineGridDataSource(ticket);
    var paymentDataSource = getPaymentGridDataSource(ticket);

    $scope.ticketLineGrid.setDataSource(lineDataSource);
    $scope.ticketPaymentGrid.setDataSource(paymentDataSource);

    $scope.ticketLineGrid.dataSource.read();
    $scope.ticketPaymentGrid.dataSource.read();
};

如果 DataSource 已经包含本地数据,通常不需要额外调用 read():

$scope.ticketLineGrid.setDataSource(
    new kendo.data.DataSource({
        data: ticket.lines
    })
);

是否需要调用 read(),要看 DataSource 使用的是本地 data,还是通过 transport.read 请求远程接口。

监听 kendoWidgetCreated

部分 Kendo UI AngularJS 版本会在 widget 创建完成后触发 kendoWidgetCreated。如果项目当前使用的版本确实提供这个事件,可以直接监听:

$scope.$on('kendoWidgetCreated', function (event, widget) {
    if (widget === $scope.ticketLineGrid) {
        $scope.ticketControl.lineGridReady = true;
    }
});

也可以通过 widget 对应的元素来判断:

$scope.$on('kendoWidgetCreated', function (event, widget) {
    if (widget.element.attr('id') === 'ticketLineGrid') {
        $scope.ticketControl.lineGridReady = true;
    }
});

旧版 Kendo UI AngularJS 集成在不同版本间存在差异。只有确认当前版本会触发该事件时,才适合使用这种方式。否则,用 $watch 监听 kendo-grid 的绑定变量更稳妥。

不要用一次性广播保存状态

$broadcast 适合通知“刚刚发生了某个动作”,却不适合表示“ticket 已经准备好”这类状态。事件不会保留。如果监听器注册得稍晚,就无法收到此前的广播。

这类场景更适合使用共享状态:

$scope.ticketControl.ticketReady = true;
$scope.ticketControl.lineGridReady = true;
$scope.ticketControl.paymentGridReady = true;

即使子 controller 稍后才初始化,也能读取这些状态。如果仍要保留 PosReady,至少应在广播前确认两个目标 Grid 都已经创建:

if ($scope.ticketLineGrid && $scope.ticketPaymentGrid) {
    $scope.$broadcast('PosReady', $scope.ticket);
}

不过,使用统一的状态协调函数更容易避免事件丢失和重复初始化。

注意事项

  • initialized 标志不能省略。多个 $watch 可能在同一个 digest 或连续的 digest 中触发,需要用它防止重复设置数据源。
  • 如果 Grid 会被销毁后重新创建,例如使用 ng-if 切换页面区域,需要重置对应的 ready 和 initialized 状态。
  • 如果 Grid 和父 controller 使用隔离作用域,应通过 directive binding、共享 service 或显式回调传递状态,不能假设 $scope.ticketControl 会自动继承。
  • $timeout(fn, 0) 有时可以把操作推迟到下一个 digest,但这不代表 Grid 已经初始化完成,不能把它当作 widget 的就绪信号。
  • Kendo UI 的 AngularJS 集成方式不同于当前的 Kendo UI for Angular。本文讨论的是旧版 AngularJS directive,例如 kendo-grid、k-options 和作用域中的 widget 引用。

备注:内容仅供参考。