Best Pratice: Sidebar / Menu Navigation with Badges


#1

Just looking for pointers, as neither Google nor this forum returned any results.

I’m looking to implement (phone app) a sidebar / menu driven page navigation with highlighted list items (indicating the screen the user is on) and badges.

Badges can be changed from any other component / page that’s part of the app.

How to best implement this?

I was thinking a long the lines of introducing a NAV class that keeps track of current page & badge stati.
Or alternatively an app.component located pageNav = {} data structure that drives the UI control, including badge states, and is also controlled via a global EventEmitter Service.

TL;DR
It’s EventEmitter vs CLASS.

Any thoughts?
Performance implications?


#2

Hi @_oliver. I think your question is best answered split into two:

  • What is the best way to handle a CSS equivalent for mobiles of :active
  • How to then implement this within MenuController/NavController

Ionic 2 has a SASS variable for :active here:

The three variables I’ve found work for me are:
$action-sheet-ios-button-background-activated
$action-sheet-md-button-background-activated
$action-sheet-wp-button-background-activated

I wish there was a single SASS variable that could set all three, but looks like you have to do it separately unless you make your own.

Regarding implementation in your MenuController/NavController, I’m not sure what the best practice is. The way I have it set up is a small custom function checking which page is open and then equating this to my custom function. I then have a class applied to my menu button if the open page evaluates to the menu item selected.


#3

Sorry, I probably wasn’t too clear, my bad.

The ‘:active’ / styling part is easy.

I’m more trying to understand…

*1 how to best (in a single place) keep track of what page I’m & how to wire this best (variable or function) to the sidebar nav control
-> I don’t want to have to keep track of each page access via it’s ionic lifecycle event

*2 From anywhere in the app I need to be able to inc or decrement a pages badge number, shown in the nav control
-> So either this is part of a centralized pageTracking class with or without Emitter Services etc.

There is always a way I could get it to work, just wondering about best practice.
Especially in light of performance.


#4

Thanks for clarifying. I see now what your dilemma is.

I’m not sure there is a ‘best practice’ for something like this. I suppose you could say there is a better way to do it in terms of the memory stack, ie. to limit the number of function calls by declaring only when and where you need it. Probably using an Emitter Service is a better idea than hand-crafting a function that runs globally. But then this depends on the needs of your project and you may need to access your class in every page (for instance, if every page is available from your nav).

I don’t think I’m experienced enough to answer that really so hopefully someone else will chip in with a better understanding.

Sorry, that’s all I can offer.


#5

Ok, I gave it try myself.

Again, not sure if this is a best practice approach, but perhaps a start. At a minimum I hope to trigger some good discussion. With that said, it was probably just fair to attempt it myself first.

In the CLASS part you see that I’m directly accessing the SHARED pages data object, one of the questions:

Q1 Does direct access limit me in any way (later on)?
Q2 Would a Event/Subscribe model be more useful / appropriate?

HTML (part of sidebar template)

<ion-list>
    <ion-list-header>Nav Menu</ion-list-header>
    <button menuClose ion-item *ngFor="let page of pages" (click)="openPage(page)" [trackNav]="page">
        <ion-icon name="{{page.icon}}" item-left></ion-icon>
        <ion-label fixed>{{page.title}}</ion-label>
        <ion-badge item-right color="secondary" [hidden]="page.badge < 1">{{page.badge}}</ion-badge>
    </button>
</ion-list>

DIRECTIVE (trackNav)

@Directive({ selector: '[trackNav]' })
export class NavActiveDirective {

	@Input('trackNav') targetPage;

	constructor(
		private _appCtrl: App,
		private _element: ElementRef,
		private _renderer: Renderer
	) {
		// Nothing to do yet
	}

	ngOnInit() {
		this._appCtrl.getRootNav().viewDidEnter.subscribe(
			(view) => {
				this.checkActive(view);
			}
		)
	}

	ngAfterViewInit() {
		this.checkActive(null);
	}

	checkActive(view) {
		let active = (view && view.component === this.targetPage.component)?true:false; // isActive link
		this._renderer.setElementClass(this._element.nativeElement, 'nav-link-active', active);
	}
}

CLASS (sidebar controller)

@Component({
	selector: 'left-sidebar',
	templateUrl: 'left-sidebar.html'
})
export class LeftSidebarComponent {

	public pages: Array<IPageNav>;

	constructor(
		private _appCtrl: App,
		public menuNavCtrl: MenuNavService,
	) {
		this.pages = this.menuNavCtrl.pages;
		// ^^^ debatable if direct access or a subscription model is more useful
	}

	public openPage(page) {
		if (page && page.component != this._appCtrl.getRootNav().getActive().component) 
			this._appCtrl.getRootNav().setRoot(page.component);
	}
}

SHARED SERVICE (nav tracking & badge ctrl class)

export interface IPageNav {
    title: string;
    component: any;
    badge: number;
    icon: string;
}

@Injectable()
export class MenuNavService {

    public pages: Array<IPageNav>;

    constructor( @Inject(DemoDataService) private _ddService: IDemoDataService) {
        // 
    }

    public fetchPageObject(pageTitle: string): any {
        for (var i = this.pages.length - 1; i >= 0; i--) {
            if (this.pages[i].title === pageTitle)
                return this.pages[i];
        }
        return null;
    }

    public incrementBadge(pageTitle: string): void {
        let pageObject = this.fetchPageObject(pageTitle);
        pageObject && pageObject.badge++;
    }

    public decrementBadge(pageTitle: string): void {
        let pageObject = this.fetchPageObject(pageTitle);
        pageObject && pageObject.badge--;

        if (pageObject && pageObject.badge < 0)
            pageObject.badge = 0;
    }

    public resetBadge(pageTitle: string): void {
        let pageObject = this.fetchPageObject(pageTitle);
        pageObject && (pageObject.badge = 0);
    }

    public getAllPages(): Array<IPageNav> {
        return this.pages;
    }

    public setPages(pages: Array<IPageNav>): void {
        this.pages = pages;
    }
}

EDIT: here is the initial PAGES object init, in app.components:

this.menuNavCtrl.setPages([
    { title: 'Page One', component: Page1Page, badge: 0, icon: 'some-icon' },
    { title: 'Page Two', component: Page2Page, badge: 1, icon: 'some-other-icon' }
]);

#6

No opinions, thoughts, suggestions?