Hiding tabs dynamically in Ionic 4
Tabs are the centerpiece of most Ionic apps, but sometimes you need to hide them quickly and easily. To do so correctly is a bit of setup at first, but once you do it, you’ll thank yourself, and me for convincing you to do so.
Ionic 4 has come with a number of changes. The biggest change, in my opinion, is how we utilize Tabs and Navigation. In ionic 3 we could add an attribute to certain tabs called tabsHideOnSubPages
which allowed us to do exactly that — hide the tab bar on subpages of that tab. Unfortunately, that has not been implemented for Ionic 4 as of yet, though it has been opened as a feature request.
So what should we do for Ionic 4?
Ionic 4 uses Angular’s routing system, instead of the Ionic 3 method of navController
. We’ll be leveraging this change, to allow us to pragmatically hide the tab bar on pages of our choice while leaving it visible everywhere else.
Let’s get started
To start, we’ll need to generate a new global service provider. This is done for us by default in Ionic 4, through the use of the providedIn: 'root'
attribute.
We’ll create the service provider by running the command ionic g service core/core
. After the command finishes, you’ll notice a new folder in your src/app/
folder, with our new service provider, called core.service.ts
.
Next, we’ll need to import the service into our app.modules.ts
provider array, as well as importing it into our app.component.ts
file, in our constructor.
app.module.ts
@NgModule({
declarations: [AppComponent],
entryComponents: [],
imports: [
...
],
providers: [
CoreService
...
],
bootstrap: [AppComponent],
})
export class AppModule {
}
app.component.ts
@Component({
selector: 'app-root',
templateUrl: 'app.component.html',
})
export class AppComponent {
constructor(public core: CoreService) {
...
}
...
}
Configure the CoreService provider
When you first open up the new service provider, you should see the default template below.
import { Injectable } from '@angular/core';@Injectable({
providedIn: 'root'
})
export class CoreService { constructor() { }
}
Next, we’ll create a new private variable called hideTabBarPages
. This is a string array, that we’ll use to tell Ionic what pages to hide our tab bar on. As you can see, I want to hide the tab bar on a page called new-group
. But you can change this to include whatever you want.
private hideTabBarPages: string[] = [
'new-group',
];
Next, in your constructor, you should import the Router
and the Platform
providers like so:
constructor(private router: Router, private platform: Platform) {
this.platform.ready().then(() => {
this.navEvents();
});
}
All this does is wait for the platform to be ready before we do anything, as we can’t hide tabs that aren’t created yet, now can we?
Now we’ll go ahead and hook up the above function called NavEvents
, which will handle our navigation events for us. It’s a pretty simple function, and really only a container for the router.events
subscription.
this.router.events
.pipe(filter((e) => e instanceof NavigationEnd))
.subscribe((e: any) => {
this.showHideTabs(e);
});
All we’re doing is subscribing to the router.events
Observable, filtering out the events so that we only get the NavigationEnd
events, and then run another function with the event data. The reason we’re filtering out certain events is that Angular triggers a number of lifecycle events, whenever we navigate to a page. If we didn’t do this, the inner function this.showHideTabs(e)
would be called numerous times with every page change, which isn’t ideal.
Now we should create our function showHideTabs()
. This is what will handle whether the tabs should be shown, or hidden depending on the page we navigated to.
private showHidetabs(e: NavigationEnd) {
...
}
The e: NavigationEnd
event will have the following information in it. This will be different depending on the page that you’re navigating to.
{
id: 13
url: "/tabs/groups/new-group?type=msg"
urlAfterRedirects: "/tabs/groups/new-group?type=msg"
}
First, we need to get the event URL in a format that we can use.
const urlArray = e.url.split('/');
// Result: urlArray: ["", "tabs", "groups", "new-group?type=group"]
Next, we’ll grab the last page URL in the path.
// Grab the last page url.
const pageUrl = urlArray[urlArray.length - 1];
// Result: new-group?type=group
Since we’re left with new-group?type=group
, we need to remove the parameters, since we only want the page name. The parameters that I’m passing are shown after the ?
character, as ?type=group
. You should include this regardless, as it future proof’s your code.
// Remove the parameters from the URL.
const page = pageUrl.split('?')[0];
// Result: new-group
One of the last things we need to do in this function is to check if we should hide this particular page or not.
// Check if we should hide or show tabs.
const shouldHide = this.hideTabBarPages.indexOf(page) > -1;
// Result: true
Lastly, we’ll need to actually hide the tabs if, or show them if they’ve been previously hidden.
shouldHide ? this.hideTabs() : this.showTabs()
The logic is mostly explained in the comments, but if you’re confused about the line shouldHide ? this.hideTabs() : this.showTabs()
, this is called a ternary operator, which is a fancy way of writing an if / else
statement.
Basically, it says that if ShouldHide
is true, then run this.hideTabs()
if shouldHide
is false run this.showTabs()
.
Smoother Transitions (optional, recommended):
If you want to hide the tabs during the transition between pages, you can make a simple setTimeout
, that will wait 300ms before running the above.
// Not ideal to set the timeout, but I haven't figured out a better method to wait until the page is in transition...
try {
setTimeout(() => shouldHide ? this.hideTabs() : this.showTabs(), 300);
} catch (err) {}
The try / catch
statement is just for any weird errors that pop up. Sometimes the tabs aren’t initialized yet, even after we check if the platform is ready in the constructor
. So this will prevent our application from throwing out an error, unnecessarily.
Handling Pages with Route Parameters (Optional, recommended):
Route parameters i.e:('product-details/:id')
will not automatically work with our current implementation. Here is how you can go about handling that. Now, this assumes that you are using Angular best practices, and are only passing route-parameters that are numbers, and not anything odd.
product-details/5
is a good example of what we’re looking for.
Create a new public variable calledrouteParamPages
, it’s a string array, same as our previous one hideTabBarPages
. This will house the pages that have route-parameters, such as 'product-details/:id'
where product-details
is the parent page and :id
is the route parameter that you are passing to it. It’ll end up looking something like this:
routeParamPages: string[] = [
'product-details',
];
Now in our ShowHideTabs
function, we need to add a few extra things. The first is a new variable called pageUrlParent
.
This is similar to the pageUrl
variable, but instead of grabbing the page we’re navigating to from the urlArray
, we’re going to grab the parent of that page (one level up). So in my example above, product-details
is the parent, and :id
is the child. It’s the same page technically, but naming conventions are hard sometimes, and I wanted it to be somewhat clear what we were doing.
Add pageUrlParent
somewhere above shouldHideTabs
.
const pageUrlParent = urlArray[urlArray.length - 2];
Next, we’ll add in a check to see if the parentPage
is in our routeParamPages
array. This will tell us if the page in question is a routeParamPage
, and if it needs to be hidden. Add this in right above shouldHideTabs
// handle param pages
const hideParamPage = this.routeParamPages.indexOf(pageUrlParent) > -1 && !isNaN(Number(page));
If you want to know what’s happening here, we’re first checking if he pageUrlParent
is in our routeParamPages
array. This would be product-details
in my example and would evaluate as being true.
And then we are converting the page
variable to a Number. You could use parseInt, but it has a lot of odd edge-cases that I didn’t like, so I decided to use Number(page)
instead. If the page we are navigating to has any non-numeric characters, this will evaluate to Nan
which is Not A Number. Lastly, we are checking that the converted Number(page)
is not Nan
through !isNan(Number(page))
. Then we just have to tack on an or statement to our shouldHide
variable and we’re back in business!
const shouldHideTabs = this.hideTabBarPages.indexOf(page) > -1 || hideParamPage;
Create the hideTabs() & showTabs() functions.
Here I have set these methods as public so we can access them outside of the service if necessary, but for this example, it does not affect anything. You can set them to private if you wish, but I recommend keeping them as public.
public hideTabs() {
const tabBar = document.getElementById('myTabBar');
if (tabBar.style.display !== 'none') tabBar.style.display = 'none';
}public showTabs() {
const tabBar = document.getElementById('myTabBar');
if (tabBar.style.display !== 'flex') tabBar.style.display = 'flex';
}
I am also checking if the tab bar has a certain display value before changing it. flex
for if we’re going to show the tabs and none
if we’re going to hide them. This just prevents us from doing an unnecessary operation and possibly causing the tabs to flicker.
Lastly
You’ll need to add an ID to your tabBar element in your tabs.page.html
. In the above example, the ID I included is called myTabBar
, but you can name it whatever you want.
<ion-tab-bar #myTabBar id="myTabBar" slot="bottom">
That’s it!
There you have it. Save the changes we made, and test out, and you’ll see that your tabs automatically hide when you navigate to a page in the hideTabBarPages
array.
Here is the full service, if you just want to copy-paste it, we’ve all done it before ;-)
import { Injectable } from '@angular/core';
import { filter } from 'rxjs/operators';
import { NavigationEnd, Router } from '@angular/router';
import { Platform } from '@ionic/angular';@Injectable({
providedIn: 'root',
})
export class CoreService { hideTabBarPages = [
'new-group',
];routeParamPages: string[] = [
'product-details',
];constructor(private router: Router, private platform: Platform) {
this.platform.ready().then(() => {
console.log('Core service init');
this.navEvents();
});
} public hideTabs() {
const tabBar = document.getElementById('myTabBar');
if (tabBar.style.display !== 'none') tabBar.style.display = 'none';
} public showTabs() {
const tabBar = document.getElementById('myTabBar');
if (tabBar.style.display !== 'flex') tabBar.style.display = 'flex';
} // A simple subscription that tells us what page we're currently navigating to.
private navEvents() {
this.router.events.pipe(filter(e => e instanceof NavigationEnd)).subscribe((e: any) => {
console.log(e);
this.showHideTabs(e);
});
} private showHideTabs(e: any) {
// Result: e.url: "/tabs/groups/new-group?type=group" // Split the URL up into an array.
const urlArray = e.url.split('/');
// Result: urlArray: ["", "tabs", "groups", "new-group?type=group"]// Grab the parentUrl
const pageUrlParent = urlArray[urlArray.length - 2];// Grab the last page url.
const pageUrl = urlArray[urlArray.length - 1];
// Result: new-group?type=group const page = pageUrl.split('?')[0];
// Result: new-group// Check if it's a routeParamPage that we need to hide on
const hideParamPage = this.routeParamPages.indexOf(pageUrlParent) > -1 && !isNaN(Number(page));// Check if we should hide or show tabs.
const shouldHide = this.hideTabBarPages.indexOf(page) > -1 || hideParamPage;
// Result: true // Not ideal to set the timeout, but I haven't figured out a better method to wait until the page is in transition...
try {
setTimeout(() => shouldHide ? this.hideTabs() : this.showTabs(), 300);
} catch (err) {
}
}
}
Questions?
You can find me on:
- GitHub: https://github.com/bengejd/
- Medium: https://medium.com/@JordanBenge
Who am I? My name is Jordan Benge, I am a Software Developer who loves helping others and contributing to Open-Source. I’ve been working in the Ionic Framework since Ionic 1, and have tried to keep up to date on the latest and greatest when it comes to Hybrid Mobile App Development.