Skip to content
This repository has been archived by the owner on May 4, 2022. It is now read-only.

Allow ion-nav-bar to work in modals #217

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

sinedied
Copy link

Short description of what this resolves:

This allows using <ion-nav-bar> directive properly inside modal, with animations and back button working as expected.

Changes proposed in this pull request:

  • Detects whether the <ion-nav-bar> is within a modal or not and separate those within and those not when updating.
  • Allows <ion-view> lifecycle events to be triggered when inside a modal that has a <ion-nav-view

You can now use nested states inside modals, with routes like this one:

    .state('navInsideModal', {
      // the route can have an url or not, both are working
      views: {
        'modal-nav@': {
          templateUrl: 'nav-inside-modal.html',
          controller: 'navInsideModalController as vm'
        }
      }
    })

Then you can use in a modal this way:

var modal = $ionicModal.fromTemplate(`
          <ion-modal-view> 
            <ion-nav-bar>
              <ion-nav-back-button></ion-nav-back-button>
            </ion-nav-bar>
            <ion-nav-view name="modal-nav"></ion-nav-view>
          </ion-modal-view>
        `, {
        scope: $rootScope,
        hardwareBackButtonClose: false // disable to allow back button navigation
      });
modal.show();
// disable animation for the first view to show up in modal
$ionicHistory.nextViewOptions({ disableAnimate: true });
$state.go('navInsideModal');

// now you can navigate in modal using regular $state.go() calls with
// states using the `modal-nav@` nested view. 

Ionic Version: 1.x

Fixes: #1838, #1893, trello issue, related forum post

@osirisr
Copy link

osirisr commented Jan 30, 2018

Hi, I've implemented your commit, but when the modal pops up, the view is blank and hidden. Would you know what is wrong?

UPDATE - SOLUTION: Turns out <ion-nav-bar> causes the whole screen to go blank. Using <ion-header-bar> instead works as expected.

@sinedied
Copy link
Author

@osirisr It should work with <ion-nav-bar>, allowing for proper navigation with the changes provided.

Here is the generic modal we use for navigation, along with a dedicated service to handle show/hide using navigation events and properly take care of history stuff:

modal:

<ion-modal-view id="modal-nav-screen" class="modal-nav-screen">
  <ion-nav-bar class="bar-positive has-shadow">
    <ion-nav-title ng-bind="modalViewTitle"></ion-nav-title>
    <ion-nav-back-button></ion-nav-back-button>
  </ion-nav-bar>
  <ion-nav-view name="modal-nav"></ion-nav-view>
</ion-modal-view>

modal-navigation-service.ts

/**
 * Modal navigation service: manages view navigation inside modals.
 */
export class ModalNavigationService {

  static MODAL_NAV_VIEW_TARGET = 'modal-nav@';
  static MODAL_SHOWN_EVENT = 'modalNav.shown';
  static MODAL_HIDDEN_EVENT = 'modalNav.hidden';

  private views: any;
  private rootHistory: any;

  private backView = null;
  private currentView = null;
  private modal: ionic.modal.IonicModalController = null;
  private options: ionic.modal.IonicModalOptions = {
    animation: 'slide-in-up',
    focusFirstInput: false,
    backdropClickToClose: false,
    // Disabled to allow back view navigation
    hardwareBackButtonClose: false
  };

  constructor(private $rootScope: ng.IRootScopeService,
              private $ionicModal: ionic.modal.IonicModalService,
              private $state: angular.ui.IStateService,
              private $ionicHistory: ionic.navigation.IonicHistoryService) {
  }

