/**
@module ember
*/
/**
The `{{link-to}}` component renders a link to the supplied
`routeName` passing an optionally supplied model to the
route as its `model` context of the route. The block
for `{{link-to}}` becomes the innerHTML of the rendered
element:
```handlebars
{{#link-to 'photoGallery'}}
Great Hamster Photos
{{/link-to}}
```
You can also use an inline form of `{{link-to}}` component by
passing the link text as the first argument
to the component:
```handlebars
{{link-to 'Great Hamster Photos' 'photoGallery'}}
```
Both will result in:
```html
Great Hamster Photos
```
### Supplying a tagName
By default `{{link-to}}` renders an `` element. This can
be overridden for a single use of `{{link-to}}` by supplying
a `tagName` option:
```handlebars
{{#link-to 'photoGallery' tagName="li"}}
Great Hamster Photos
{{/link-to}}
```
```html
Great Hamster Photos
```
To override this option for your entire application, see
"Overriding Application-wide Defaults".
### Disabling the `link-to` component
By default `{{link-to}}` is enabled.
any passed value to the `disabled` component property will disable
the `link-to` component.
static use: the `disabled` option:
```handlebars
{{#link-to 'photoGallery' disabled=true}}
Great Hamster Photos
{{/link-to}}
```
dynamic use: the `disabledWhen` option:
```handlebars
{{#link-to 'photoGallery' disabledWhen=controller.someProperty}}
Great Hamster Photos
{{/link-to}}
```
any truthy value passed to `disabled` will disable it except `undefined`.
See "Overriding Application-wide Defaults" for more.
### Handling `href`
`{{link-to}}` will use your application's Router to
fill the element's `href` property with a url that
matches the path to the supplied `routeName` for your
router's configured `Location` scheme, which defaults
to HashLocation.
### Handling current route
`{{link-to}}` will apply a CSS class name of 'active'
when the application's current route matches
the supplied routeName. For example, if the application's
current route is 'photoGallery.recent' the following
use of `{{link-to}}`:
```handlebars
{{#link-to 'photoGallery.recent'}}
Great Hamster Photos
{{/link-to}}
```
will result in
```html
Great Hamster Photos
```
The CSS class name used for active classes can be customized
for a single use of `{{link-to}}` by passing an `activeClass`
option:
```handlebars
{{#link-to 'photoGallery.recent' activeClass="current-url"}}
Great Hamster Photos
{{/link-to}}
```
```html
Great Hamster Photos
```
To override this option for your entire application, see
"Overriding Application-wide Defaults".
### Keeping a link active for other routes
If you need a link to be 'active' even when it doesn't match
the current route, you can use the `current-when` argument.
```handlebars
{{#link-to 'photoGallery' current-when='photos'}}
Photo Gallery
{{/link-to}}
```
This may be helpful for keeping links active for:
* non-nested routes that are logically related
* some secondary menu approaches
* 'top navigation' with 'sub navigation' scenarios
A link will be active if `current-when` is `true` or the current
route is the route this link would transition to.
To match multiple routes 'space-separate' the routes:
```handlebars
{{#link-to 'gallery' current-when='photos drawings paintings'}}
Art Gallery
{{/link-to}}
```
### Supplying a model
An optional model argument can be used for routes whose
paths contain dynamic segments. This argument will become
the model context of the linked route:
```javascript
Router.map(function() {
this.route("photoGallery", {path: "hamster-photos/:photo_id"});
});
```
```handlebars
{{#link-to 'photoGallery' aPhoto}}
{{aPhoto.title}}
{{/link-to}}
```
```html
Tomster
```
### Supplying multiple models
For deep-linking to route paths that contain multiple
dynamic segments, multiple model arguments can be used.
As the router transitions through the route path, each
supplied model argument will become the context for the
route with the dynamic segments:
```javascript
Router.map(function() {
this.route("photoGallery", { path: "hamster-photos/:photo_id" }, function() {
this.route("comment", {path: "comments/:comment_id"});
});
});
```
This argument will become the model context of the linked route:
```handlebars
{{#link-to 'photoGallery.comment' aPhoto comment}}
{{comment.body}}
{{/link-to}}
```
```html
A+++ would snuggle again.
```
### Supplying an explicit dynamic segment value
If you don't have a model object available to pass to `{{link-to}}`,
an optional string or integer argument can be passed for routes whose
paths contain dynamic segments. This argument will become the value
of the dynamic segment:
```javascript
Router.map(function() {
this.route("photoGallery", { path: "hamster-photos/:photo_id" });
});
```
```handlebars
{{#link-to 'photoGallery' aPhotoId}}
{{aPhoto.title}}
{{/link-to}}
```
```html
Tomster
```
When transitioning into the linked route, the `model` hook will
be triggered with parameters including this passed identifier.
### Allowing Default Action
By default the `{{link-to}}` component prevents the default browser action
by calling `preventDefault()` as this sort of action bubbling is normally
handled internally and we do not want to take the browser to a new URL (for
example).
If you need to override this behavior specify `preventDefault=false` in
your template:
```handlebars
{{#link-to 'photoGallery' aPhotoId preventDefault=false}}
{{aPhotoId.title}}
{{/link-to}}
```
### Overriding attributes
You can override any given property of the `LinkComponent`
that is generated by the `{{link-to}}` component by passing
key/value pairs, like so:
```handlebars
{{#link-to aPhoto tagName='li' title='Following this link will change your life' classNames='pic sweet'}}
Uh-mazing!
{{/link-to}}
```
See [LinkComponent](/api/ember/release/classes/LinkComponent) for a
complete list of overrideable properties. Be sure to also
check out inherited properties of `LinkComponent`.
### Overriding Application-wide Defaults
``{{link-to}}`` creates an instance of `LinkComponent` for rendering. To
override options for your entire application, export your customized
`LinkComponent` from `app/components/link-to.js` with the desired overrides:
```javascript
// app/components/link-to.js
import LinkComponent from '@ember/routing/link-component';
export default LinkComponent.extend({
activeClass: "is-active",
tagName: 'li'
})
```
It is also possible to override the default event in this manner:
```javascript
import LinkComponent from '@ember/routing/link-component';
export default LinkComponent.extend({
eventName: 'customEventName'
});
```
@method link-to
@for Ember.Templates.helpers
@param {String} routeName
@param {Object} [context]*
@param [options] {Object} Handlebars key/value pairs of options, you can override any property of Ember.LinkComponent
@return {String} HTML string
@see {LinkComponent}
@public
*/
import { computed, get } from '@ember/-internals/metal';
import { isSimpleClick } from '@ember/-internals/views';
import { assert, warn } from '@ember/debug';
import { flaggedInstrument } from '@ember/instrumentation';
import { inject as injectService } from '@ember/service';
import { DEBUG } from '@glimmer/env';
import EmberComponent, { HAS_BLOCK } from '../component';
import layout from '../templates/link-to';
/**
@module @ember/routing
*/
/**
`LinkComponent` renders an element whose `click` event triggers a
transition of the application's instance of `Router` to
a supplied route by name.
`LinkComponent` components are invoked with {{#link-to}}. Properties
of this class can be overridden with `reopen` to customize application-wide
behavior.
@class LinkComponent
@extends Component
@see {Ember.Templates.helpers.link-to}
@public
**/
const LinkComponent = EmberComponent.extend({
layout,
tagName: 'a',
/**
Used to determine when this `LinkComponent` is active.
@property current-when
@public
*/
'current-when': null,
/**
Sets the `title` attribute of the `LinkComponent`'s HTML element.
@property title
@default null
@public
**/
title: null,
/**
Sets the `rel` attribute of the `LinkComponent`'s HTML element.
@property rel
@default null
@public
**/
rel: null,
/**
Sets the `tabindex` attribute of the `LinkComponent`'s HTML element.
@property tabindex
@default null
@public
**/
tabindex: null,
/**
Sets the `target` attribute of the `LinkComponent`'s HTML element.
@since 1.8.0
@property target
@default null
@public
**/
target: null,
/**
The CSS class to apply to `LinkComponent`'s element when its `active`
property is `true`.
@property activeClass
@type String
@default active
@public
**/
activeClass: 'active',
/**
The CSS class to apply to `LinkComponent`'s element when its `loading`
property is `true`.
@property loadingClass
@type String
@default loading
@private
**/
loadingClass: 'loading',
/**
The CSS class to apply to a `LinkComponent`'s element when its `disabled`
property is `true`.
@property disabledClass
@type String
@default disabled
@private
**/
disabledClass: 'disabled',
/**
Determines whether the `LinkComponent` will trigger routing via
the `replaceWith` routing strategy.
@property replace
@type Boolean
@default false
@public
**/
replace: false,
/**
By default the `{{link-to}}` component will bind to the `href` and
`title` attributes. It's discouraged that you override these defaults,
however you can push onto the array if needed.
@property attributeBindings
@type Array | String
@default ['title', 'rel', 'tabindex', 'target']
@public
*/
attributeBindings: ['href', 'title', 'rel', 'tabindex', 'target'],
/**
By default the `{{link-to}}` component will bind to the `active`, `loading`,
and `disabled` classes. It is discouraged to override these directly.
@property classNameBindings
@type Array
@default ['active', 'loading', 'disabled', 'ember-transitioning-in', 'ember-transitioning-out']
@public
*/
classNameBindings: ['active', 'loading', 'disabled', 'transitioningIn', 'transitioningOut'],
/**
By default the `{{link-to}}` component responds to the `click` event. You
can override this globally by setting this property to your custom
event name.
This is particularly useful on mobile when one wants to avoid the 300ms
click delay using some sort of custom `tap` event.
@property eventName
@type String
@default click
@private
*/
eventName: 'click',
// this is doc'ed here so it shows up in the events
// section of the API documentation, which is where
// people will likely go looking for it.
/**
Triggers the `LinkComponent`'s routing behavior. If
`eventName` is changed to a value other than `click`
the routing behavior will trigger on that custom event
instead.
@event click
@private
*/
/**
An overridable method called when `LinkComponent` objects are instantiated.
Example:
```app/components/my-link.js
import LinkComponent from '@ember/routing/link-component';
export default LinkComponent.extend({
init() {
this._super(...arguments);
console.log('Event is ' + this.get('eventName'));
}
});
```
NOTE: If you do override `init` for a framework class like `Component`,
be sure to call `this._super(...arguments)` in your
`init` declaration! If you don't, Ember may not have an opportunity to
do important setup work, and you'll see strange behavior in your
application.
@method init
@private
*/
init() {
this._super(...arguments);
// Map desired event name to invoke function
let eventName = get(this, 'eventName');
this.on(eventName, this, this._invoke);
},
_routing: injectService('-routing'),
/**
Accessed as a classname binding to apply the `LinkComponent`'s `disabledClass`
CSS `class` to the element when the link is disabled.
When `true` interactions with the element will not trigger route changes.
@property disabled
@private
*/
disabled: computed({
get(_key) {
// always returns false for `get` because (due to the `set` just below)
// the cached return value from the set will prevent this getter from _ever_
// being called after a set has occured
return false;
},
set(_key, value) {
this._isDisabled = value;
return value ? get(this, 'disabledClass') : false;
},
}),
_isActive(routerState) {
if (get(this, 'loading')) {
return false;
}
let currentWhen = get(this, 'current-when');
if (typeof currentWhen === 'boolean') {
return currentWhen;
}
let isCurrentWhenSpecified = !!currentWhen;
currentWhen = currentWhen || get(this, 'qualifiedRouteName');
currentWhen = currentWhen.split(' ');
let routing = get(this, '_routing');
let models = get(this, 'models');
let resolvedQueryParams = get(this, 'resolvedQueryParams');
for (let i = 0; i < currentWhen.length; i++) {
if (routing.isActiveForRoute(models, resolvedQueryParams, currentWhen[i], routerState, isCurrentWhenSpecified)) {
return true;
}
}
return false;
},
/**
Accessed as a classname binding to apply the `LinkComponent`'s `activeClass`
CSS `class` to the element when the link is active.
A `LinkComponent` is considered active when its `currentWhen` property is `true`
or the application's current route is the route the `LinkComponent` would trigger
transitions into.
The `currentWhen` property can match against multiple routes by separating
route names using the ` ` (space) character.
@property active
@private
*/
active: computed('activeClass', '_active', function computeLinkToComponentActiveClass() {
return this.get('_active') ? get(this, 'activeClass') : false;
}),
_active: computed('_routing.currentState', 'attrs.params', function computeLinkToComponentActive() {
let currentState = get(this, '_routing.currentState');
if (!currentState) {
return false;
}
return this._isActive(currentState);
}),
willBeActive: computed('_routing.targetState', function computeLinkToComponentWillBeActive() {
let routing = get(this, '_routing');
let targetState = get(routing, 'targetState');
if (get(routing, 'currentState') === targetState) {
return;
}
return this._isActive(targetState);
}),
transitioningIn: computed('active', 'willBeActive', function computeLinkToComponentTransitioningIn() {
if (get(this, 'willBeActive') === true && !get(this, '_active')) {
return 'ember-transitioning-in';
}
else {
return false;
}
}),
transitioningOut: computed('active', 'willBeActive', function computeLinkToComponentTransitioningOut() {
if (get(this, 'willBeActive') === false && get(this, '_active')) {
return 'ember-transitioning-out';
}
else {
return false;
}
}),
/**
Event handler that invokes the link, activating the associated route.
@method _invoke
@param {Event} event
@private
*/
_invoke(event) {
if (!isSimpleClick(event)) {
return true;
}
let preventDefault = get(this, 'preventDefault');
let targetAttribute = get(this, 'target');
if (preventDefault !== false && (!targetAttribute || targetAttribute === '_self')) {
event.preventDefault();
}
if (get(this, 'bubbles') === false) {
event.stopPropagation();
}
if (this._isDisabled) {
return false;
}
if (get(this, 'loading')) {
// tslint:disable-next-line:max-line-length
warn('This link-to is in an inactive loading state because at least one of its parameters presently has a null/undefined value, or the provided route name is invalid.', false, {
id: 'ember-glimmer.link-to.inactive-loading-state',
});
return false;
}
if (targetAttribute && targetAttribute !== '_self') {
return false;
}
let qualifiedRouteName = get(this, 'qualifiedRouteName');
let models = get(this, 'models');
let queryParams = get(this, 'queryParams.values');
let shouldReplace = get(this, 'replace');
let payload = {
queryParams,
routeName: qualifiedRouteName,
};
// tslint:disable-next-line:max-line-length
flaggedInstrument('interaction.link-to', payload, this._generateTransition(payload, qualifiedRouteName, models, queryParams, shouldReplace));
return false;
},
_generateTransition(payload, qualifiedRouteName, models, queryParams, shouldReplace) {
let routing = get(this, '_routing');
return () => {
payload.transition = routing.transitionTo(qualifiedRouteName, models, queryParams, shouldReplace);
};
},
queryParams: null,
qualifiedRouteName: computed('targetRouteName', '_routing.currentState', function computeLinkToComponentQualifiedRouteName() {
let params = get(this, 'params');
let paramsLength = params.length;
let lastParam = params[paramsLength - 1];
if (lastParam && lastParam.isQueryParams) {
paramsLength--;
}
let onlyQueryParamsSupplied = this[HAS_BLOCK] ? paramsLength === 0 : paramsLength === 1;
if (onlyQueryParamsSupplied) {
return get(this, '_routing.currentRouteName');
}
return get(this, 'targetRouteName');
}),
resolvedQueryParams: computed('queryParams', function computeLinkToComponentResolvedQueryParams() {
let resolvedQueryParams = {};
let queryParams = get(this, 'queryParams');
if (!queryParams) {
return resolvedQueryParams;
}
let values = queryParams.values;
for (let key in values) {
if (!values.hasOwnProperty(key)) {
continue;
}
resolvedQueryParams[key] = values[key];
}
return resolvedQueryParams;
}),
/**
Sets the element's `href` attribute to the url for
the `LinkComponent`'s targeted route.
If the `LinkComponent`'s `tagName` is changed to a value other
than `a`, this property will be ignored.
@property href
@private
*/
href: computed('models', 'qualifiedRouteName', function computeLinkToComponentHref() {
if (get(this, 'tagName') !== 'a') {
return;
}
let qualifiedRouteName = get(this, 'qualifiedRouteName');
let models = get(this, 'models');
if (get(this, 'loading')) {
return get(this, 'loadingHref');
}
let routing = get(this, '_routing');
let queryParams = get(this, 'queryParams.values');
if (DEBUG) {
/*
* Unfortunately, to get decent error messages, we need to do this.
* In some future state we should be able to use a "feature flag"
* which allows us to strip this without needing to call it twice.
*
* if (isDebugBuild()) {
* // Do the useful debug thing, probably including try/catch.
* } else {
* // Do the performant thing.
* }
*/
try {
routing.generateURL(qualifiedRouteName, models, queryParams);
}
catch (e) {
// tslint:disable-next-line:max-line-length
assert('You attempted to define a `{{link-to "' +
qualifiedRouteName +
'"}}` but did not pass the parameters required for generating its dynamic segments. ' +
e.message);
}
}
return routing.generateURL(qualifiedRouteName, models, queryParams);
}),
loading: computed('_modelsAreLoaded', 'qualifiedRouteName', function computeLinkToComponentLoading() {
let qualifiedRouteName = get(this, 'qualifiedRouteName');
let modelsAreLoaded = get(this, '_modelsAreLoaded');
if (!modelsAreLoaded || qualifiedRouteName === null || qualifiedRouteName === undefined) {
return get(this, 'loadingClass');
}
}),
_modelsAreLoaded: computed('models', function computeLinkToComponentModelsAreLoaded() {
let models = get(this, 'models');
for (let i = 0; i < models.length; i++) {
let model = models[i];
if (model === null || model === undefined) {
return false;
}
}
return true;
}),
_getModels(params) {
let modelCount = params.length - 1;
let models = new Array(modelCount);
for (let i = 0; i < modelCount; i++) {
let value = params[i + 1];
models[i] = value;
}
return models;
},
/**
The default href value to use while a link-to is loading.
Only applies when tagName is 'a'
@property loadingHref
@type String
@default #
@private
*/
loadingHref: '#',
didReceiveAttrs() {
let queryParams;
let params = get(this, 'params');
if (params) {
// Do not mutate params in place
params = params.slice();
}
assert('You must provide one or more parameters to the link-to component.', params && params.length);
let disabledWhen = get(this, 'disabledWhen');
if (disabledWhen !== undefined) {
this.set('disabled', disabledWhen);
}
// Process the positional arguments, in order.
// 1. Inline link title comes first, if present.
if (!this[HAS_BLOCK]) {
this.set('linkTitle', params.shift());
}
// 2. `targetRouteName` is now always at index 0.
this.set('targetRouteName', params[0]);
// 3. The last argument (if still remaining) is the `queryParams` object.
let lastParam = params[params.length - 1];
if (lastParam && lastParam.isQueryParams) {
queryParams = params.pop();
}
else {
queryParams = { values: {} };
}
this.set('queryParams', queryParams);
// 4. Any remaining indices (excepting `targetRouteName` at 0) are `models`.
if (params.length > 1) {
this.set('models', this._getModels(params));
}
else {
this.set('models', []);
}
},
});
LinkComponent.toString = () => '@ember/routing/link-component';
LinkComponent.reopenClass({
positionalParams: 'params',
});
export default LinkComponent;