Skip to content

Commit

Permalink
fix(lifecycle): lifecycle events fire reliably from the correct scope
Browse files Browse the repository at this point in the history
  • Loading branch information
danbucholtz committed May 5, 2016
1 parent e31498c commit d637333
Show file tree
Hide file tree
Showing 16 changed files with 9,564 additions and 130 deletions.
1 change: 1 addition & 0 deletions js/angular/controller/navViewController.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ function($scope, $element, $attrs, $compile, $controller, $ionicNavBarDelegate,
if (navViewAttr(viewElement) == VIEW_STATUS_ACTIVE) {
viewScope = viewElement.scope();
viewScope && viewScope.$emit(ev.name.replace('Tabs', 'View'), data);
viewScope && viewScope.$broadcast(ev.name.replace('Tabs', 'ParentView'), data);
break;
}
}
Expand Down
30 changes: 30 additions & 0 deletions js/angular/directive/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@
* show. Also contained is transition data, such as the transition type and
* direction that will be or was used.
*
* Life cycle events are emitted upwards from the transitioning view's scope. In some cases, it is
* desirable for a child/nested view to be notified of the event.
* For this use case, `$ionicParentView` life cycle events are broadcast downwards.
*
* <table class="table">
* <tr>
* <td><code>$ionicView.loaded</code></td>
Expand Down Expand Up @@ -83,6 +87,32 @@
* <td>The view's controller has been destroyed and its element has been
* removed from the DOM.</td>
* </tr>
* <tr>
* <td><code>$ionicParentView.enter</code></td>
* <td>The parent view has fully entered and is now the active view.
* This event will fire, whether it was the first load or a cached view.</td>
* </tr>
* <tr>
* <td><code>$ionicParentView.leave</code></td>
* <td>The parent view has finished leaving and is no longer the
* active view. This event will fire, whether it is cached or destroyed.</td>
* </tr>
* <tr>
* <td><code>$ionicParentView.beforeEnter</code></td>
* <td>The parent view is about to enter and become the active view.</td>
* </tr>
* <tr>
* <td><code>$ionicParentView.beforeLeave</code></td>
* <td>The parent view is about to leave and no longer be the active view.</td>
* </tr>
* <tr>
* <td><code>$ionicParentView.afterEnter</code></td>
* <td>The parent view has fully entered and is now the active view.</td>
* </tr>
* <tr>
* <td><code>$ionicParentView.afterLeave</code></td>
* <td>The parent view has finished leaving and is no longer the active view.</td>
* </tr>
* </table>
*
* ## LifeCycle Event Usage
Expand Down
3 changes: 0 additions & 3 deletions js/angular/service/history.js
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,6 @@ function($rootScope, $state, $location, $window, $timeout, $ionicViewSwitcher, $
// create an element from the viewLocals template
ele = $ionicViewSwitcher.createViewEle(viewLocals);
if (this.isAbstractEle(ele, viewLocals)) {
console.log('VIEW', 'abstractView', DIRECTION_NONE, viewHistory.currentView);
return {
action: 'abstractView',
direction: DIRECTION_NONE,
Expand Down Expand Up @@ -417,8 +416,6 @@ function($rootScope, $state, $location, $window, $timeout, $ionicViewSwitcher, $
}
}

console.log('VIEW', action, direction, viewHistory.currentView);

hist.cursor = viewHistory.currentView.index;

return {
Expand Down
143 changes: 125 additions & 18 deletions js/angular/service/viewSwitcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -298,32 +298,67 @@ function($timeout, $document, $q, $ionicClickBlock, $ionicConfig, $ionicNavBarDe

},

emit: function(step, enteringData, leavingData) {
var enteringScope = enteringEle.scope(),
leavingScope = leavingEle && leavingEle.scope();
emit: function(step, enteringData, leavingData) {
var enteringScope = getScopeForElement(enteringEle, enteringData);
var leavingScope = getScopeForElement(leavingEle, leavingData);

if (step == 'after') {
if (enteringScope) {
enteringScope.$emit('$ionicView.enter', enteringData);
}
var prefixesAreEqual;

if (leavingScope) {
leavingScope.$emit('$ionicView.leave', leavingData);
if ( !enteringData.viewId || enteringData.abstractView ) {
// it's an abstract view, so treat it accordingly

} else if (enteringScope && leavingData && leavingData.viewId) {
enteringScope.$emit('$ionicNavView.leave', leavingData);
// we only get access to the leaving scope once in the transition,
// so dispatch all events right away if it exists
if ( leavingScope ) {
leavingScope.$emit('$ionicView.beforeLeave', leavingData);
leavingScope.$emit('$ionicView.leave', leavingData);
leavingScope.$emit('$ionicView.afterLeave', leavingData);
leavingScope.$broadcast('$ionicParentView.beforeLeave', leavingData);
leavingScope.$broadcast('$ionicParentView.leave', leavingData);
leavingScope.$broadcast('$ionicParentView.afterLeave', leavingData);
}
}
else {
// it's a regular view, so do the normal process
if (step == 'after') {
if (enteringScope) {
enteringScope.$emit('$ionicView.enter', enteringData);
enteringScope.$broadcast('$ionicParentView.enter', enteringData);
}

if (enteringScope) {
enteringScope.$emit('$ionicView.' + step + 'Enter', enteringData);
}
if (leavingScope) {
leavingScope.$emit('$ionicView.leave', leavingData);
leavingScope.$broadcast('$ionicParentView.leave', leavingData);
}
else if (enteringScope && leavingData && leavingData.viewId && enteringData.stateName !== leavingData.stateName) {
// we only want to dispatch this when we are doing a single-tier
// state change such as changing a tab, so compare the state
// for the same state-prefix but different suffix
prefixesAreEqual = compareStatePrefixes(enteringData.stateName, leavingData.stateName);
if ( prefixesAreEqual ) {
enteringScope.$emit('$ionicNavView.leave', leavingData);
}
}
}

if (leavingScope) {
leavingScope.$emit('$ionicView.' + step + 'Leave', leavingData);
if (enteringScope) {
enteringScope.$emit('$ionicView.' + step + 'Enter', enteringData);
enteringScope.$broadcast('$ionicParentView.' + step + 'Enter', enteringData);
}

} else if (enteringScope && leavingData && leavingData.viewId) {
enteringScope.$emit('$ionicNavView.' + step + 'Leave', leavingData);
if (leavingScope) {
leavingScope.$emit('$ionicView.' + step + 'Leave', leavingData);
leavingScope.$broadcast('$ionicParentView.' + step + 'Leave', leavingData);

} else if (enteringScope && leavingData && leavingData.viewId && enteringData.stateName !== leavingData.stateName) {
// we only want to dispatch this when we are doing a single-tier
// state change such as changing a tab, so compare the state
// for the same state-prefix but different suffix
prefixesAreEqual = compareStatePrefixes(enteringData.stateName, leavingData.stateName);
if ( prefixesAreEqual ) {
enteringScope.$emit('$ionicNavView.' + step + 'Leave', leavingData);
}
}
}
},

Expand Down Expand Up @@ -407,6 +442,15 @@ function($timeout, $document, $q, $ionicClickBlock, $ionicConfig, $ionicNavBarDe
containerEle.innerHTML = viewLocals.$template;
if (containerEle.children.length === 1) {
containerEle.children[0].classList.add('pane');
if ( viewLocals.$$state && viewLocals.$$state.self && viewLocals.$$state.self['abstract'] ) {
angular.element(containerEle.children[0]).attr("abstract", "true");
}
else {
if ( viewLocals.$$state && viewLocals.$$state.self ) {
angular.element(containerEle.children[0]).attr("state", viewLocals.$$state.self.name);
}

}
return jqLite(containerEle.children[0]);
}
}
Expand Down Expand Up @@ -491,4 +535,67 @@ function($timeout, $document, $q, $ionicClickBlock, $ionicConfig, $ionicNavBarDe
}
}

function compareStatePrefixes(enteringStateName, exitingStateName) {
var enteringStateSuffixIndex = enteringStateName.lastIndexOf('.');
var exitingStateSuffixIndex = exitingStateName.lastIndexOf('.');

// if either of the prefixes are empty, just return false
if ( enteringStateSuffixIndex < 0 || exitingStateSuffixIndex < 0 ) {
return false;
}

var enteringPrefix = enteringStateName.substring(0, enteringStateSuffixIndex);
var exitingPrefix = exitingStateName.substring(0, exitingStateSuffixIndex);

return enteringPrefix === exitingPrefix;
}

function getScopeForElement(element, stateData) {
if ( !element ) {
return null;
}
// check if it's abstract
var attributeValue = angular.element(element).attr("abstract");
var stateValue = angular.element(element).attr("state");

if ( attributeValue !== "true" ) {
// it's not an abstract view, so make sure the element
// matches the state. Due to abstract view weirdness,
// sometimes it doesn't. If it doesn't, don't dispatch events
// so leave the scope undefined
if ( stateValue === stateData.stateName ) {
return angular.element(element).scope();
}
return null;
}
else {
// it is an abstract element, so look for element with the "state" attributeValue
// set to the name of the stateData state
var elements = aggregateNavViewChildren(element);
for ( var i = 0; i < elements.length; i++ ) {
var state = angular.element(elements[i]).attr("state");
if ( state === stateData.stateName ) {
stateData.abstractView = true;
return angular.element(elements[i]).scope();
}
}
// we didn't find a match, so return null
return null;
}
}

function aggregateNavViewChildren(element) {
var aggregate = [];
var navViews = angular.element(element).find("ion-nav-view");
for ( var i = 0; i < navViews.length; i++ ) {
var children = angular.element(navViews[i]).children();
var childrenAggregated = [];
for ( var j = 0; j < children.length; j++ ) {
childrenAggregated = childrenAggregated.concat(children[j]);
}
aggregate = aggregate.concat(childrenAggregated);
}
return aggregate;
}

}]);
Loading

0 comments on commit d637333

Please sign in to comment.