Ionic-framework: bug: $ionicView.leave doesn't fire with nested views.

Created on 12 Aug 2015  路  39Comments  路  Source: ionic-team/ionic-framework

Type: bug

Platform: all

If you have an app with a nested ion-nav-view, the "$ionicView.leave" event is not called on the child view. (It is called on the parent view) You have to resort to using "$destroy".

In the CodePen below, you can see that the child controllers "$ionicView.leave" event is not called.

CodePen:

http://play.ionic.io/app/6eafd1a2beb4

Most helpful comment

Yes it's working after 1.3.1

All 39 comments

Try using $ionicNavView.leave

Doesn't seem to work - in fact if I put this in the PrivateChildCtrl:

$scope.$on("$ionicNavView.leave", function(){
  console.log("IonicNavView leave event");
});

It gets called when entering (???) the child view after pressing Login.

I guess I should say that the event _does_ fire - but maybe it's being caught and prevented from propogating by the parent view?

Okay, seeing this as well. I'll let @adamdbradley take a look at this.

+1, also experiencing this

Any progress?

@adamdbradley Hello, have you looked through this issue?

Also experiencing this.

  1. When I go to tab view inside one of menu items $ionicView.enter fires correctly.
  2. When I switch between tabs in this same view (same menu item) $ionicView.leave and $ionicView.enter are fired correctly.
  3. If I switch to other menu item out of tab which needs to fire $ionicView.leave nothing happens. HTML inspector shows that tabs stays in "active" state and don't go to cache or other inactive state.
  4. $ionicNavView.leave doesn't help here (was discussed in other similar issue)

Short vid with output of leave events
http://quick.as/bnbzir5j9

Code example is down here:

routing:

