ionic-team / ionic-v1

The repo for Ionic 1.x. For the latest version of Ionic, please see https://github.com/ionic-team/ionic
Other
193 stars 187 forks source link

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

Open sinedied opened 7 years ago

sinedied commented 7 years ago

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:

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 commented 6 years ago

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 commented 6 years ago

@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'
        }
      }
    })