Ionic 2/3 demo of a two-level side menu. The component currently supports only two levels of items. If more levels are needed, maybe using tabs layout for the other levels would be a better approach.
The component also supports two different modes: default and accordion.
Cli packages: (/usr/local/lib/node_modules)
@ionic/cli-utils : 1.9.2
ionic (Ionic CLI) : 3.9.2
Global packages:
Cordova CLI : 7.0.1
Local packages:
@ionic/app-scripts : 2.1.4
Cordova Platforms : android 6.1.0 ios 4.4.0
Ionic Framework : ionic-angular 3.6.0
You can take a look at the demo online in the following Stackblitz project:
https://multi-level-side-menu.stackblitz.io
Inside of the project folder, run npm install and then to run the demo in the browser ionic serve [-t android/ios]
Just copy the side-menu-content folder (inculding the html, ts and scss files) in your project. Then include the SideMenuContentComponent in the declarations array from your @NgModule.
Header options and sub menu items should have the following format:
// MenuOptionModel interface
export interface MenuOptionModel {
// If the option has sub items and the iconName is null,
// the default icon will be 'ios-arrow-down'.
iconName?: string;
// The name to display in the menu
displayName: string;
// Target component (or null if it's a "special option")
component?: any;
// Here you can pass whatever you need. That way you can
// handle login/logout options, changing the language, and so on...
custom?: any;
// Set if this option is selected by default
selected?: boolean;
// List of sub items if any
subItems?: Array<MenuOptionModel>;
}
So an item with nested sub items would look like this:
let menuOption: MenuOptionModel = {
displayName: 'Option Name',
subItems: [
{
// With icon
iconName: 'ios-basket',
displayName: 'Sub Option 1',
component: SomePage
},
{
// Without icon
displayName: 'Sub Option 2',
component: SomeOtherPage
},
{
// Special option with icon
iconName: 'log-in',
displayName: 'Login',
custom: {
isLogin: true
}
},
{
// Another special option but without icon
displayName: 'Spanish',
custom: {
shouldChangeLanguage: true,
targetLanguage: 'ES'
}
}
]
};
When an option is selected, the MenuOptionModel object is returned to the caller by the selectOption event. The MenuOptionModel returned object can then be used to check if we need to push/set as root a new page, or if we need to handle that option as a special option.
<side-menu-content [options]="options" (selectOption)="selectOption($event)"></side-menu-content>
And then in the App component code:
@Component({
templateUrl: 'app.html'
})
export class MyApp {
// ...
public selectOption(option: MenuOptionModel): void {
if (option.custom && option.custom.isLogin) {
// Handle the login...
} else if (option.custom && option.custom.isLogout) {
// Handle the logout...
} else if(option.component) {
// Push or set as root the option.component page
}
}
}
The component also defines the SideMenuSettings interface, to customize the behaviour of the component.
// SideMenuSettings interface
export interface SideMenuSettings {
accordionMode?: boolean;
itemHeight?: {
ios?: number,
md?: number,
wp?: number
};
arrowIcon?: string;
showSelectedOption?: boolean;
selectedOptionClass?: string;
indentSubOptionsWithoutIcons?: boolean;
subOptionIndentation?: {
ios?: string,
md?: string,
wp?: string
};
}
The settings should be send to the component using the settings property:
@Component({
templateUrl: 'app.html'
})
export class MyApp {
//...
// Settings for the SideMenuComponent
public sideMenuSettings: SideMenuSettings = {
accordionMode: true,
showSelectedOption: true,
selectedOptionClass: 'my-selected-option'
};
// ...
}
And in the view:
<side-menu-content [settings]="sideMenuSettings" [options]="options" (selectOption)="selectOption($event)"></side-menu-content>
| Param | Description | Default |
|---|---|---|
accordionMode |
Collapses any opened option when a new option is expanded. | false |
itemHeight |
The height of the items is used to animate the side menu changes when hidding and showing the sub menu items. You can set a custom height for the items for each mode. | 50 for all md, ios and wp |
arrowIcon |
The Ionic icon name to be used as the arrow in the header options | ios-arrow-down |
showSelectedOption |
If the selected option should be highlighted (if it is a sub item, its parent will also be highlighted). If set to true and the selected option is a sub item, its parent will be always shown expanded when opening the side menu |
false |
selectedOptionClass |
Name of the class to be added to the selected option. Only used when showSelectedOption is true |
selected-option |
indentSubOptionsWithoutIcons |
Vertically aligns the sub options without icons to the parent option | false |
subOptionIndentation |
The padding-left of the sub options, so you can indent them if needed |
'16px' for all md, ios and wp |
The component also exposes a collapseAllOptions() method to reset the state of the options when needed (after selecting an option, or when closing the side menu for example):
@Component({
templateUrl: 'app.html'
})
export class MyApp {
// Get the instance to call the public methods
@ViewChild(SideMenuContentComponent) sideMenu: SideMenuContentComponent;
// ...
public selectOption(option: MenuOptionModel): void {
this.menuCtrl.close().then(() => {
// Collapse all the options
this.sideMenu.collapseAllOptions();
// ...
});
}
}
If you set the showSelectedOption setting to true, and try to navigate to a given page using a button on the content on the page instead of clicking on that option from the side menu, that page won't be shown as selected in the menu. In order to avoid this, the component also exposes an event and its payload:
// SideMenuRedirectEvent constant
export const SideMenuRedirectEvent: string = 'sidemenu:redirect';
// SideMenuRedirectEventData interface
export interface SideMenuRedirectEventData {
displayName?: string;
}
So in your page, you just need to publish that event using Ionic Events:
import { SideMenuRedirectEvent, SideMenuRedirectEventData } from './../../shared/side-menu-content/side-menu-content.component';
// ...
public goToSubOption(): void {
// Since we're redirecting to a page without clicking the option from the
// side menu, we need to use events to tell the side menu component
// which option should be marked as selected.
let redirectData: SideMenuRedirectEventData = {
displayName: 'Sub Option 2'
};
// Send the event to the side menu component
this.eventCtrl.publish(SideMenuRedirectEvent, redirectData);
// Now we can set that page as root
this.navCtrl.setRoot(DetailsPage, { title: 'Sub Option 2' });
}
30/08/2017: Added showSelectedOption and selectedOptionClass to highlight the currently selected option. Added custom subproperty in the MenuOptionModel model to allow the user to add any custom property on the items.
16/09/2017: Added indentSubOptionsWithoutIcons and subOptionIndentation settings to allow the user to customize the indentation of the sub items. Improved inner logic so now if the showSelectedOption setting is true and the currently selected option is a sub item, its parent will be shown expanded when opening the side menu.
- Convert this demo into an npm package
- Add unit testing
- Add e2e testing
- Having an issue or looking for support? Open an issue and I'll do my best to help you out.
- Got a new feature or a bug fix? Fork the repo, make your changes, and submit a pull request.
If you find this project useful, please star the repo to let people know that it's reliable. Also, share it with friends and colleagues that might find this useful as well. Thank you! :)

