Generate URLs using the Ember router service

27 July 2017

For some inexplicable reason, I've always felt really excited about having a proper router service added to Ember.js, ever since the RFC was first fleshed out, in September of 2015.

Then a few weeks ago, in the release blog post for Ember 2.14 I saw that the (phase 1) implementation of said router service would be included in the next release, 2.15. Above that, I also learned that there is already a polyfill so that you can start using the router service today.

Right, but what is this router service you rave about?

I'm glad you asked :) The router service sits on top of Ember's router, which is responsible for "recognizing" routes from the URL, changing the path according to which route the app is on, transitioning between routes and calling the different hooks and actions for each route. In short, everything that is related to routing, although that'd be quite a tautological definition.

Ember developers mostly interact with the router through instructing it to transition to a new route (by calling this.transition from a route, or using the {{link-to}} helper from a template) and by using visit from acceptance tests.

These are, however, special API methods and there is no coherent way to use these services through a ... service, like you can do for example with Ember Data's store or the session service in authentication add-ons.

The router service fills this hole.

Generating paths for a route name

Today, there is no easy way to programmatically get the generated path for a given route name. Imagine you have the following routes:

1Router.map(function() {
2  this.route('bands', function() {
3    this.route('band', { path: ':id' }, function() {
4      this.route('songs');
5      this.route('details');
6    });
7  });
8});

, and you want to see what path is generated for the bands.band route for a band that has the id 24 (my lucky number, if I have to give you one). There is no easy way to accomplish that - that is, without using the router service.

Sure, but why would you need that? - you might ask. You can just use {{link-to 'bands.band' band}} or this.transitionTo('bands.band', band) and the router will do the right thing. True, but what if I want to share the URL for an answer to a question (imagine Quora or Stack Overflow) without going to the answer's page and copy-pasting the URL from the address bar manually?

That's just one of the possible features where you might need this but it's the one I came across and want to show you.

Sharing URLs to band pages

Being a big fan of (hard) rock and having an app that has bands (instead of answers), the example I'm going to use is sharing URLs to the page of a band. We want it look something like this:

Share URLs for each band

As we currently don't yet have the router service baked into the stable Ember version, we'll use the aforementioned polyfill. Let's install it first:

1$ ember install ember-router-service-polyfill

We now have access to the router service, so let's open the controller of the bands route which is where the list gets rendered and inject the servic , like any other one:

 1// app/controllers/bands.js
 2import Ember from 'ember';
 3
 4const { inject } = Ember;
 5
 6export default Ember.Controller.extend({
 7  (...)
 8
 9  router: inject.service()
10});

We have to put the share button alongside each list item and trigger an action that generates and copies the URL. Let's call the action shareBandURL:

 1<!-- app/templates/bands.hbs -->
 2<div class="col-md-4">
 3  (...)
 4    {{#each model as |band|}}
 5      {{#link-to "bands.band" band class="list-group-item band-link"}}
 6        {{capitalize band.name}}
 7        <button class="btn btn-default btn-xs share-button" onclick={{action 'shareBandURL'}} data-band-id={{band.id}}>
 8          Share
 9        </button>
10      {{/link-to}}
11    {{/each}}
12  (...)
13</div>
14<div class="col-md-8">
15  {{outlet}}
16</div>

(Actually, there is an easier way to achieve the same without the need for the data-band-id attribute. See the UPDATE below).

Let's see the new button, on line 7. We trigger the shareBandURL when it's clicked and set a data attribute with the id of the band. Now, let's write the action handler that should generate the URL for that band and then copy it to the clipboard:

 1// app/controllers/bands.js
 2import Ember from 'ember';
 3
 4const { inject } = Ember;
 5
 6export default Ember.Controller.extend({
 7  (...)
 8
 9  router: inject.service(),
10
11  actions: {
12    shareBandURL(event) {
13      let target = event.target;
14      let path = this.get('router').urlFor('bands.band', target.getAttribute('data-band-id'));
15      let { protocol, host } = window.location;
16      let url = `${protocol}//${host}${path}`;
17      copyTextToClipboard(url);
18      event.stopPropagation();
19      return false;
20    }
21  }
22});

We first identify the element that was clicked and then use its data-band-id attribute to get the band's id. Now comes the best part. We use the urlFor method on the router service to get the generated path (the method name is a bit deceiving as it doesn't return the URL, just the path) for the individual band (the bands.band route) and the id. This gives something like /bands/24. We should now prepend the protocol and the host to give us the full URL to copy.

We then proceed to copy this URL to the clipboard, so that it can be pasted. I didn't find it relevant to show how this is exactly done as I wanted to focus on the Ember routing stuff, but you can find the necessary code here.

Lastly, since the button is embedded in a link (more precisely, a link-to) we prevent the click from bubbling up and causing a transition to the new route. And that is all, we can now share the URL by pasting it to wherever we'd like:

Share Led Zeppelin's URL

Other things the router service can do for you

I only showed one method of the router service, urlFor, but it has a lot more and I already have an idea about how to show some others through a not totally contrived example. So stay tuned!

UPDATE

As Gabor Babicz correctly pointed out in the comments (thanks, Gabor!), the data-band-id attribute is not needed as we can pass in the band id directly.

I knew that the action helper takes any number of arguments that will be passed to the handler, but I didn't know that the event itself (which we need to stop its propagation, so that the click doesn't trigger a route transition) is always passed in as the last parameter to the handler.

Using this knowledge, we can get rid of the data attribute:

 1<!-- app/templates/bands.hbs -->
 2<div class="col-md-4">
 3  (...)
 4    {{#each model as |band|}}
 5      {{#link-to "bands.band" band class="list-group-item band-link"}}
 6        {{capitalize band.name}}
 7        <button class="btn btn-default btn-xs share-button" onclick={{action 'shareBandURL' band.id}}>Share</button>
 8      {{/link-to}}
 9    {{/each}}
10  (...)
11</div>
12<div class="col-md-8">
13  {{outlet}}
14</div>

The handler receives the band's id as the first parameter and the event itself as the second, last, one:

 1// app/controllers/bands.js
 2import Ember from 'ember';
 3
 4const { inject } = Ember;
 5
 6export default Ember.Controller.extend({
 7  (...)
 8
 9  router: inject.service(),
10
11  actions: {
12    shareBandURL(bandId, event) {
13      let path = this.get('router').urlFor('bands.band', bandId);
14      let { protocol, host } = window.location;
15      let url = `${protocol}//${host}${path}`;
16      copyTextToClipboard(url);
17      event.stopPropagation();
18      return false;
19    }
20  }
21});

This results in shorter code and dispenses with the data-band-id attribute so it's definitely better than the previous version.

Share on Twitter