Skip to content

Link primitive to pass around self-contained route references. It's {{link-to}}, but better!

License

Notifications You must be signed in to change notification settings

esbanarango/ember-link

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ember-link

CI npm version Download Total Ember Observer Score code style: prettier
Dependabot enabled dependencies Status devDependencies Status

Introduces a new Link primitive to pass around self-contained references to routes, like URLs, but with state (isActive, ...) and methods (transitionTo, ...). Also brings along an accompanying template helper and component for easy usage in templates.

ember-link does to routing what ember-concurrency did to asynchrony!

— /r/whatjawsdid

Installation

ember install ember-link

👉 This is an Ember Octane addon. For a version that is compatible with older versions of Ember check out the 0.x series.

👉 You are viewing the docs for an improved & refactored release (^1.1.0), that is 100 % backwards compatible to the 1.0.0 version you're used to. There's no reason not to upgrade. ✨

Usage

{{link}} Helper

The {{link}} helper returns a UILink instance.

Invocation Styles

Positional Parameters
{{#let
  (link
    "blogs.posts.post"
    @post.blog.id
    @post.id
    (query-params showFullPost=true)
  )
  as |l|
}}
  <a href={{l.url}} {{on "click" l.transitionTo}}>
    Read the full "{{@post.title}}" story on our {{@post.blog.name}} blog!
  </a>
{{/let}}
Named Parameters
{{#let
  (link
    route="blogs.posts.post"
    models=(array @post.blog.id @post.id)
    query=(hash showFullPost=true)
  )
  as |l|
}}
  <a href={{l.url}} {{on "click" l.transitionTo}}>
    Read the full "{{@post.title}}" story on our {{@post.blog.name}} blog!
  </a>
{{/let}}

When passing a single model, you can use model instead of models:

{{#let
  (link
    route="blogs.posts"
    model=@post.blog.id
  )
  as |l|
}}
  <a href={{l.url}} {{on "click" l.transitionTo}}>
    Read more stories in the {{@post.blog.name}} blog!
  </a>
{{/let}}
Mix & Match

You can also mix and match the parameter styles, however you like.

{{#let
  (link
    "blogs.posts.post"
    @post.blog.id
    @post.id
    query=(hash showFullPost=true)
  )
  as |l|
}}
  <a href={{l.url}} {{on "click" l.transitionTo}}>
    Read the full "{{@post.title}}" story on our {{@post.blog.name}} blog!
  </a>
{{/let}}
fromURL

Instead of the positional & named link parameters described above, you can also create a Link instance from a serialized URL.

