Building a mobile nav

Design a single interface for your app's navigation using contextual components

Summary

Learn how to use contextual components to build an <app-header> component. This component will give developers a single interface for defining their navigation routes, and then those routes will automatically display on desktop as well as in a mobile-friendly nav menu.

We'll also discuss why composability is so important for designing components that work well with the rest of the Ember framework and ecosystem.


Final caller:

{{#application/app-header as |header|}}
  {{header.link-to 'Michael S' 'michael'}}
  {{header.link-to 'Jim' 'jim'}}
  {{header.link-to 'Pam' 'pam'}}

  {{#header.link-to 'dwight'}}
    Dwight
  {{/header.link-to}}

  {{#if media.isMobile}}
    {{header.link-to 'About us' 'index'}}
  {{/if}}
{{/application/app-header}}

Final <app-header> component:

<header>
  <nav class="container">
    {{#link-to 'index' class='logo'}}
      {{#if media.isMobile}}
        <img class='mobile-logo' src="https://shirt-happens.com/wp-content/uploads/2015/12/black-dunder-mifflin.png?x46096&x46096" alt="">
      {{else}}
        Dunder Mifflin
      {{/if}}
    {{/link-to}}

    {{#if media.isMobile}}

      <button {{action (mut isShowingNav) true}} class='mobile-nav__toggle'></button>

      {{#if isShowingNav}}
        {{#tether-dialog close=(action (mut isShowingNav) false)
          attachment='middle right'
          targetAttachment='middle right'}}
          <div {{action (mut isShowingNav) false}} class='mobile-nav__inner'>

            {{yield (hash
              link-to=(component 'link-to' class='mobile-nav__link')
            )}}

          </div>
        {{/tether-dialog}}
      {{/if}}

    {{else}}

      {{yield (hash
        link-to=(component 'link-to')
      )}}

    {{/if}}
  </nav>
</header>

Transcript

Let's learn how to make a mobile nav.

In this video I want to show you how to make a mobile navigation menu.

We're looking at a simple application here, we're on the home page and we can see some different routes in the header. If we open up the inspector and turn on the device emulator, we shrink the site down to mobile, we'll see that the nav items shrink collapse into this mobile nav. If we click it, we'll see this mobile nav menu appear. We can click on these links and navigate around the application.

So this is what we're going to be making today, and we want to do it in a way that the code we're using to define the routes won't be duplicated between the desktop and mobile versions of the header.

Alright, let's start with the base project, which currently just has the desktop navigation. We'll come over and start the server, and we can see that the app has some navigation items which let us navigate. But if we make the site smaller, they just kind of pile up on top of each other. And we want that nice collapsing nav that makes it easier to use on mobile.

So, the first thing we need is to be able to detect when our application is on a smaller screen. To do that, we're going to use an addon called ember-responsive. This addon will give us a service that we can use to detect what screen width our app is being displayed in. If we look at the instructions, we'll see that to install this we need to run ember install ember-responsive.

So we'll go back to our terminal, stop our server, and install the addon. We'll start the server again and go back to our app.

We'll see that installing ember-responsive gave us this breakpoints file, which comes with some predefined breakpoints that we can use in our templates. We see here we have one called mobile. Now the way we use ember-responsive is through a service called media. This let's us do something like this: if media.isMobile render this block, otherwise we'll render our normal desktop template.

Now if we save this and check out our app, we'll see on desktop we still have our normal navigation, but when we go down to mobile, everything hides. So this is a super easy way to customize your template based on the screen width of your app.

If we think back to our final demo, you'll remember that we always want the company logo showing in the left part of the header, regardless if we're on mobile or desktop.

Let's come back and move the index link above our media query. And now we see that it's always showing.

We actually have a logo we want to use on mobile, so let's switch this over to the block form of link-to, remove the label, and use media.isMobile again to customize the block. We'll add some css to give the logo a height of 80 pixels.

Now we have the logo taken care of, which always links back to the homepage. It's time to work on the links.

Eventually we want to use contextual components so that we only have to code our list of links one time. But first let's just get the mobile nav working on its own.

When we're on mobile, we want to show the hamburger menu button, which will open the mobile nav. So let's start here. We'll give this a class and style it. We're using flexbox so if we set margin-left to auto, it will pull to the right. We'll also bump the size a bit.

Ok, we have our button but its not doing anything yet. Clicking the button should show the mobile nav menu.

Let's add an action to the button that uses the mut helper to set a new variable called isShowingNav to true.

Now, when isShowingNav is true we'll render our mobile menu.

Ok, now for the actual menu.

Whenever I need something like a popover or modal window, my go-to library is ember-modal-dialog. It's maintained by Yapp labs so you know its reliable, and it makes it really easy to work with dialog windows.

Let's follow the installation instructions. We already have ember-cli-sass installed, so we'll install ember-modal-dialog now.

Next, we need to import some included SASS files. I usually import just the structure (so the positioning works), but leave out the appearance so I can style things myself.

Now there's one more thing to point out here. We can get some better positioning behavior for modal-dialog if we install another optional package called ember-tether. Let's go ahead and do that.

Ok, now we can start the server again and use the tether-dialog component in our template. We'll start with a simple tether-dialog for our mobile nav menu.

And there we can see it rendering. Let's add some styling. Let's give it a green background, we'll make it full-height, have it take up 80% of the width.

Great. Now, by default it's centered. We want it to tether to the right-hand side of the app. So we'll set our dialog's attachment point of middle right to the target's attachment point of middle right. (Note that by default, the target is body).

Next, let's style the overlay, which is added here by default. We'll make it dark and transparent.

Ok, we need a way to close our tether dialog. It comes with a close action that will fires whenever we tap on the overlay. Let's use it to mut isShowingNav back to false.

Awesome. One more thing, let's add some animation.

We want our dialog to slide in. If we inspect our dialog, we can see its left property is at 0px. We want it just off the screen - look at that, 80%, which makes sense because that's exactly how big our dialog is.

So, we'll create an animation called slideIn, and start with a left value of 80%, and go to 0%. Let's add it to our dialog and with a duration of 0.3s.

Looks good! Right now, the background just appears, so it'd be nice if it matched the duration of our slide animation. Let's make a fadeIn animation that animates opacity from 0 to 1, and add it to the overlay.

Great - much better.

Ok, now let's copy our links over to our dialog. And let's give them a class, and style them a bit.

Ok, we're getting there! We can see in the background that the links are actually working - but clicking a link doesn't dismiss the dialog. Let's add a wrapper with an action that sets isShowingNav back to false. We'll also add some padding.

Alright, things appear to be working!

Now, looking at our template we see some duplication of our links. This is a smaller app, so it might not seem like a problem. But bigger apps can have much more complex navigation logic. They have can sensitive links that depend on a user being authenticated or having a certain role, or just have lots of links through sub navigation menus.

So, what we want to do next is to abstract away all of these details into a new component, and use a contextual component to let us declare our app's navigation in a single place.

Everything within this <header> is related to mobile or desktop logic and navigation, so let's comment out the entire thing and come up with our new component's API.

Let's call it <app-header>, and we'll give it a block where the user of this component can customize it. For now, we'll make it so the user only passes in links, and we'll keep the rest of the header - the logo homepage button and the rest of the behavior - within our new component. Those things don't change as much as our nav links do, so its nice to hide those details within our component's implementation.

Since this header is really specific to our application template, let's nest it under application/. We'll create the app-header's template, and then copy over the code. Alright, things seem to be wired up correctly.

Now we know our header links will be passed in. Let's go ahead and comment out our links within the template. Ok, the nav is rendered but there aren't any links, as we expect.

Now let's come back to the application template. We need to decide what to call our contextual components.

What I like to do when creating new abstractions is, put myself in the shoes of the user of this component, and think about what interface would make sense if someone else had written this for me. Well, I'm an Ember developer, which means I've certainly heard of link-to, and I know its API. So why don't we start off with naming our contextual components header.link-to, and giving them exactly the same API as Ember's link-to.

So the first route label will be 'Michael S', and then the route name is 'michael'. We'll go ahead and copy the rest of our existing routes here.

Now suppose another developer joins your team a few months from now, and they need to add a route. They'd probably start by opening up the application template, and this is what they'll see. This seems like it would be pretty easy to change, and the nav routes and labels are the most likely things to change from our application's header. So this feels like a pretty good abstraction.

Alright, let's make this actually work. We'll go back to our component's template, and start with the desktop section.

Instead of typing link-to directly, we want to yield a contextual component called link-to. We'll type yield hash link-to, and use the component helper, and now we can choose any component we want. Well for our example, Ember's link-to is enough, so we'll use that. In some of my other projects, if the nav gets more complex I might make a separate component for the navigation item, something like a desktop-nav-item.

Let's save and check it out. Awesome, it works! So again, we're yielding out a contextual component from our app-header called link-to, so in the calling template here when a developer uses our app-header, they can call header.link-to and its as if they're using the underlying link-to component that we yielded. So Ember's link-to. But the contextual component lets us customize it a bit if we need, and gives us more control over how the links are finally displayed.

Ok let's do the mobile nav part. Remember we also used Ember's link-to on the mobile nav, so let's just copy the same yield block and put it in our mobile menu.

Ok, we see the links - but we forgot something. When they were in the mobile nav, we gave our links a class that we used for styling. Instead of making the caller do it, we can pre-wire our contextual component with this class, like this. Now the caller doesn't have to worry about it. This is why contextual components work so well here, we get to hide all the implementation details of the desktop menu vs the mobile menu within our application header.

Alright, our mobile nav is fully working again! And of course the desktop nav is too. Look at Dwight. Love that guy.

Now I just want to drive home a point on composability here. Looking at this list of links, we're really just asking the developer to provide us with some data. And you could imagine that instead of asking them to use our contextual components, we could just say, put a list of links on the controller, maybe as an array of objects, and pass that list in to our application-header, maybe as a linksList. Then we'd have those links within our <application-header>, and we could iterate over them in both the desktop and mobile case.

But now, what the developer using your app-header wants to customize the template for a link? Maybe they need to add an icon. How would they do it? Well they'd need to ask you to add some sort of API, where they can specify an optional icon they want to use for a link.

But look at our current app-header component. This is just a link-to - we already know how to customize that block. Let's change Dwight over to the block-form and, if we had font-awesome, we could put an icon right here. Let's say the square-face icon.

So you see contextual components work so well here, because they compose so well with other pieces of the Ember framework. What if we had logic around showing a particular nav item if the user were an admin? Well we could do that too. Let's say we had a currentUser property here, We could use if to check if they're an admin, and just add in another link to the admin panel.

Or we could add a route to an about page. And maybe we only want to add it on mobile. So we can compose nicely with our media service, and just render a new item on mobile. And this is all possible without us having to change anything inside of <app-header>. There's our new mobile link.

So composability is a crucial principle to understand when designing components, and making your components play nicely with other parts of the Ember framework and ecosystem.

Alright, hope you learned something, if you have feedback on this video or suggestions of what you'd like to see next, let us know!

👋 Hey there, Ember dev!

We hope you enjoyed this free video 🙂

If you like it and want to keep learning with us, we've written a free 6-lesson email course about the fundamental patterns of modern component design in Ember.

To get the first lesson now, enter your best email address below:

You can also check out more details about the course by clicking here.

Questions?

Send us a tweet:

Or ask us in Inside EmberMap, our private Slack workspace for subscribers.