AngularJS: $viewContentLoaded在部分视图出现之前触发

26

对于一个局部视图,我想要执行一些通常用$(document).ready(function() {...})完成的 JavaScript 操作,比如将事件监听器绑定到元素上。但我知道这在 AngularJS 和加载到“根”视图中的局部视图中不起作用。

因此,我在控制器中添加了一个监听器来监听 $viewContentLoaded 事件。监听器函数被调用,所以事件被触发,但是似乎在局部视图渲染之前。当我在监听器函数中设置断点并使用 firebug 进行调试时,看不到元素,也不能通过该函数内的 jQuery 选择器找到局部视图的元素。

这是控制器的样子:

angular.module('docinvoiceClientAngularjsApp')
  .controller('LoginController', function ($scope, $rootScope) {

$scope.$on('$viewContentLoaded', function(event) {
  console.log("content loaded");
  console.log($("#loginForm"));   // breakpoint here 
});

[...]

如果这是一个常见的错误,stackoverflow 上应该有更多的帖子,我猜我做错了什么。

由于我正在使用 ui-routerui-view,我将给您提供路由文件的摘录:

angular
  .module('docinvoiceClientAngularjsApp', [
    'ui.router',
    'ngAnimate',
    'ngCookies',
    'ngResource',
    'ngMessages',
    'ngRoute',
    'ngSanitize',
    'ngTouch'
  ])
 .config(function ($routeProvider, $stateProvider) {
    $stateProvider
    .state('login', {
        url: '/',
        templateUrl: 'components/login/loginView.html',
        controller: 'LoginController'
    })
    .run(['$state', function ($state) {
        $state.transitionTo('login');
    }])

 [...]

非常感谢您的帮助。谢谢并致以诚挚的问候。

更新1:我将错误简化为以下用例:loginView.html如下所示:

<div id="loginContainer" style="width: 300px">
  <form id="loginForm" ng-submit="login(credentials)" ng-if="session.token == undefined">

[...]
一旦我从div标签中删除ng-if,它将按预期工作。事件在DOM呈现后触发,因此jQuery会找到该元素。如果
标签附加ng-if,则行为与最初描述的相同。 更新2:如承诺所示,我添加了一个工作演示,展示了添加ng-if指令时不同的行为。有人能指点我正确的方向吗?请不要局限于登录表单本身,因为还有许多用例,在这些用例中,我希望基于一些表达式删除视图的某些部分,并在部分视图准备好后执行一些JavaScript操作。
您可以在此处找到工作演示:Demo

你可以为你的DOM元素创建一个指令。 - dfsq
1
我已经根据你的代码创建了一个正常工作的演示,并且它正常运行。下次请尝试自己提供演示。 - hon2a
非常感谢您提供的工作演示。我已经更新了描述,因为我发现ng-if导致了不同的行为。您有什么想法为什么它会表现得不同吗?正如我所看到的那样,表达式被评估为“true”,只是稍微晚了一点才能看到登录表单 ;) - Florian
我稍后会添加一个可工作的演示,并发布链接以供进一步调查。 - Florian
DOM是通过$digest循环迭代构建的。$viewContentLoaded在ui-view编译模板并执行控制器后立即触发。DOM可能会在下一个$digest中响应控制器中的某些事件而发生变化。目前没有很好的机制来复制document.ready()风格的jquery。 - Chris T
3个回答

47

这与Angular的脏检查循环有关,涉及Angular在底层如何工作、数据绑定等内容。有很多优秀的教程可以解释这些。

要解决您的问题,请使用$timeout,它将使代码在下一个循环执行,此时ng-if已被解析:

app.controller('LoginController', function ($scope, $timeout) {
    $scope.$on('$viewContentLoaded', function(event) {
      $timeout(function() {
        $scope.formData.value = document.getElementById("loginForm").id;
      },0);
    });
});

固定演示在这里:http://codepen.io/anon/pen/JoYPdv

但我强烈建议您使用指令进行任何DOM操作,控制器不是用于此目的。以下是如何执行此操作的示例: AngularJS中轻松进行DOM操作 - 单击按钮,然后将焦点设置到输入元素


我会把这个视为最有价值的答案,因为它提供了一种可能的解决方案,并提示如何找到有关摘要周期的进一步信息,尽管我现在正在使用指令。 - Florian
讲解得非常清楚!我能够通过使用上面的代码块集成App.js和AngularJS,并且也理解了timeout的细节。 - NiRUS
7
这种方法的问题在于,在ui-router中,$viewContentLoaded事件会针对每个视图都触发。如果你在单个页面中有很多视图,你的代码将会执行多次。是否有一种方法可以使其在所有视图加载完成后仅执行一次? - Hades
我编写了一组指令,它们向服务注册其几何信息,以便在页面上实现几何导航。在阅读这篇文章之前,它的行为极不可预测。50毫秒的几何计算延迟解决了我的问题!我将寻找更确定性的方法。 - Thomson Comer

1

我已经通过指令解决了这个问题。在你的元素上添加一个指令(例如<div my-dir></div>),并在相应的指令中对元素进行操作,如下所示:

app.directive('myDir', function () {
    return {
        restrict: 'A',
        link: function (scope, element) {
           // Do manipulations here with the help of element parameter
        }
    };
});

我也尝试过状态提供者事件,比如$stateChangeSuccess$viewContentLoaded,但无法解决问题。因为在这些事件触发后,渲染到DOM上需要时间。
所以,我们可以采用这种方法来实现Angular JS中的完美结果和正确方式 :)

0
这是对Hades的回答,可能有点晚了,但可能会帮助其他人。我设置了一个服务,稍后可以从控制器或指令中调用它。

'use strict';

app.factory('viewContentLoaded', function($q, $rootScope, $timeout) {
    var viewContentLoaded = $q.defer(),
        
        foo = function() {
            $timeout(function() {
                viewContentLoaded.resolve();
                // Get all entries
            }, 100);//Timeout
        },

        checkViewContenLoadedListner = $rootScope.$on('$viewContentLoaded', foo);//Rootscope

    return {
        getLoaded: function() {      
            return viewContentLoaded.promise;
        },
        removeViewContenListner: function() {
            //Remove event listern on $viewContentLoaded no $off so call it will unsubscribe it
            //$rootScope.$off('$viewContentLoaded', foo);//Rootscope
            //Don't forget to unsubscribe later
            checkViewContenLoadedListner();
        }

    };
});

在控制器或指令中包含viewContentLoaded,并使用promise调用get函数。不要忘记将app更改为您的应用程序名称,并包含要加载的服务文件。

viewContentLoaded.getLoaded().then(function(){
    //Remove Listner when done
    viewContentLoaded.removeViewContenListner();
    //Your code to run       

}, function(reason) {
    //$log.error(reason);
});


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接