A lighter weight implementation of link-to
02 February 2018
A while ago I showed how to use the router service to copy front-end generated URLs to the clipboard without transitioning to the routes that generate those URLs. In this post, I want to build on top of that scenario and implement Ember's built-in link-to using a great little add-on, ember-href-to and some custom code.
Why do that?
There's nothing wrong with link-to in most cases but each link-to invocation creates a component instance which on a page with a high number of links can lead to decreased performance. Gavin Joyce realized this while working on the Ember app at Intercom and implemented (with several other contributors) a replacement for link-to that provides a helper that generates the URL (and handles the click events) for Ember generated routes.
There's also an argument to be made (and I'm going to make it at the end of this post) about having more flexibility when you decompose a feature to its composing parts.
Ok, let's use the ember-href-to
addon to start replacing the band links in our app.
Replace link-to calls with a
tags and href-to
The band template currently looks as below:
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>
You can see we create the links for each band with a link-to on line 5. Let's install the ember-href-to
add-on so that we have access to the href-to
helper:
$ ember install ember-href-to
We can now rewrite the link-to line:
This works but no link is marked as active:
That makes sense as one of the extras you get with link-to is that the appropriate links are automatically marked as active (by adding a configurable 'active' class to them). The add-on states that clearly in its README:
As {{href-to}} simply generates a URL, you won't get automatic active class bindings as you do with {{link-to}}.
Fair enough. Armed with a full-fledged router service, this is not difficult to add ourselves, so let's get to it.
Restore the active class for links
Let's create a helper that returns whether a certain route (including models and query params) is active or not. We'll have to create a class-based helper as we'll need access to the router service and simple, functional helpers cannot do that.
Ember CLI currently doesn't have a way to create class-based helpers (I started work on adding it but haven't got to finishing it yet) so we'll do it manually:
1// app/helpers/is-active.js 2import Helper from '@ember/component/helper'; 3import { inject as service } from '@ember/service'; 4import { observer } from '@ember/object'; 5 6export default Helper.extend({ 7 router: service(), 8 9 compute(params) { 10 return this.get('router').isActive(...params); 11 } 12});
Believe it or not, that's really it. Since the router service has an isActive
method, we just had to enable calling it in the template layer - via a helper. We can now add the class we'd like based on the result of the is-active
call:
This should work now, let's see it in action:
Oops, the active link is correctly marked as such but when we go to a new band's page (when we transition to a new route), the activeness is not updated.
Update link activeness for each transition
The problem is that our is-active
helper doesn't know it should recalculate its output (run its compute
function) as none of its inputs (the bands.band
string or the band object) change. There's a way to trigger a "manual" update and it's through the helper's recompute
function. So when should we call recompute? Whenever the router makes a transition, in other words, whenever the URL changes.
Fortunately, the router service also exposes a currentURL
property so we can react to its changes:
1// app/helpers/is-active.js 2import Helper from '@ember/component/helper'; 3import { inject as service } from '@ember/service'; 4import { observer } from '@ember/object'; 5 6export default Helper.extend({ 7 router: service(), 8 9 compute(params) { 10 return this.get('router').isActive(...params); 11 }, 12 13 onURLChange: observer('router.currentURL', function() { 14 this.recompute(); 15 }), 16});
And now we're really done, we have successfully assembled link-to from its constituent parts:
The argument for added flexibility via composition
With link-to
you get a package deal: you'll get an a
tag through a component and an active
class set on it, when appropriate. However, in some cases, you might need the active
class on a parent tag (or not need it at all). People have been struggling with this (when integrating with Bootstrap, for example) and Alex Speller even wrote an add-on to tackle this problem.
Having command over the parts that make the whole is a great benefit: you can assemble the full functionality from its parts when you need to - and only when you need to, I should add.
Share on Twitter