$stateProvider
  .state('app', {
    url: '/app',
    abstract: true,
    templateUrl: 'templates/menu.html',
  })

  .state('app.tabs', {
    url: '/tabs',
    abstract: true,
    views: {
      'menuContent': {
        templateUrl: 'templates/tabs.html'
      }
    }
  })
    .state('app.tabs.tab1', {
      url: "/tab1",
      views: {
        'report-tab': {
          templateUrl: "templates/tabs/tab1.html",
          controller: 'TabOneCtrl' // HERE we need to $ionicSideMenuDelegate.canDragContent(false);
                                   // because we use some component that uses slide as well
        }
      }
    })
    .state('app.tabs.tab2', {
      url: "/history",
      views: {
        'history-tab': {
          templateUrl: "templates/tabs/tab2.html",
        }
      }
    })

  .state('app.some-view', {
    url: '/some-view',
    views: {
      'menuContent': {
        templateUrl: 'templates/some-view.html',
      }
    }

controller:

angular.module('app.controllers', [])
  .controller('TabOneCtrl', function($scope, $ionicSideMenuDelegate, ...) {
    /* controller code here */

    // Disable menu swipe as it interferes with tab1 UX
    $scope.$on('$ionicView.enter', function(){
      $ionicSideMenuDelegate.canDragContent(false);
    });
    $scope.$on('$ionicView.leave', function(){
      $ionicSideMenuDelegate.canDragContent(true);
    });
  });

You have a typo in your controller. It should be $ionicView.leave Not $ionicView.Leave. Lowercase "l"

@spectravp I'm sorry. this typo happened later after copying code here when I've tried beforeLeave and afterLeave.
leave doesn't work.

You don't catch me so easy :P

+1
also seeing $ionicView.leave and $destroy fired and caught not by the scope of the state that is being left, but by the previous state.

@demoalex same issue here. But more deeper...
I have two tab views, same as you on menu switch the last view will stay active. After opening a view in the other tab view, all view events will fire in the last view from the first tab view... strange behavior. (To test change the url #/home/module1/entry3 to #/home/module2/entry3 )

See the console.log in this plunker:
http://plnkr.co/edit/p4Uj1buIEH9YfqsNCEvN?p=preview

See this plunker http://plnkr.co/edit/7z5pU2bzFj3sA04ndsCa?p=preview
For example the Menu Entries only change to the last parent state. You can see that the ionicView.enter will call on the wrong scope.

Will be resolved with v2 navigation

a bit too late for me, pity

Sounds like this might be the same issue as #2617 . Kind of silly that we can't really use one of the main features of ui-router (nested states), and haven't been able to for over a year now.

This is really important for me to cancel a timer and $ionicView.leave still not work

@onigetoc you can try setting view caching to false and using the $destroy event

I used the $destroy and it's working, but i do not understand why the IONIC theam do not fix the .leave function.

Any progress on this issue ?

I'm willing to beta test this issue if helpfull.

Hi Guys!

I was looking for the solution of my problem and I ended up here.

Could you guys give me some help?

If there is not the place to ask it, I ask for sorry.

I need that "$ionicView.afterEnter" be triggered in a view, but there's no way to get it working.

I don't understand what is going on.

The basic structure in my project is a login and after this a main page with a side menu.

When a go to the "app.newEvent" page to add itens to database, I'd like to clear the form before input some data, because maybe it was filled by user in a first time.

Below there's my $stateProvider:

(function() {
    'use strict';

    angular.module('app')
    .config(appConfig);

    appConfig.$inject = ['$stateProvider', '$urlRouterProvider', '$ionicConfigProvider'];

    function appConfig($stateProvider, $urlRouterProvider, $ionicConfigProvider) {

        $ionicConfigProvider.backButton.text('').icon('ion-ios-arrow-back')
        $ionicConfigProvider.views.transition('ios');
        /*  $ionicConfigProvider.views.transition('none'); */

        $stateProvider

        .state('splash', {
            url: '/splash',
            templateUrl: 'templates/splash.html',
            controller: 'ctrlSplash',
            controllerAs: 'vm'
        })

        .state('login', {
            url: '/login',
            templateUrl: 'templates/login/login.html',
            controller: 'ctrlLogin',
            controllerAs: 'vm'
        })

        .state('signin', {
            url: '/signin',
            templateUrl: 'templates/login/signin.html',
            controller: 'ctrlSignin',
            controllerAs: 'vm'
        })

        .state('terms', {
            url: '/terms',
            templateUrl: 'templates/terms/terms.html',
        })

        .state('app', {
            url: '/app',
            abstract: true,
            templateUrl: 'templates/menu/menu.html',
            controller: 'ctrlMenu',
            controllerAs: 'vm'
        })

        .state('app.main', {
            url: '/main',
            views: {
                'menuContent': {
                    templateUrl: 'templates/main/main.html',
                    controller: 'ctrlMain',
                    controllerAs: 'vm'
                },
                'menuLeft': {
                    templateUrl: 'templates/menu/menuLeft.html',
                    controller: 'ctrlMenu',
                    controllerAs: 'vm'
                }
            }
        })

        .state('app.terms', {
            url: '/terms',
            views: {
                'menuContent': {
                    templateUrl: 'templates/terms/terms.html'
                },
                'menuLeft': {
                    templateUrl: 'templates/menu/menuLeft.html',
                    controller: 'ctrlMenu',
                    controllerAs: 'vm'
                }
            }
        })


        .state('app.newEvent', {
            url: '/newEvent',
            cache: true,
            views: {
                'menuContent': {
                    templateUrl: 'templates/newEvent/newEvent.html',
                    controller: 'ctrlNewEvent',
                    controllerAs: 'vm'
                },
                'menuLeft': {
                    templateUrl: 'templates/menu/menuLeft.html',
                    controller: 'ctrlMenu',
                    controllerAs: 'vm'
                }
            }
        })

        /* this is a tab abstract wrapper */
        .state('app.eventDetail', {
            url: '/eventDetail',
            abstract: true,
            cache: true,
            views: {
                'menuContent': {
                    templateUrl: 'templates/eventDetail/eventDetail.html'
                }
            }
        })

        .state('app.eventDetail.poll', {
            url: '/poll',
            views: {
                'tab-poll': {
                    templateUrl: 'templates/eventDetail/poll/poll.html',
                    controller: 'ctrlPoll as vm'
                }
            }
        })

        .state('app.eventDetail.participants', {
            url: '/participants',
            views: {
                'tab-participants': {
                    templateUrl: 'templates/eventDetail/participants/participants.html',
                    controller: 'ctrlParticipants as vm'
                }
            }
        })

        .state('app.addPeople', {
            url: '/addPeople',
            views: {
                'menuContent': {
                    templateUrl: 'templates/eventDetail/participants/addPeople/addPeople.html',
                    controller: 'ctrlAddPeople as vm'
                }
            }
        })

        .state('app.addPlace', {
            url: '/addPlace',
            views: {
                'menuContent': {
                    templateUrl: 'templates/eventDetail/poll/addPlace/addPlace.html',
                    controller: 'ctrlAddPlace as vm'
                }
            }
        })


        // if none of the above states are matched, use this as the fallback
        $urlRouterProvider.otherwise('/splash');
    }

})();

This is the controller "ctrlNewEvent":

(function() {
    'use strict';

    angular.module('app')
        .controller('ctrlNewEvent', ctrlNewEvent);

    ctrlNewEvent.$inject = ['$scope', '$state', '$stateParams', '$location', '$ionicHistory', '$ionicPlatform', '$window', '$ionicScrollDelegate', '$ionicLoading', 'authFactory', 'eventsFactory', 'errMsgFactory'];

    function ctrlNewEvent($scope, $state, $stateParams, $location, $ionicHistory, $ionicPlatform, $window, $ionicScrollDelegate, $ionicLoading, authFactory, eventsFactory, errMsgFactory) {
        /* jshint validthis: true */
        var vm = this;
        vm.msgs = [];
        vm.frmNew = {};


        /* External Functions
        ----------------------------------------------------------------------*/
        vm.goHome = goHome;
        vm.createGroup = createGroup;
        vm.dateClick = dateClick;

        /* VARs - END -
        ===================================================================== */



        vm.backButtonPressed = $ionicPlatform.registerBackButtonAction(
            goHome, 1000
        );

        debug('ctrlNewEvent called! 1');
        clearForm();
        setFocus();


        /* verify if user is logged
        ---------------------------------------------*/

        $scope.$on('$ionicNavView.afterEnter', function() {
            debug('$ionicNavView.afterEnter called');
        });

        $scope.$on("$ionicView.enter", function() {
            debug("$ionicView.enter - NewEvent.controller.js");
        });

        $scope.$on('$ionicNavView.afterEnter', function() {
            debug('ctrlNewEvent called! 2');
            if ((authFactory.isLogged())) {
                debug('ctrlNewEvent.isLogged() called - user logged');

            } else {
                debug('ctrlNewEvent.isLogged() called - user not logged');
                authFactory.resetAuthUser();
                $ionicHistory.nextViewOptions({
                    disableBack: true
                });
                $state.go('login');
            }
        });



        $scope.$watch("vm.frmNew.groupName", function(newValue, oldValue) {
            //debug('groupName: oldValue = ' + oldValue + ' / newValue = ' + newValue);
            if ((vm.frmNew.groupName) && (vm.frmNew.groupName.length > 0) && (vm.frmNew.date)) {
                vm.formOk = true;
            } else {
                vm.formOk = false;
            };
        });

        $scope.$watch("vm.frmNew.date", function(newValue, oldValue) {
            //debug('vm.frmNew.date: oldValue = ' + oldValue + ' / newValue = ' + newValue);
            if ((vm.frmNew.groupName) && (vm.frmNew.groupName.length > 0) && (vm.frmNew.date)) {
                vm.formOk = true;
            } else {
                vm.formOk = false;
            };
        });


        /*--------------------------------------------------------------------
        * Local functions
        ----------------------------------------------------------------------*/

        function clearForm() {
            vm.frmNew.groupName = '';
            vm.frmNew.groupDesc = '';
            vm.frmNew.date = new Date();
            vm.frmNew.date.setMilliseconds(0);
            vm.frmNew.date.setSeconds(0);
            vm.formOk = false;
        };

        function gotoAfterCreate(objId) {
            debug(objId);
            eventsFactory.setActiveEvent(objId);
            $ionicHistory.nextViewOptions({
                disableBack: true
            });
            $state.go('app.eventDetail.participants');
        };

        function setFocus() {
            debug('ctrlNewEvent.setFocus() called!')
            $('#newgroupname').focus();
            $location.hash('newgroupname');
            $ionicScrollDelegate.anchorScroll();
        };

        function setLabels(date) {
            vm.frmNew.dateLabel = getDateFormated(date);
            vm.frmNew.timeLabel = getTimeFormated(date);
            //vm.frmNew.dateTimeLabel = fomatDateTimeLabel(date);
        };

        function getDateFormated(inputDate) {

            var today = inputDate;
            if (typeof(today) === 'undefined') {
                today = new Date();
            }

            var dd = today.getDate();
            var mm = today.getMonth() + 1; //January is 0!
            var yyyy = today.getFullYear();

            if (dd < 10) {
                dd = '0' + dd
            }

            if (mm < 10) {
                mm = '0' + mm
            }

            //today = mm + '/' + dd + '/' + yyyy; <- American format
            today = dd + '/' + mm + '/' + yyyy;
            return today;
        }


        function getTimeFormated(inputTime) {

            var iTime = inputTime;
            if (typeof(iTime) === 'undefined') {
                iTime = new Date();
                iTime = new Date(iTime.getTime() - iTime.getTimezoneOffset());
            }

            var hh = iTime.getHours();
            var mm = iTime.getMinutes();

            if (hh < 10) {
                hh = '0' + hh
            }

            if (mm < 10) {
                mm = '0' + mm
            }

            var now = hh + ':' + mm;
            return now;
        }




        /*--------------------------------------------------------------------
        * Public functions
        ----------------------------------------------------------------------*/

        function dateClick() {
            debug('dateClick called!');
            //angular.element('#datetimelocal').trigger('focus');
            angular.element('#datetimelocal').trigger('click');
        }

        function createGroup() {
            vm.msgs = [];
            debug('newEvent.controller.js - createGroup() called');
            $ionicLoading.show({
                template: 'Criando evento...'
            });
            var nEvent = {};
            nEvent.groupName = vm.frmNew.groupName;
            nEvent.groupDesc = vm.frmNew.groupDesc;
            nEvent.groupDate = vm.frmNew.date;
            nEvent.groupTime = vm.frmNew.date;


            /*
            ver esse para usar:
            personService.getPerson()
            .then(function(personResult){
            return TranslateData.getTranslation();
        })
        .then(function(translationResult){
        // do something after translation
    });
    fonte: http://stackoverflow.com/questions/27603673/how-to-call-angular-factory-after-another-is-finished
    */


            //alert('peopleSearch ' + vm.partialId)
            eventsFactory.createEvent(nEvent).then(function(response) {
                debug(response);
                debug('createEvent().response[0].status = ' + response.status)
                if (response.status == 0) {
                    debug('startVote() input parameters:');
                    debug(response);
                    debug(nEvent.groupDate);
                    debug(nEvent.groupTime);

                    eventsFactory.getAllEvents().then(function(resp2) {
                        debug('eventsFactory.getAllEvents():')
                        debug(resp2);
                        vm.groups = resp2;
                    }, function(error2) {
                        debug('eventsFactory.getAllEvents():')
                        debug(error2);
                        vm.msgs.push(error2.msg);
                    }).finally(function() {
                        gotoAfterCreate(response.data.groupid);
                    });

                }
            }, function(error) {
                debug('eventsFactory.getAllEvents():')
                debug(error);
                vm.msgs.push(error.msg);
            }).finally(function() {
                $ionicLoading.hide();
            });;
        };

        function goHome() {
            $ionicHistory.nextViewOptions({
                disableBack: true
            });
            $state.go('app.main');
        };

    }
})();

Thank you!

In my case $ionicView.beforeLeave is fired normally, but $ionicView.leave and $ionicView.afterLeave are not fired.

I tried to trace back the Ionic https://github.com/driftyco/ionic/blob/e2727d2e8f0815c3418e1fc29c92e2180e513408/js/angular/service/viewSwitcher.js code. This is all I got:

  1. The after step does not fire $ionicView.leave and $ionicView.afterLeave because leavingEle (https://github.com/driftyco/ionic/blob/e2727d2e8f0815c3418e1fc29c92e2180e513408/js/angular/service/viewSwitcher.js#L303) is undefined (so does the leavingScope variable)
  2. Said leavingEle is undefined because it never gets assign from https://github.com/driftyco/ionic/blob/e2727d2e8f0815c3418e1fc29c92e2180e513408/js/angular/service/viewSwitcher.js#L78
  3. Which because somehow navViewCtrl.getViewElements() returns less elements than usual and does not include the leavingEle but contain the enteringEle

However the before step is working just fine (and navViewCtrl.getViewElements() returns nav elements that include both the leavingEle and enteringEle).

I ran out of time and couldn't dig further, hope this information help you guys.

any progess on this issue?
I need an event to cancel my timers...
setting cache to false is not a solution (the view is very very heavy: gmaps + ++++ lots of things...)

thanks

@clembou590 Could you use $ionicView.beforeLeave to cancel your timers instead? It works on my case.

No it's not called.

    $scope.$on('$ionicView.leave', function() {
        console.log("leave main renter view");

    });


    $scope.$on('$ionicView.beforeLeave', function() {
        console.log("beforeLeave main renter view");

    });



    $scope.$on('$ionicView.afterLeave', function() {
        console.log("afterLeave main renter view");

    });

none of these are called when I leave the "abstract state"
ex:
when going from sideMenu.page1 to sideMenu.page2 everything is fired.
but when going from sideMenue.page1 to "anotherPage" nothing is fired.

Hello everyone,

I have looked into this issue and it seems like a risky fix for Ionic 1. It would be a major undertaking. We're really sorry about that. Navigation and the life cycle events are complicated and somewhat fragile, so I am hesitant to make a change and break something else.

I have a work around that is low risk:

In your parent view controller, listen for the $ionicView.beforeLeave event. This event fires more reliably then the $ionicView.leave event when interacting with one-or-more abstract and parent views.

After your parent controller receives this event, you can $broadcast an event down to the child views/controllers.

$scope.$on("$ionicView.beforeLeave", function(event, data){
   // the parent has received the event, so broadcast an event to the children
   $scope.$broadcast("parentViewLeaving", {});
});

We know this is not ideal - but it will work.

Please let me know if you have any issues with this. If we come up with a low risk solution for this, we will re-open this issue.

I have tested this with @c2software's excellent plunkr. Thank you for contributing everyone.

Thanks,
Dan

Hello everyone,

We made some changes to life cycle events and the life cycle events should fire from the correct scope reliably in the next version of Ionic. We also added some new events that fire downwards specifically for the case of child/nested ui-router views.

Please let me know if you have any questions, comments, concerns, etc. Thanks again for contributing and for using Ionic.

https://github.com/driftyco/ionic/commit/f19b2fe9f99cb2b3cfb906f07e68f2fcaf3fbc6a

Thanks,
Dan

@danbucholtz So this issue will not be fixed until Ionic 2 release?

No, it was released in 1.3.1 which went out into the wild on Thursday. See the release here.

Yes it's working after 1.3.1

If you cannot upgrade to 1.3.1 for some reason, $scope.$on('$destory', function () {...}) can be a alternative solution. Be careful with cache: false in router. Hope this can be helpful someone struggle with this problem :)

I am using Ionic 1.3.1 and my $ionicView.leave still does not fire when switching tabs.

@babinc,

Are your templates containers? If not, can you wrap in a div? This should resolve the issue.

Thanks,
Dan

my templates are using templateUrl's with an html file and an ion-view. I managed to get what i needed to work by using a combination of $destroy and $ionicView.beforeLeave.

I was having similar issues and it turns out to be caused by what @babinc described. In my view's html file, I also had some of my modal templates. Example:

<script id="custom-modal.html" type="text/ng-template">
    <div class="modal">
        <ion-header-bar class="bar bar-stable">
            <!-- Amazing Header -->
        </ion-header-bar>
        <ion-content delegate-handle="createScroll">
            <!-- Amazing Content -->
        </ion-content>
    </div>
</script>

Moving these <script type="text/ng-template"> into their own html files alone made it so all of the events I used to use like $ionicView.leave and $ionicView.enter all started working again.

cc: @danbucholtz thanks for the help, it was indeed "something else"

aah, beforeLeave fires if you wrap the abstract state ion-nav-view in a div element like this:
template: '<div><ion-nav-view animation="slide-left-right" class="ion-nav-view-nested"/></div>',
instead of
template: '<ion-nav-view animation="slide-left-right" class="ion-nav-view-nested"/>',

then everything works

I'm using Ionic v1.7.13 and i have same problema.

My app enter in first view, using tabs, but not fire any view life cycle (loaded, enter, beforeEnter...)

This problem persist when navigate by tabs.

I found a work around for this.
You can use this to observe any 'to and from' view changes. Just doing a string comparison on the url of the fromState emulated ionicView.leave well enough to achieve what I needed it for. Keep in mind you need to be using UI-router to do this. Hope this helps!

$rootScope.$on('$stateChangeStart', 
    function(event, toState, toParams, fromState, fromParams, options){ 
        if(fromState.url == "/video/:Id"){
            console.log("Leaving the view.");
        }
    });

hi,everyone
if you have modal content ,don't write it in main page,write it in new page instead.
it will work

Thanks for the issue! This issue is being locked to prevent comments that are not relevant to the original issue. If this is still an issue with the latest version of Ionic, please create a new issue and ensure the template is fully filled out.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

GeorgeAnanthSoosai picture GeorgeAnanthSoosai  路  3Comments

MrBokeh picture MrBokeh  路  3Comments

RobFerguson picture RobFerguson  路  3Comments

BilelKrichen picture BilelKrichen  路  3Comments

manucorporat picture manucorporat  路  3Comments