AngularJS: $viewContentLoaded fired before partial view appears
Asked Answered
F

3

26

For a partial view I want to do some JavaScript stuff that I usually would do with $(document).ready(function() {...}), e.g. bind venet listeners to elements. I know that this doesn't work for AngularJS and partial views loaded into the "root" view.

Thus I added a listener to the controller that listens to the $viewContentLoaded event. The listener's function is invoked, so the event is fired but it seems to me as if it is before the partial view is rendered. Neither do I see the elements when I set a breakpoint in the listener's function and debug it with firebug, nor does the jquery selection within the function find the partial view's elements.

This is what the controller looks like:

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

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

[...]

I guess that I am doing something wrong as there had to be more posts on stackoverflow if this is a common bug.

As I am using ui-router and ui-view, I will give you an excerpt of the routing file:

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');
    }])

 [...]

Any help is appreciated. Thanks and kind regards

UPDATE 1: I stripped the error down to the following usecase: The loginView.html looks like the following:

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

[...]

As soon as I remove the ng-if from the div tag, it works as expected. The event is triggered after the DOM is rendered, thus jQuery finds the element. If the ng-if is attached to the div tag, the behaviour is as first described.

UPDATE 2: As promised I added a working demo that shows the different behaviour when adding a ng-if directive. Can anyone point me the right direction? Don't stick to the login form as such, as there are many more use cases where I want to remove certain parts of a view based on some expression and do some JavaScript stuff after the partial view is ready.

You can find the working demo here: Demo

Featherhead answered 25/11, 2014 at 14:52 Comment(5)
You can create a directive for you DOM stuff.Ryanryann
I've created a working demo based on your code and it's working correctly. Next time, please try to provide the demo yourself.Inwards
Many thanks for the working demo. I have updated the description as I found out that the ng-if causes the different behaviour. Do you have an idea why it behaves differently? The expression is evaluated to "true" as I can see the login form, just a moment too late ;)Featherhead
I will add a working demo later on and post the link for further investigation.Featherhead
The DOM is built iteratively through $digest cycles. $viewContentLoaded is fired as soon as the ui-view has the template compiled, and the controller has been executed. The dom may change at the next $digest in response to something that happened in the controller, or whatever. There is no good mechanism to replicate document.ready() style jquery.Eleaseeleatic
G
47

This is related to angular digest cycle, it's about how angular works underneath the hood, data binding etc. There are great tutorials explaining this.

To solve your problem, use $timeout, it will make the code execute on the next cycle, whem the ng-if was already parsed:

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

Fixed demo here: http://codepen.io/anon/pen/JoYPdv

But I strongly advise you to use directives do any DOM manipulation, the controller isn't for that. Here is a example of how do this: Easy dom manipulation in AngularJS - click a button, then set focus to an input element

Geralyngeraniaceous answered 29/11, 2014 at 1:25 Comment(4)
I will consider this as the most valuable answer, as it provides a possible solution and a hint to find further information about the digest cycle, though I am using directive now.Featherhead
Very well explained! i was able to integrate App.js and AngularJS by using the above code chunk and also understood timeout intricate detail.Leekgreen
The problem with this approach is that in ui-router $viewContentLoaded fires for each and every view. If you have lots of views in a single page your code would execute multiple times. Is there a way to have it execute just once when ALL views have loaded?Enrollment
I wrote a set of directives that register their geometries with a service to enable geometric navigation around the page. It behaved extremely unpredictably until I read this. 50msec delay on geometry calculation fixed me up! I'll be on the lookout for a more deterministic approach.Manifesto
B
1

I have solved this issue with the help of Directives. Add one direcitve to your element (like <div my-dir></div>) and do manipulations to the element in respective directive as follows,

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

I have also tried state provider events like $stateChangeSuccess, $viewContentLoaded but couldn't solve the issue. Because after those events got fired, it's taking time to render on the DOM.

So, we can follow this approach which gives perfect results and proper way to implement in Angular JS :)

Balikpapan answered 28/7, 2016 at 9:28 Comment(0)
F
0

This is an answer to Hades a bit late but might help someone else. I setup a service which I will later be able to call from a controller or directive.

'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();
        }

    };
});

In the controller or directive include viewContentLoaded and call the get function with the promise. Don't forget to change the app to whatever you app name is and include the service file to be loaded.

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

}, function(reason) {
    //$log.error(reason);
});
Faradmeter answered 15/1, 2016 at 9:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.