Replacing Deprecated getRootNav Call in Ionic

January 5th 2018 Ionic Framework

Ionic 3.5 introduced the concept of multiple root navigation elements to fully support SplitPane, for example. This required API changes, among others making App.getRootNav() function deprecated:

(getRootNav) is deprecated and will be removed in the next major release. Use getRootNavById instead.

Although the guidance seems to be clear, there's still no good answer to what the value of the required parameter for the replacement function should be. The correct approach to avoid calling the deprecated function depends on the way it was used. In the rest of the post I'll assume that we're dealing with an application which only has a single root navigation element.

The suggested solution will be easier to apply if getRootNav() is only called from a single location in the application. I find that a good practice in general - to have a dedicated navigation provider taking care of all the navigation in the application. So, no more direct access to the NavController in almost every page of the application:

import { Component } from '@angular/core';
import { IonicPage, NavController } from 'ionic-angular';

@IonicPage()
@Component({
  selector: 'page-first',
  templateUrl: 'first.html'
})
export class FirstPage {

  constructor(private navCtrl: NavController) { }

  pushSecondPage() {
    this.navCtrl.push('SecondPage');
  }
}

Instead, all the navigation calls go through the provider:

import { NavigationProvider } from './../../providers/navigation/navigation';
import { Component } from '@angular/core';
import { IonicPage } from 'ionic-angular';

@IonicPage()
@Component({
  selector: 'page-first',
  templateUrl: 'first.html'
})
export class FirstPage {

  constructor(private navigation: NavigationProvider) { }

  pushSecondPage() {
    this.navigation.pushSecondPage();
  }
}

This has several advantages:

  • String-based page names which cannot be checked at compile time are only used in one place.
  • Navigation parameters can be exposed as strongly typed arguments of the provider's navigation methods, again allowing compile-time checking.
  • The provider is easier to mock in unit tests. The calls to the navigation methods from the unit under test are also easier to verify.

Unfortunately, there's no way to inject a NavController into a provider. Attempting to do so will result in the following error:

Error: Uncaught (in promise): Error: StaticInjectorError[NavController]:
  StaticInjectorError[NavController]:
    NullInjectorError: No provider for NavController!

Before Ionic 3.5, using App.getRootNav() was a valid solution for this problem:

import { Injectable } from '@angular/core';
import { App } from 'ionic-angular';

@Injectable()
export class NavigationProvider {

  constructor(private app: App) { }

  pushSecondPage() {
    this.app.getRootNav().push('SecondPage');
  }
}

Since the method is now deprecated, it makes sense to find a different approach. Instead of relying on Ionic's helper methods, we can access the root navigation element directly where it is declared - in app.component.ts:

import { Component, ViewChild } from '@angular/core';
import { Nav } from 'ionic-angular';

@Component({
  templateUrl: 'app.html'
})
export class MyApp {

  @ViewChild(Nav) rootNav: Nav;

  // remaining code
}

Now, it's just the matter of making the navigation element available to our navigation provider. We could inject it during the application initialization phase:

import { Component, OnInit, ViewChild } from '@angular/core';
import { Nav } from 'ionic-angular';
import { NavigationProvider } from '../providers/navigation/navigation';

@Component({
  templateUrl: 'app.html'
})
export class MyApp implements OnInit {

  @ViewChild(Nav) rootNav: Nav;

  constructor(/*...*/ private navigation: NavigationProvider) {
    // existing constructor code
  }

  ngOnInit(): void {
    this.navigation.initRootNav(this.rootNav);
  }

  // remaining code
}

The navigation provider code would need to be only slightly modified:

import { Injectable } from '@angular/core';
import { NavController } from 'ionic-angular';

@Injectable()
export class NavigationProvider {

  private rootNav: NavController;

  initRootNav(rootNav: NavController) {
    this.rootNav = rootNav;
  }

  pushSecond() {
    this.rootNav.push('SecondPage');
  }
}

If you don't like the idea of having to initialize the provider before it's ready to use (although the above code should work reliably unless you forget to call initRootNav()), you could use Ionic's event system to pass navigation requests from the provider to app.component.ts:

import { Injectable } from '@angular/core';
import { Events } from 'ionic-angular/util/events';

@Injectable()
export class NavigationProvider {

  constructor(private events: Events) { }

  pushSecond() {
    this.events.publish('navigate', 'SecondPage');
  }
}

In app.component.ts you would then subscribe to these events and navigate accordingly:

import { Events } from 'ionic-angular/util/events';
import { Component, ViewChild } from '@angular/core';
import { Nav } from 'ionic-angular';

@Component({
  templateUrl: 'app.html'
})
export class MyApp {

  @ViewChild(Nav) rootNav: Nav;

  constructor(/*...*/ private events: Events) {
    // other constructor code

    this.events.subscribe('navigate', page => this.navigate(page));
  }

  private navigate(page: string) {
    this.rootNav.push(page);
  }

  // remaining code
}

This code can be further extended to support navigation parameters, but can become quite complex if you want to support other NavController's methods than push. That's why I prefer the first approach.

Copyright
Creative Commons License