Making a Menu Work in Ionic 4
Creating a menu in Ionic 4 should be simple and the process seems to be well documented but I struggled with it longer than I should have. Therefore, I decided to share my findings for my future self and for anyone else who might find this helpful.
Adding a Menu to an Existing Application
There are two key parts to adding a menu into an existing Ionic 4 application:
The menu markup in
app.component.html
:<ion-app> <ion-menu side="start" menuId="first"> <ion-header> <ion-toolbar color="primary"> <ion-title>Start Menu</ion-title> </ion-toolbar> </ion-header> <ion-content> <ion-list> <ion-item>Menu Item</ion-item> <ion-item>Menu Item</ion-item> <ion-item>Menu Item</ion-item> <ion-item>Menu Item</ion-item> <ion-item>Menu Item</ion-item> </ion-list> </ion-content> </ion-menu> <ion-router-outlet></ion-router-outlet> </ion-app>
The menu button in a page toolbar:
<ion-header> <ion-toolbar> <ion-buttons slot="start"> <ion-menu-button></ion-menu-button> </ion-buttons> <ion-title> Ionic Blank </ion-title> </ion-toolbar> </ion-header>
However, after running the application, there was no sign of the menu button. In the browser console, I noticed the following error:
Menu: must have a "content" element to listen for drag events on.
It seemed to be related to opening the menu using the swipe gesture, so I decided to ignore it temporarily and focus on getting the menu to open using the menu button.
Since the Ionic's built-in menu button didn't show up, I created my own button. Its click handler called MenuController::open
to open the menu imperatively. It didn't work either.
Looking at the MenuController
methods, getMenus
seemed useful to do some troubleshooting. It returned an empty array, so there wasn't much to troubleshoot.
I tried out a couple of more things but eventually ran out of ideas. As I got desperate enough, I started browsing the Ionic source code, hoping to get to the bottom of my issue. It took me a while, but in the end I figured out what was happening. All the relevant code was in the Menu::componentWillLoad
method. Based on the error message in the browser console, the initialization code was exiting prematurely, without even registering my menu in the MenuController
:
if (!content || !content.tagName) {
// requires content element
console.error(
'Menu: must have a "content" element to listen for drag events on.'
);
return;
}
// ... some code omitted
// register this menu with the app's menu controller
menuCtrl!._register(this);
Obviously, I shouldn't have ignored the error. But what do I need to do for the code to find the required content element? Here's the relevant part of the code:
const el = this.el;
const parent = el.parentNode as any;
const content =
this.contentId !== undefined
? document.getElementById(this.contentId)
: parent && parent.querySelector && parent.querySelector("[main]");
Unless contentId
is specified on the ion-menu
element, the code is looking for an element with the main
attribute. In the sample code, this attribute is set on the ion-router-outlet
element. However, there's no mention of it in the text.
If you use Ionic CLI to create a new project from the sidemenu
template, then the main
attribute is also set on the ion-router-outlet
element. But it isn't set if you use the blank
template. How is one supposed to know about this undocumented attribute?
Anyway, as soon as I added the main
attribute to my project, the issue was resolved.
<ion-router-outlet main></ion-router-outlet>
The menu button appeared, the error in the browser console was gone and the menu opened when I clicked the button or when I called MenuController::open
.
Moving the Menu to a Component
To better structure the code, you might want to move the menu in its own component. Especially if you have multiple menus, you don't want to have all its supporting code in app.component.ts
which has a tendency to get quite large even without all the menu code.
This is how the markup in app.component.html
could look like after you do that:
<ion-app>
<app-sidemenu></app-sidemenu>
<ion-router-outlet main></ion-router-outlet>
</ion-app>
Unfortunately, after that change the menu will stop working again: the menu button will be gone and the error in the browser console will reappear.
You might wonder why. Well, the reason is again in the following code:
const el = this.el;
const parent = el.parentNode as any;
const content =
this.contentId !== undefined
? document.getElementById(this.contentId)
: parent && parent.querySelector && parent.querySelector("[main]");
Because of the component root element (app-sidemenu
in my case), the ion-element
is nested one level deeper in the markup. Hence, the above code is looking for the main
attribute inside app-sidemenu
element instead of inside the ion-app
element. Of course, it can't find it.
To resolve the issue now, we need to set the menu's contentId
property. The documentation is rather terse about it (again):
The content's id the menu should use.
Although it implies that we should provide the id
of an ion-content
element, I (correctly) assumed that it should be an ion-router-outlet
element as was the case with the main
attribute.
So, the app.component.html
should actually look like this:
<ion-app>
<app-sidemenu></app-sidemenu>
<ion-router-outlet id="main"></ion-router-outlet>
</ion-app>
And the ion-menu
element in the component needs to have a matching value in its content-id
attribute:
<ion-menu side="start" menuId="first" content-id="main">
<ion-header>
<ion-toolbar color="primary">
<ion-title>Start Menu</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item>Menu Item</ion-item>
<ion-item>Menu Item</ion-item>
<ion-item>Menu Item</ion-item>
<ion-item>Menu Item</ion-item>
<ion-item>Menu Item</ion-item>
</ion-list>
</ion-content>
</ion-menu>
Of course, I could always make the value an input property of the component if necessary.
In any case, after the change, the menu starts working again. It's not that difficult to set it up once you know how. With better documentation, I wouldn't have spent almost an entire working day getting the menu to work in the application I was upgrading from Ionic 3.