ember-source
Version:
A JavaScript framework for creating ambitious web applications
619 lines (548 loc) • 19.4 kB
JavaScript
import { isSimpleClick } from '../-internals/views/lib/system/utils.js';
import inspect from '../debug/lib/inspect.js';
import { debugFreeze, warn } from '../debug/index.js';
import { getEngineParent } from '../engine/lib/engine-parent.js';
import { flaggedInstrument } from '../instrumentation/index.js';
import { action } from '../object/index.js';
import { service } from '../service/index.js';
import { k as consumeTag, l as createCache, r as getValue, x as untrack } from '../../shared-chunks/cache-B7dqAS38.js';
import { t as tagFor } from '../../shared-chunks/meta-BmRXesrk.js';
import { on } from '../modifier/on.js';
import { t as templateFactory } from '../../shared-chunks/index-CyZiGiEI.js';
import { I as InternalComponent, o as opaquify } from '../../shared-chunks/internal-CAL-jHZt.js';
import { d as decorateFieldV2, i as initializeDeferredDecorator, a as decorateMethodV2 } from '../../shared-chunks/runtime-CYyqkz5q-BOdRhmsS.js';
import { assert } from '../debug/lib/assert.js';
const LinkToTemplate = templateFactory(
/*
<a
{{!-- for compatibility --}}
id={{this.id}}
class={{this.class}}
{{!-- deprecated attribute bindings --}}
role={{this.role}}
title={{this.title}}
rel={{this.rel}}
tabindex={{this.tabindex}}
target={{this.target}}
...attributes
href={{this.href}}
{{on 'click' this.click}}
>{{yield}}</a>
*/
{
"id": null,
"block": "[[[11,3],[16,1,[30,0,[\"id\"]]],[16,0,[30,0,[\"class\"]]],[16,\"role\",[30,0,[\"role\"]]],[16,\"title\",[30,0,[\"title\"]]],[16,\"rel\",[30,0,[\"rel\"]]],[16,\"tabindex\",[30,0,[\"tabindex\"]]],[16,\"target\",[30,0,[\"target\"]]],[17,1],[16,6,[30,0,[\"href\"]]],[4,[32,0],[\"click\",[30,0,[\"click\"]]],null],[12],[18,2,null],[13]],[\"&attrs\",\"&default\"],[\"yield\"]]",
"moduleName": "packages/@ember/-internals/glimmer/lib/templates/link-to.hbs",
"scope": () => ({
on
}),
"isStrictMode": true
});
const EMPTY_ARRAY = [];
const EMPTY_QUERY_PARAMS = {};
debugFreeze(EMPTY_ARRAY);
debugFreeze(EMPTY_QUERY_PARAMS);
function isMissing(value) {
return value === null || value === undefined;
}
function isPresent(value) {
return !isMissing(value);
}
function isQueryParams(value) {
return typeof value === 'object' && value !== null && value['isQueryParams'] === true;
}
/**
The `LinkTo` 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 `LinkTo`
becomes the contents of the rendered element:
```gjs
import { LinkTo } from '@ember/routing';
<template>
<LinkTo @route='photoGallery'>
Great Hamster Photos
</LinkTo>
</template>
```
This will result in:
```html
<a href="/hamster-photos">
Great Hamster Photos
</a>
```
### Disabling the `LinkTo` component
The `LinkTo` component can be disabled by using the `disabled` argument. A disabled link
doesn't result in a transition when activated, and adds the `disabled` class to the `<a>`
element.
(The class name to apply to the element can be overridden by using the `disabledClass`
argument)
```handlebars
<LinkTo @route='photoGallery' @disabled={{true}}>
Great Hamster Photos
</LinkTo>
```
### Handling `href`
`<LinkTo>` will use your application's Router to fill the element's `href` property with a URL
that matches the path to the supplied `routeName`.
### Handling current route
The `LinkTo` component 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', then the following invocation of `LinkTo`:
```handlebars
<LinkTo @route='photoGallery.recent'>
Great Hamster Photos
</LinkTo>
```
will result in
```html
<a href="/hamster-photos/this-week" class="active">
Great Hamster Photos
</a>
```
The CSS class used for active classes can be customized by passing an `activeClass` argument:
```handlebars
<LinkTo @route='photoGallery.recent' @activeClass="current-url">
Great Hamster Photos
</LinkTo>
```
```html
<a href="/hamster-photos/this-week" class="current-url">
Great Hamster Photos
</a>
```
### 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
<LinkTo @route='photoGallery' @current-when='photos'>
Photo Gallery
</LinkTo>
```
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
<LinkTo @route='gallery' @current-when='photos drawings paintings'>
Art Gallery
</LinkTo>
```
### 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
<LinkTo @route='photoGallery' @model={{this.aPhoto}}>
{{aPhoto.title}}
</LinkTo>
```
```html
<a href="/hamster-photos/42">
Tomster
</a>
```
### Supplying multiple models
For deep-linking to route paths that contain multiple
dynamic segments, the `models` argument 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
<LinkTo @route='photoGallery.comment' @models={{array this.aPhoto this.comment}}>
{{comment.body}}
</LinkTo>
```
```html
<a href="/hamster-photos/42/comments/718">
A+++ would snuggle again.
</a>
```
### Supplying an explicit dynamic segment value
If you don't have a model object available to pass to `LinkTo`,
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
<LinkTo @route='photoGallery' @model={{aPhotoId}}>
{{this.aPhoto.title}}
</LinkTo>
```
```html
<a href="/hamster-photos/42">
Tomster
</a>
```
When transitioning into the linked route, the `model` hook will
be triggered with parameters including this passed identifier.
### Supplying query parameters
If you need to add optional key-value pairs that appear to the right of the ? in a URL,
you can use the `query` argument.
```handlebars
<LinkTo @route='photoGallery' @query={{hash page=1 per_page=20}}>
Great Hamster Photos
</LinkTo>
```
This will result in:
```html
<a href="/hamster-photos?page=1&per_page=20">
Great Hamster Photos
</a>
```
@for Ember.Templates.components
@method LinkTo
@public
*/
/**
@module @ember/routing
*/
/**
See [Ember.Templates.components.LinkTo](/ember/release/classes/Ember.Templates.components/methods/input?anchor=LinkTo).
@for Ember.Templates.helpers
@method link-to
@see {Ember.Templates.components.LinkTo}
@public
**/
/**
An opaque interface which can be imported and used in strict-mode
templates to call <LinkTo>.
See [Ember.Templates.components.LinkTo](/ember/release/classes/Ember.Templates.components/methods/input?anchor=LinkTo).
@for @ember/routing
@method LinkTo
@see {Ember.Templates.components.LinkTo}
@public
**/
class _LinkTo extends InternalComponent {
static toString() {
return 'LinkTo';
}
static {
decorateFieldV2(this.prototype, "routing", [service('-routing')]);
}
#routing = (initializeDeferredDecorator(this, "routing"), void 0);
validateArguments() {
(!(!this.isEngine || this.engineMountPoint !== undefined) && assert('You attempted to use the <LinkTo> component within a routeless engine, this is not supported. ' + 'If you are using the ember-engines addon, use the <LinkToExternal> component instead. ' + 'See https://github.com/ember-engines/ember-engines for more info.', !this.isEngine || this.engineMountPoint !== undefined));
(!('route' in this.args.named || 'model' in this.args.named || 'models' in this.args.named || 'query' in this.args.named) && assert('You must provide at least one of the `@route`, `@model`, `@models` or `@query` arguments to `<LinkTo>`.', 'route' in this.args.named || 'model' in this.args.named || 'models' in this.args.named || 'query' in this.args.named));
(!(!('model' in this.args.named && 'models' in this.args.named)) && assert('You cannot provide both the `@model` and `@models` arguments to the <LinkTo> component.', !('model' in this.args.named && 'models' in this.args.named)));
super.validateArguments();
}
get class() {
let classes = 'ember-view';
if (this.isActive) {
classes += this.classFor('active');
if (this.willBeActive === false) {
classes += ' ember-transitioning-out';
}
} else if (this.willBeActive) {
classes += ' ember-transitioning-in';
}
if (this.isLoading) {
classes += this.classFor('loading');
}
if (this.isDisabled) {
classes += this.classFor('disabled');
}
return classes;
}
get href() {
if (this.isLoading) {
return '#';
}
let {
routing,
route,
models,
query
} = this;
(!(isPresent(route)) && assert('[BUG] route can only be missing if isLoading is true', isPresent(route))); // consume the current router state so we invalidate when QP changes
// TODO: can we narrow this down to QP changes only?
consumeTag(tagFor(routing, 'currentState'));
{
try {
return routing.generateURL(route, models, query);
} catch (e) {
let details = e instanceof Error ? e.message : inspect(e);
let message = `While generating link to route "${route}": ${details}`;
if (e instanceof Error) {
e.message = message;
throw e;
} else {
throw message;
}
}
}
}
click(event) {
if (!isSimpleClick(event)) {
return;
}
let element = event.currentTarget;
(!(element instanceof HTMLAnchorElement || element instanceof SVGAElement) && assert('[BUG] must be an <a> element', element instanceof HTMLAnchorElement || element instanceof SVGAElement));
let target = element instanceof SVGAElement ? element.target.baseVal : element.target;
let isSelf = target === '' || target === '_self';
if (isSelf) {
this.preventDefault(event);
} else {
return;
}
if (this.isDisabled) {
return;
}
if (this.isLoading) {
(warn('This link is in an inactive loading state because at least one of its models ' + 'currently has a null/undefined value, or the provided route name is invalid.', false, {
id: 'ember-glimmer.link-to.inactive-loading-state'
}));
return;
}
let {
routing,
route,
models,
query,
replace
} = this;
let payload = {
transition: undefined
};
flaggedInstrument('interaction.link-to', payload, () => {
(!(isPresent(route)) && assert('[BUG] route can only be missing if isLoading is true', isPresent(route)));
payload.transition = routing.transitionTo(route, models, query, replace);
});
}
static {
decorateMethodV2(this.prototype, "click", [action]);
}
get route() {
if ('route' in this.args.named) {
let route = this.named('route');
(!(isMissing(route) || typeof route === 'string') && assert('The `@route` argument to the <LinkTo> component must be a string', isMissing(route) || typeof route === 'string'));
return route && this.namespaceRoute(route);
} else {
return this.currentRoute;
}
}
// GH #17963
currentRouteCache = createCache(() => {
consumeTag(tagFor(this.routing, 'currentState'));
return untrack(() => this.routing.currentRouteName);
});
get currentRoute() {
return getValue(this.currentRouteCache);
}
// TODO: not sure why generateURL takes {}[] instead of unknown[]
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
get models() {
if ('models' in this.args.named) {
let models = this.named('models');
(!(Array.isArray(models)) && assert('The `@models` argument to the <LinkTo> component must be an array.', Array.isArray(models)));
return models;
} else if ('model' in this.args.named) {
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
return [this.named('model')];
} else {
return EMPTY_ARRAY;
}
}
get query() {
if ('query' in this.args.named) {
let query = this.named('query');
(!(query !== null && typeof query === 'object') && assert('The `@query` argument to the <LinkTo> component must be an object.', query !== null && typeof query === 'object'));
return {
...query
};
} else {
return EMPTY_QUERY_PARAMS;
}
}
get replace() {
return this.named('replace') === true;
}
get isActive() {
return this.isActiveForState(this.routing.currentState);
}
get willBeActive() {
let current = this.routing.currentState;
let target = this.routing.targetState;
if (current === target) {
return null;
} else {
return this.isActiveForState(target);
}
}
get isLoading() {
return isMissing(this.route) || this.models.some(model => isMissing(model));
}
get isDisabled() {
return Boolean(this.named('disabled'));
}
get isEngine() {
let owner = this.owner;
return getEngineParent(owner) !== undefined;
}
get engineMountPoint() {
let owner = this.owner;
return owner.mountPoint;
}
classFor(state) {
let className = this.named(`${state}Class`);
(!(isMissing(className) || typeof className === 'string' || typeof className === 'boolean') && assert(`The \`@${state}Class\` argument to the <LinkTo> component must be a string or boolean`, isMissing(className) || typeof className === 'string' || typeof className === 'boolean'));
if (className === true || isMissing(className)) {
return ` ${state}`;
} else if (className) {
return ` ${className}`;
} else {
return '';
}
}
namespaceRoute(route) {
let {
engineMountPoint
} = this;
if (engineMountPoint === undefined) {
return route;
} else if (route === 'application') {
return engineMountPoint;
} else {
return `${engineMountPoint}.${route}`;
}
}
isActiveForState(state) {
if (!isPresent(state)) {
return false;
}
if (this.isLoading) {
return false;
}
let currentWhen = this.named('current-when');
if (typeof currentWhen === 'boolean') {
return currentWhen;
} else if (typeof currentWhen === 'string') {
let {
routing
} = this;
return currentWhen.split(' ').some(route => routing.isActiveForRoute([], undefined, this.namespaceRoute(route), state));
} else {
let {
route,
models,
query,
routing
} = this;
(!(isPresent(route)) && assert('[BUG] route can only be missing if isLoading is true', isPresent(route)));
return routing.isActiveForRoute(models, query, route, state);
}
}
preventDefault(event) {
event.preventDefault();
}
isSupportedArgument(name) {
let supportedArguments = ['route', 'model', 'models', 'query', 'replace', 'disabled', 'current-when', 'activeClass', 'loadingClass', 'disabledClass'];
return supportedArguments.indexOf(name) !== -1 || super.isSupportedArgument(name);
}
}
let {
prototype
} = _LinkTo;
let descriptorFor = (target, property) => {
if (target) {
return Object.getOwnPropertyDescriptor(target, property) || descriptorFor(Object.getPrototypeOf(target), property);
} else {
return null;
}
};
// @href
{
let superOnUnsupportedArgument = prototype['onUnsupportedArgument'];
Object.defineProperty(prototype, 'onUnsupportedArgument', {
configurable: true,
enumerable: false,
value: function onUnsupportedArgument(name) {
if (name === 'href') {
(assert(`Passing the \`\` argument to <LinkTo> is not supported.`));
} else {
superOnUnsupportedArgument.call(this, name);
}
}
});
}
// QP
{
let superModelsDescriptor = descriptorFor(prototype, 'models');
(!(superModelsDescriptor && typeof superModelsDescriptor.get === 'function') && assert(`[BUG] expecting models to be a getter on <LinkTo>`, superModelsDescriptor && typeof superModelsDescriptor.get === 'function')); // eslint-disable-next-line @typescript-eslint/no-empty-object-type
let superModelsGetter = superModelsDescriptor.get;
Object.defineProperty(prototype, 'models', {
configurable: true,
enumerable: false,
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
get: function models() {
let models = superModelsGetter.call(this);
if (models.length > 0 && !('query' in this.args.named)) {
if (isQueryParams(models[models.length - 1])) {
models = models.slice(0, -1);
}
}
return models;
}
});
let superQueryDescriptor = descriptorFor(prototype, 'query');
(!(superQueryDescriptor && typeof superQueryDescriptor.get === 'function') && assert(`[BUG] expecting query to be a getter on <LinkTo>`, superQueryDescriptor && typeof superQueryDescriptor.get === 'function')); // eslint-disable-next-line @typescript-eslint/no-empty-object-type
let superQueryGetter = superQueryDescriptor.get;
Object.defineProperty(prototype, 'query', {
configurable: true,
enumerable: false,
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
get: function query() {
if ('query' in this.args.named) {
let qp = superQueryGetter.call(this);
if (isQueryParams(qp)) {
return qp.values ?? EMPTY_QUERY_PARAMS;
} else {
return qp;
}
} else {
let models = superModelsGetter.call(this);
if (models.length > 0) {
let qp = models[models.length - 1];
if (isQueryParams(qp) && qp.values !== null) {
return qp.values;
}
}
return EMPTY_QUERY_PARAMS;
}
}
});
}
// Positional Arguments
{
let superOnUnsupportedArgument = prototype['onUnsupportedArgument'];
Object.defineProperty(prototype, 'onUnsupportedArgument', {
configurable: true,
enumerable: false,
value: function onUnsupportedArgument(name) {
if (name !== 'params') {
superOnUnsupportedArgument.call(this, name);
}
}
});
}
const LinkTo = opaquify(_LinkTo, LinkToTemplate);
export { LinkTo };