Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

Ng-class on ng-view not updating when route is being changed #16239

Closed
1 of 3 tasks
gogachinchaladze opened this issue Sep 23, 2017 · 3 comments
Closed
1 of 3 tasks

Ng-class on ng-view not updating when route is being changed #16239

gogachinchaladze opened this issue Sep 23, 2017 · 3 comments

Comments

@gogachinchaladze
Copy link

I'm submitting a ...

  • bug report
  • feature request
  • other (Please do not submit support requests here (see above))

Current 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.

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.

Expected / new behavior:
ng-class on leaving view must be updated.

Minimal reproduction of the problem with instructions:
Here is the plunker:
http://plnkr.co/edit/huGdIRY5ukJajRU1x0li?p=preview

AngularJS version: 1.6.5, 1.6.6

Browser: [Chrome Canary 63.0.3218.0 | Chrome 60.0.3112.113| Firefox 47.0 | Safari 10.0.3]
Operating system: MacOs Sierra 10.3

Question on stackoverflow:
https://stackoverflow.com/questions/46365402/angular-not-updating-ng-class-on-ng-view

@gkalpak
Copy link
Member

gkalpak commented Sep 23, 2017

This is what (I think) 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)

Sidenote:
An obvious "fix" that comes to mind is delaying the scope destruction until after the leave animation is comleted. I.e. moving this line...

currentScope.$destroy();

...down here...

previousLeaveAnimation.done(function(response) {
if (response !== false) previousLeaveAnimation = null;
});

This still doesn't work (in this case at least), because ngClass will find the ongoing .view animation and attach itself to it, essentially delaying the removal of the active class for the duration of the animation.

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

  • Listen for an event ($routeChangeStart).
  • Set a flag (animationProperties.visibility.views) that triggers ngClass to remove a class.
  • Removing the class, triggers a CSS animation.
  • In the meantime, have a custom JavaScript animation (animation('.view')) synchronize itself with the CSS animation and then set the flag back.
  • By setting the flag back, trigger the opposite CSS animation for the entering view.

😕

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 😃):

[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)

@gogachinchaladze
Copy link
Author

Thanks a lot for help!!!

The plunker is simplified for the question, in real case the animations are much more complicated and I'm animating elements outside the view, so css only solution will not work in that case.

The solution I found was that on ng-view only, I'm just adding/removing class with angular.element and that works perfectly, without any delays and stuff.

But I really needed explanation why the bug happens, so thank you again for your answer!

@gkalpak
Copy link
Member

gkalpak commented Sep 23, 2017

Great! Glad you were able to work it out.
I am going to close this then, since I don't see how we can fix it (without breking other usecases).

@gkalpak gkalpak closed this as completed Sep 23, 2017
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

2 participants