Making a Menu Work in Ionic 4

September 6th 2019 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.

Copyright
Creative Commons License