Best practice for a dynamic menu bar


#1

I’m getting ready to start building an app in ember that will function like a mobile phone app (that will be where it is intended to be used) and this app will have a navigation bar at the bottom of the screen like many apps do (or did, I know patterns are getting away from that, that’s beside point) and this navigation bar will have a variable number of items in it, and each set of items will be different depending on what route/page/view you are currently on.

As a quick for instance, on the main page this nav bar may have 4 items it in, but when you click on on of them to go to a products page, it would only have three, and those three might be totally different items or a subset of the items that were there before it.

I intend to make this a component, but in my mind I was just going to wrap each of these in an if statement, and maybe check against a computed property that is dependent on if a certain set of routes is currently loaded and go about it that way.

My other thought was so define an Ember object possibly on each route that defines what navigation items that route should be showing, those would be passed in and the component would use that object to render the navigation menu template using that object as it’s information. I feel like this would allow me to not have to have a bunch of if statements in the template since the template won’t care about what items it receives it will just render what it’s handed on each route. This would break up the items being rendered on a per route basis so editing the names of menu items or icons for the menu items means I would have to open each route file that uses that menu item to edit it.

The final thought is something like how ember-liquid-fire handles animations, in that they are defined in a single transition file that all the routes call to get info about how they are to animate when transitioning. However here I could maintain a list of menu items with the info that goes with them, and then have a map of sorts that tells each route which ones to get. I suppose this could be combined with the second option to say there is an Ember object file that I import into the route file, and I extend from that object the menu items that I want for that route. That way I can still go and edit the menu items in the one object file and they get updated everywhere, but would only have to open the route file that I want to add or remove a menu item from without touching any other files.

But is there a best practice suited for achieving this?


#2

Could an initializer be a good option for this? I can setup an a json object in the initializer that outlines the name, route, and icon info for each menu item that will be, and then inject that object into all the routes, from there each route can just define a new menu property that pulls certain menu items out of that object and then pass those on to the component.


#3

I think that you have a few solid ideas for how this might work. I don’t think there’s a “this is the way you do it” solution for this, but here’s a few patterns that might work.

One pattern might be to use the renderTemplate hook and named routes. (Note: the creator of Liquid Fire recently discussed why he does not think this is good pattern). But, I think this is a good use case for it.

First, create an outlet for the navigation menu.

// application/template.hbs
{{outlet}}
{{outlet 'nav-menu'}}

Then, for each of your routes that has a distinct navigation menu, create a template for it.

// route-alpha/template.hbs
<h1>Route Alpha Content!</h1>

// route-alpha-nav/template.hbs
<ul><!-- Route Alpha Nav --></ul>

Lastly, in your route, render that template into the outlet in the application view (this works no matter how nested you are in your routes).

// routeA/route.js
import Ember from 'ember';

export default Ember.Route.extend({
  renderTemplate(controller, model) {
    // render the standard template
    this._super(controller, model); 
    // Render the nav into the application template
    this.render('route-alpha-nav', {
      into: 'application',
      outlet: 'nav-menu'
    });
  }
});

A more “Ember 2.0” pattern might be to create a component instead of that named outlet and a service that manages the state of the navigation menu. This is close to what you mentioned, but there shouldn’t be a need to use an initializer.