{{! someURL = "/blogs/tech/posts/dont-break-the-web" }}
{{#let (link fromURL=this.someURL) as |l|}}
  <a href={{l.url}} {{on "click" l.transitionTo}}>
    Read the next great post.
  </a>
{{/let}}

fromURL is mutually exclusive with the other link parameters: route, model & models, query

Parameters

In addition to the parameters shown above, the {{link}} helper also accepts a preventDefault default parameter. It defaults to true and intelligently prevents hard browser transitions when clicking <a> elements.

See @preventDefault and UILink.

đź’ˇ Pro Tips

Instead of using the {{#let}} helper, you can use the <Link> component to achieve the same scoping effect, with subjectively nicer syntax.

Even better yet, make Link / UILink a first-class primitive in your app architecture! Instead of manually wiring up Link#url and Link#transitionTo() every time, rather create your own ready-to-use, style-guide-compliant link and button components that accept @link as an argument instead of @href and @onClick.

This is akin to the popular async task button component concept.

<Ui::LinkButton @link={{link "subscribe"}}>
  Become a Premium member
</Ui::LinkButton>
<a
  href={{@link.url}}
  class="btn"
  {{on "click" @link.transitionTo}}
  ...attributes
>
  {{yield}}
</a>

<Link> Component

Similar to the the {{link}} helper, the <Link> component yields a UILink instance.

<Link
  @route="some.route"
  @models={{array 123}}
  @query={{hash foo="bar"}}
as |l|>
  <a
    href={{l.url}}
    class={{if l.isActive "is-active"}}
    {{on "click" l.transitionTo}}
  >
    Click me
  </a>
</Link>

Arguments

@route

Required.

The target route name.

Example

<Link @route="some.route" as |l|>
  <a
    href={{l.url}}
    class={{if l.isActive "is-active"}}
    {{on "click" l.transitionTo}}
  >
    Click me
  </a>
</Link>

{{link-to}} equivalent

{{#link-to "some.route"}}
  Click me
{{/link-to}}
@models

Optional. Mutually exclusive with @model.

An array of models / dynamic segments.

Example

<Link @route="some.route" @models={{array someModel someNestedModel}} as |l|>
  <a
    href={{l.url}}
    class={{if l.isActive "is-active"}}
    {{on "click" l.transitionTo}}
  >
    Click me
  </a>
</Link>

{{link-to}} equivalent

{{#link-to "some.route" someModel someNestedModel}}
  Click me
{{/link-to}}
@model

Optional. Mutually exclusive with @models.

Shorthand for providing a single model / dynamic segment. The following two invocations are equivalent:

<Link @route="some.route" @model={{someModel}} />
<Link @route="some.route" @models={{array someModel}} />
@query

Optional.

Query Params object.

Example

<Link @route="some.route" @query={{hash foo="bar"}} as |l|>
  <a
    href={{l.url}}
    class={{if l.isActive "is-active"}}
    {{on "click" l.transitionTo}}
  >
    Click me
  </a>
</Link>

{{link-to}} equivalent

{{#link-to "some.route" (query-params foo="bar")}}
  Click me
{{/link-to}}
@fromURL

Optional. Mutually exclusive with @route, @model / @models, @query.

Example

<Link @fromURL="/blogs/tech/posts/dont-break-the-web" as |l|>
  <a
    href={{l.url}}
    class={{if l.isActive "is-active"}}
    {{on "click" l.transitionTo}}
  >
    Click me
  </a>
</Link>
@preventDefault

Optional. Default: true

If enabled, the transitionTo and replaceWith actions will try to call event.preventDefault() on the first argument, if it is an event. This is an anti-foot-gun to make <Link> just work™️ with <a> and <button>, which would otherwise trigger a native browser navigation / form submission.

Yielded Parameters

The <Link> component yields a UILink instance.

url

string

The URL for this link that you can pass to an <a> tag as the href attribute.

<Link @route="some.route" as |l|>
  <a href={{l.url}} {{on "click" l.transitionTo}}>
    Click me
  </a>
</Link>
isActive

boolean

Whether this route is currently active, including potentially supplied models and query params.

In the following example, only one link will be is-active at any time.

<Link @route="some.route" @models={{array 123}} @query={{hash foo="bar"}} as |l|>
  <a
    href={{l.url}}
    class={{if l.isActive "is-active"}}
    {{on "click" l.transitionTo}}
  >
    One
  </a>
</Link>

<Link @route="some.route" @models={{array 123}} @query={{hash foo="quux"}} as |l|>
  <a
    href={{l.url}}
    class={{if l.isActive "is-active"}}
    {{on "click" l.transitionTo}}
  >
    Two
  </a>
</Link>
isActiveWithoutQueryParams

boolean

Whether this route is currently active, including potentially supplied models, but ignoring query params.

In the following example, the first two links will be is-active simultaneously.

<Link @route="some.route" @models={{array 123}} @query={{hash foo="bar"}} as |l|>
  <a
    href={{l.url}}
    class={{if l.isActiveWithoutQueryParams "is-active"}}
    {{on "click" l.transitionTo}}
  >
    One
  </a>
</Link>

<Link @route="some.route" @models={{array 123}} @query={{hash foo="quux"}} as |l|>
  <a
    href={{l.url}}
    class={{if l.isActiveWithoutQueryParams "is-active"}}
    {{on "click" l.transitionTo}}
  >
    Two
  </a>
</Link>

<Link @route="some.route" @models={{array 456}} @query={{hash foo="quux"}} as |l|>
  <a
    href={{l.url}}
    class={{if l.isActiveWithoutQueryParams "is-active"}}
    {{on "click" l.transitionTo}}
  >
    Three
  </a>
</Link>
isActiveWithoutModels

boolean

Whether this route is currently active, but ignoring models and query params.

In the following example, both links will be is-active simultaneously.

<Link @route="some.route" @models={{array 123}} @query={{hash foo="bar"}} as |l|>
  <a
    href={{l.url}}
    class={{if l.isActiveWithoutModels "is-active"}}
    {{on "click" l.transitionTo}}
  >
    One
  </a>
</Link>

<Link @route="some.route" @models={{array 456}} @query={{hash foo="quux"}} as |l|>
  <a
    href={{l.url}}
    class={{if l.isActiveWithoutModels "is-active"}}
    {{on "click" l.transitionTo}}
  >
    Two
  </a>
</Link>
transitionTo()

(event?: Event) => Transition

Transition into the target route.

If @preventDefault is enabled, also calls event.preventDefault().

replaceWith()

(event?: Event) => Transition

Transition into the target route while replacing the current URL, if possible.

If @preventDefault is enabled, also calls event.preventDefault().

Link

A Link is a self-contained reference to a concrete route, including models and query params. It's basically like a <LinkTo> / {{link-to}} component you can pass around.

You can create a Link via the LinkManager service.

UILink extends Link with some anti-foot-guns and conveniences. It can also be created via the LinkManager service, but also via the {{link}} helper and <Link> component.

Properties

isActive

Type: boolean

Whether this route is currently active, including potentially supplied models and query params.

isActiveWithoutQueryParams

Type: boolean

Whether this route is currently active, including potentially supplied models, but ignoring query params.

isActiveWithoutModels

Type: boolean

Whether this route is currently active, but ignoring models and query params.

url

Type: string

The URL for this link that you can pass to an <a> tag as the href attribute.

routeName

Type: string

The target route name of this link.

models

Type: RouteModel[]

The route models passed in this link.

queryParams

Type: Record<string, unknown> | undefined

The query params for this link, if specified.

Methods

transitionTo()

Returns: Transition

Transition into the target route.

replaceWith()

Returns: Transition

Transition into the target route while replacing the current URL, if possible.

UILink

UILink extends Link with anti-foot-guns and conveniences. This class is meant to be used in templates, primarily through <a> & <button> elements.

It wraps transitionTo() and replaceWith() to optionally accept an event argument. It will intelligently

  • call event.preventDefault() to prevent hard page reloads
  • open the page in a new tab, when Cmd / Ctrl clicking

It can be created via the LinkManager service, but also via the {{link}} helper and <Link> component.

LinkManager

The LinkManager service is used by the {{link}} helper and <Link> component to create UILink instances.

You can also use this service directly to programmatically create link references.

createLink(linkParams: LinkParams): Link

Returns: Link

interface LinkParams {
  /**
   * The target route name.
   */
  route: string;

  /**
   * Optional array of models / dynamic segments.
   */
  models?: RouteModel[];

  /**
   * Optional query params object.
   */
  query?: QueryParams;
}

createUILink(linkParams: LinkParams, uiParams: UILinkParams): UILink

Returns: UILink

interface UILinkParams {
  /**
   * Whether or not to call `event.preventDefault()`, if the first parameter to
   * the `transitionTo` or `replaceWith` action is an `Event`. This is useful to
   * prevent links from accidentally triggering real browser navigation or
   * buttons from submitting a form.
   *
   * Defaults to `true`.
   */
  preventDefault?: boolean;
}

getLinkParamsFromURL(url: string): LinkParams

Returns: LinkParams

Use this method to derive LinkParams from a serialized, recognizable URL, that you can then pass into createLink / createUILink.

Testing

In acceptance / application tests (setupApplicationTest(hooks)) your app boots with a fully-fledged router, so ember-link just works normally.

In integration / render tests (setupRenderingTest(hooks)) the router is not initialized, so ember-link can't operate normally. To still support using {{link}} & friends in render tests, you can use the setupLink(hooks) test helper.

import { click, render } from '@ember/test-helpers';
import { setupRenderingTest } from 'ember-qunit';
import { module, test } from 'qunit';

import { setupLink, linkFor, TestLink } from 'ember-link/test-support';

import hbs from 'htmlbars-inline-precompile';

module('`setupLink` example', function (hooks) {
  setupRenderingTest(hooks);
  setupLink(hooks);

  test('`<Link>` component works in render tests', async function (assert) {
    await render(hbs`
      <Link @route="some.route" as |l|>
        <a
          href={{l.url}}
          class={{if l.isActive "is-active"}}
          {{on "click" l.transitionTo}}
        >
          Click me
        </a>
      </Link>
    `);

    const link = linkFor('some.route');
    link.onTransitionTo = assert.step('link clicked');

    await click('a');

    assert.verifySteps(['link clicked']);
  });
});

Related RFCs / Projects

About

Link primitive to pass around self-contained route references. It's {{link-to}}, but better!

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • TypeScript 88.2%
  • JavaScript 8.7%
  • HTML 2.7%
  • Handlebars 0.4%