  /**
   * Initializes modal navigation service.
   * Hooks are set up to automatically show/hide the navigation modal with special states targeting the "modal-nav@"
   * view.
   */
  init(): void {
    if (!this.modal) {
      // Set up modal with a separate named ion-nav-view
      let options: ionic.modal.IonicModalOptions = Util.copy(this.options);
      options.scope = this.$rootScope;

      this.modal = this.$ionicModal.fromTemplate(<string>require('modal-navigation.modal.html'), options);

      // Listen to view change to show/hide modal when needed
      this.$rootScope.$on('$stateChangeStart', (event: ng.IAngularEvent,
                                                toState: angular.ui.IState,
                                                toParams: any,
                                                fromState: angular.ui.IState) => {

        let fromModal = fromState['views'] && fromState['views'][ModalNavigationService.MODAL_NAV_VIEW_TARGET];
        let toModal = toState['views'] && toState['views'][ModalNavigationService.MODAL_NAV_VIEW_TARGET];

        if (fromModal && !toModal && this.modal.isShown()) {
          // If we are navigating from a modal state to a normal state, cancel event and hide modal
          event.preventDefault();
          this.hide();
        } else if (!fromModal && toModal && !this.modal.isShown()) {
          // If we are navigating from a normal state to a modal state, show modal
          this.show();
        }
      });

      // If the modal was not hidden with hide() method, properly restore state
      this.$rootScope.$on('modal.hidden', (event: ng.IAngularEvent, modal: ionic.modal.IonicModalController) => {
        if (modal === this.modal) {
          this.hide();
        }
      });
    }
  }

  /**
   * Sets the modal navigation options.
   * Must be used before calling the `init()` method.
   * @param {IonicModalOptions} options The options to set.
   */
  setOptions(options: ionic.modal.IonicModalOptions): void {
    angular.extend(this.options, options);
  }

  /**
   * Hides the modal and restore state history properly.
   */
  hide(): void {
    let currentView = this.currentView;

    if (currentView !== null) {
      // Restore ionic history properly
      (<any>this.$ionicHistory).backView(this.backView);
      (<any>this.$ionicHistory).currentView(currentView);

      // delete all views created within the modal
      let viewHistory = this.$ionicHistory.viewHistory();
      _.each(viewHistory.views, (data: any, key: string) => {
        if (!this.views[key]) {
          delete viewHistory.views[key];
        }
      });

      // all modals are created on the root scope
      // reset history for the the 'root' history id
      viewHistory.histories.root = this.rootHistory;

      this.backView = null;
      this.currentView = null;

      this.modal.hide();

      // bugfix : delete stateParam from stateId for route with id inside url
      let stateId = currentView.stateId ? currentView.stateId.split('_').shift() : '';

      // Properly restore browser's history in case of back navigation action
      this.$state.go(stateId, currentView.stateParams);

      this.$rootScope.$broadcast(ModalNavigationService.MODAL_HIDDEN_EVENT);
    }
  }

  /**
   * Checks if the current state is shown within a modal.
   * @return {boolean} True if the current state is shown within a modal.
   */
  isModalState(): boolean {
    let views = this.$state.current['views'];
    return !!(views && views[ModalNavigationService.MODAL_NAV_VIEW_TARGET]);
  }

  /**
   * Shows the modal.
   * This method should not be called directly, it it called when trying to navigate to a state targeting the
   * "modal-nav@" view.
   */
  private show(): void {

    if (!this.modal.isShown()) {
      // Save current state to properly restore ionic history when the modal is hidden
      this.backView = this.$ionicHistory.backView();
      this.currentView = this.$ionicHistory.currentView();
      this.views = Util.copy(this.$ionicHistory.viewHistory().views);
      this.rootHistory = Util.copy(this.$ionicHistory.viewHistory().histories.root);

      this.$ionicHistory.nextViewOptions({ disableAnimate: true });
      this.modal.show();
      this.$rootScope.$broadcast(ModalNavigationService.MODAL_SHOWN_EVENT);
    }
  }
}

Just call modalNavigationService.init(); in your run block and you're set.

Here's an example route using this:

    .state('editModal', {
      views: {
        'modal-nav@': {
          template: <string>require('edit.html'),
          controller: 'editController as vm'
        }
      }
    })

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants