javascriptangularjsng-animateng-view

Angular not updating ng-class on ng-view


I'm using angular 1.6.5 for my angular application and came across a very strange behavior.

The thing I want to achieve is: when ngroute is being changed, I must remove active class from current view, wait for leave animation to complete, then add active class to the new view.

I have set up app and routs in the config.

var app = angular.module('app', ['ngAnimate', 'ngRoute']);

app.config(function ($routeProvider) {
    $routeProvider
      .when('/', {
        templateUrl:"home.html",
        reloadOnSearch:false
      })
      .when('/about-us', {
        templateUrl:"about.html",
        reloadOnSearch:false
      })
      .when('/contact', {
        templateUrl:"contact.html",
        reloadOnSearch:false
      })
      .otherwise({
          template : "<h1>None</h1><p>Nothing has been selected</p>"
      });
});

I have a service where I store animation timings and boolean indicating visibility of view:

app.service('animationProperties', function () {
  this.animationTimings = {
    views: 1000
  };
  this.visibility = {
    view : false
  };
});

I have one main controller with simple debugging function and one onRouteChangeStart function that should remove active class from current view (by making view visibility boolean false):

app.controller('MainCtrl', function ($scope, $window, $location, 
                                     animationProperties) {

  $scope.animationProperties = animationProperties;

  $scope.$on('$routeChangeStart',function () {
    animationProperties.visibility.view = false;
  });

  $scope.toggleActive = function(){
    $scope.animationProperties.visibility.view = !$scope.animationProperties.visibility.view;
  }
});

And last thing, ngAnimate that waits for leave animation to complete, then removes current view (with done() method) and enters new view again by making visibility boolean true:

app.animation('.view', function($timeout, animationProperties) {
  return {
    enter: function(element, done) {
      $timeout(function () {
        animationProperties.visibility.view = true;
        $timeout(function () {
          done();
        }, animationProperties.animationTimings.views);//Wait to enter
      },animationProperties.animationTimings.views); //Wait for leave function
    },
    leave: function(element, done) {
      $timeout(function () {
        done();
      }, animationProperties.animationTimings.views);
    }
  }
});

Here is the plunker

When switching pages first time (from navigation) you will see that everything works fine, but when going to the pages second time view class is not updating, so animation is not played. While debugging you can clearly see that visibility boolean is updated correctly, but ng-class on leaving view is not getting updated.

Your help would be much appreciated!!!


Solution

  • Quoting myself from here:

    This is what is going on:

    1. On a $routeChangeStart, you change the value that (once evaluated) will tell ngClass to remove the active class from the leaving view.
    2. At the same time, $route starts to prepare the entering view, including getting its template.
    3. Once the everything is ready, it triggers the $routeChangeSuccess event, which signals ngView to start swapping the two views.
    4. During the swapping process, ngView destroys the scope of the leaving view, from which point onwards the scope's watchers stop being...watched.

    So, if steps 1-4 happen fast enough, the leaving view's scope is destroyed before the necessary expressions are evaluated for ngClass to remove the active class. The first time you visit a route, the animation works, because $route has to make a server request for the entering view's template (which gives ngClass time to do its job). However, when you visit a previously visited route, the template is already cached and the transition is fast.


    You can work around this by deliberately slowing down the template retrieval (even a VM turn is enough). For example:

    app.decorator('$templateRequest', ($delegate, $timeout) => {
      const $templateRequest = (...args) => $delegate(...args).
        then(tmpl => $timeout().then(() => tmpl));
      Object.defineProperty($templateRequest, 'totalPendingRequests', {
        get: () => $delegate.totalPendingRequests,
        set: nv => $delegate.totalPendingRequests = nv,
      });
      return $templateRequest;
    });
    

    (Updated plnkr 1)

    Another way to work around it, is to implement your own, simplistic, synchronous version of ngClass, so that classes are applied immediately, before the leaving view's scope is destroyed. A crud, unoptimized, non-production-ready version of such a directive could look like this:

    app.directive('myClass', () => (scope, elem, attrs) => {
      scope.$watchCollection(attrs.myClass, newValue => {
        Object.keys(newValue).forEach(c => {
          if (newValue[c]) {
            elem.addClass(c);
          } else {
            elem.removeClass(c);
          }
        });
      });
    });
    

    (Updated plnkr 2)


    All that being said, yours seems like a strange setup:

    😕

    For starters, why use both a CSS and a JS animation? You could for example handle the opacity change from the JS animation (assuming your actual setup is more complex and requires the JS animation for other effects).

    Or you can much more easily handle the fade in/out with a pure CSS animation based on the automatically added/removed ng-enter/ng-leave classes (it's just 4 tiny CSS rules :smiley:):

    [ng-view].ng-enter {
      /* Wait for the leaving view to...leave, then start transitioning in. */
      transition: opacity 1s ease 1s;
    }
    
    [ng-view].ng-leave {
      /* Start transitioning out. */
      transition: opacity 1s ease;
    }
    
    [ng-view].ng-enter,
    [ng-view].ng-leave.ng-leave-active {
      /*
       * At the beginning of the entering animation and
       * at the end of the leaving animation,
       * the view must be fully invisible.
       */
      opacity: 0;
    }
    
    [ng-view].ng-enter.ng-enter-active,
    [ng-view].ng-leave {
      /*
       * At the end of the entering animation and
       * at the beginning of the leaving animation,
       * the view must be fully visible.
       */
      opacity: 1;
    }
    

    (Updated plnkr 3)