// application/template.hbs
{{outlet}}
{{navigation-menu}}
// navigation/service.js
import Ember from 'ember';
export default Ember.Service.extend({
  visibleOptions: []
});
// navigation-menu/component.js
import Ember from 'ember';
export default Ember.Component.extend({
  navigationService: Ember.inject.service('navigation')
});
// navigation-menu/template.hbs
<ul>
  {{#each navigationService.visibleOptions as |option|}}
    <li>{{option}}</li>
  {{/each}}
</ul>
// routeA/route.js
import Ember from 'ember';
export default Ember.Route.extend({
  navigationService: Ember.inject.service('navigation'),
  activate() {
    this.get('navigationService').set('visibleOptions', ['Button A', 'Button B']);
  }
});

I’m inclined to like the first option more because you get to keep the state of the navigation bar in templates, rather than managing some list of options within the routes themselves. But, there could be a reason that’s a better choice.

Anyway, hope this helps!


#4

I really like the services example you gave, it makes a lot of sense and I think I like the way that one is going.

That being said is there some way you would recommend keeping these menu items somewhere else and just referencing them in the route file?

You have shown in the route setting the visibleOptions to an array or I could even do an array of hashes, but a good example of where this can be a small source of headache is when we decide the login icon should now be a mustache, so now I have to open up each route file, and change the icon reference for the login menu item to the mustache icon class.

Compared to if I just had the list of possible menu items and their icons stored somewhere else, and just referenced the ones I wanted in the route, that way if I do want to go update the icon for a menu item, I do it in one place as apposed to possibly many places where the menu item is being set and passed in.

Thoughts?


#5

Well, I think if you go the route of the additional templates (not the service approach), you still would be well-suited to create a set of components for each of the different buttons, so then you can mix-and-match as appropriate.

So, for example:

// route-alpha-nav/template.hbs
<ul>
  {{login-button}}
  {{logout-button}}
  {{like-button}}
</ul>
// route-beta-nav/template.hbs
<ul>
  {{login-button}}
  {{share-button}}
  {{profile-button}}
</ul>

…etc.

If you go the service route, you might do something similar — but use the component names as part of the array. So,

// route-alpha/route.js
import Ember from 'ember';
export default Ember.Route.extend({
  navigationService: Ember.inject.service('navigation'),
  activate() {
    this.get('navigationService').set('visibleOptions', ['login-button', 'share-button', 'profile-button']);
  }
});

And:

// navigation-menu/template.hbs
<ul>
  {{#each navigationService.visibleOptions as |option|}}
    <li>{{component option}}</li>
  {{/each}}
</ul>

So — either scenario, it’s still possible to make the individual buttons isolated.


#6

Given this, I’m almost more inclined to like the second option better now!


#7

So here you’re suggesting to make each button a separate component? Am I understanding that right?


#8

That is correct! (and I am making this sentence longer as this has to be at least 20 characters)


#9

That wouldn’t be considered an anti pattern? Given I know there isn’t a defacto standard for all design patters but it seems like that’s a lot, I don’t imagine I’ll have that many buttons anyway. But then again maybe it’s not so bad, as the display logic is left up to each button itself, therefore allowing each button to be different sizes, have two icons instead of one etc.

I think I like this approach. Can I save components inside extra folders? I like having things organized :grin:


#10

Yes! Absolutely! (Thank god for that…because I need some organization too).

buttons/nav-buttons/login/component.js

would be invoked as

{{buttons/nav-buttons/login}}

#11

Nice, thanks for the input! I appreciate it!


#12

I’m working on adding some more complexity to this navigation approach (yaaaaaaay :expressionless: )

So instead of an array of strings representing components I want to render, I want to pass in an array of objects with some different values that will be used to construct the navigation buttons

activate() {
		this.get('navigationService').set('navigationMenuItems', [{
			"button": 'nav-buttons/user-button',
			"width": 60
		},{
			"button": 'nav-buttons/items-button',
			"width": 60
		},{
			"button": 'nav-buttons/my-items-button',
			"width": 60
		},{
			"button": 'nav-buttons/event-button',
			"width": 60
		},{
			"button": 'nav-buttons/ticket-button',
			"width": 60
		},{
			"button": 'nav-buttons/cart-button',
			"width": 60
		},{
			"button": 'nav-buttons/donate-button',
			"width": 60
		}]);
	},

In the template however when trying to access the properties I can’t seem to get at those values, I get in the console

Uncaught TypeError: key.match is not a function

In my template I’m attempting to access it like so

{{#each navigationService.navigationMenuItems as |menuItem|}}
		{{menuItem.button}}
  {{/each}}

Any thoughts as you why this is happening?


#13

If you go for the service approach, I would typically try to hide complexity on the right place…

  • In the service: communication between your app’s route and the component, also behaviour of the navigation.
  • In the component (that uses the service): how to render the stuff… icons to use, width etc…
  • In the route: which navigation to use and to which routes actions in the component must go…

That way you can swap the component by different implementations and reuse the navigation service.

In my case I have a side-bar and an upper nav-bar. So I will try to make 2 components and 1 navigation service with 2 levels. Because the buttons on the upper nav-bar must change for each item on the sidebar.

What I also like about the service approach is that it is easier to implement stuff like for example badge counters.

Also when your menu items change, you have to do it only in your service and not across 20 different routes in order to put the appropriate buttons on the screen.