Recreating the LinkTo Component in Ember

Last reviewed on January 8, 2022

Did you know that the router service in Ember provides everything we need to recreate the LinkTo component?

Let's create a component called Link with the same public API as LinkTo.

Terminal
ember g component Link -gc

Next, we'll add some links to our application template.

app/templates/application.hbs
<Link @route="index">
  Home
</Link>

<Link @route="about" class="custom-class">
  About
</Link>

<Link @route="posts" @query={{hash direction="desc"}}>
  Posts in descending order
</Link>

<Link @route="posts.post" @model={{1}}>
  Post 1
</Link>

To begin the implementation of the Link component, let's start with the template:

app/components/link.hbs
<a
  href={{this.href}}
  {{on "click" this.handleClick}}
  ...attributes
>
  {{yield}}
</a>

In this component, we'll have a getter called href that will use the router service and the arguments passed into the component (@route, @model, and @query), to determine the URL. More on this below.

I've added a click handler that will ultimately use the router service and call transitionTo to this.href.

Lastly, I added ...attributes so that HTML attributes passed into the component will end up on the rendered anchor, like the class="custom-class" above.

Now let's look at the backing class.

app/components/link.js
import Component from "@glimmer/component";
import { inject as service } from "@ember/service";
import { action } from "@ember/object";

export default class LinkComponent extends Component {
  @service router;

  get href() {
    const args = [this.args.route];

    if ("model" in this.args) {
      args.push(this.args.model);
    }

    if ("query" in this.args) {
      args.push({
        queryParams: this.args.query,
      });
    }

    return this.router.urlFor(...args);
  }

  @action
  handleClick(event) {
    event.preventDefault();
    this.router.transitionTo(this.href);
  }
}

The href getter uses the router service's urlFor method to determine the href of the anchor based upon the arguments passed into the component, like @model and @query.

The handleClick action simply calls transitionTo on the router service with the URL computed in the href getter. I think it is more common to see transitionTo being called with the route name but it also works by passing in the computed URL, and this is a documented behavior.

And that's it for a basic implementation!

Now let's add support for the active class. Add the following isActive getter to the backing class.

app/components/link.js
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';

export default class LinkComponent extends Component {
  @service router;

  get href() {
    const args = [this.args.route];

    if ('model' in this.args) {
      args.push(this.args.model);
    }

    if ('query' in this.args) {
      args.push({
        queryParams: this.args.query,
      });
    }

    return this.router.urlFor(...args);
  }

  get isActive() {    const args = [this.args.route];    if ('model' in this.args) {      args.push(this.args.model);    }    return this.router.isActive(...args);  }
  @action
  handleClick(event) {
    event.preventDefault();
    this.router.transitionTo(this.href);
  }
}

And finally we'll update the template to use isActive.

app/components/link.hbs
<a
  href={{this.href}}
  class={{if this.isActive "active"}}  {{on "click" this.handleClick}}
  ...attributes
>
  {{yield}}
</a>

Now our anchors will have class="active" when the route is active.

Conclusion

While I recommend just using the LinkTo component since it probably handles some edge cases that I'm not aware of, creating a custom LinkTo component can be useful in some scenarios, like if you want to add in some logic before and after calling transitionTo.