Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function isMissing(value: unknown): value is null | undefined {
return value === null || value === undefined;
}
56 changes: 56 additions & 0 deletions packages/@ember/-internals/glimmer/lib/helpers/is-active.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
The `{{isActive}}` helper returns `true` if the given route (and optional
models / query params) matches the application's current route state — the
same logic that `<LinkTo>` uses to apply its `active` CSS class.

```javascript
import { isActive } from '@ember/routing';
```

```gjs
<a class={{if (isActive "about") "active"}}>About</a>
```

With a dynamic segment:

```gjs
{{isActive "post" this.post}}
```

With query params:

```gjs
{{isActive "posts" queryParams=(hash page=2)}}
```

Returns `false` if the route name or any model is null/undefined (loading state).

@method is-active
@for Ember.Templates.helpers
@public
*/
import type RouterService from '@ember/routing/router-service';
import { service } from '@ember/service';
import Helper from '@ember/component/helper';
import { isMissing } from './-router-helpers-utils';

export default class IsActiveHelper extends Helper {
@service('router') declare private router: RouterService;

compute(
[routeName, ...models]: [string | null | undefined, ...unknown[]],
{ queryParams }: { queryParams?: Record<string, unknown> }
): boolean {
if (isMissing(routeName) || models.some(isMissing)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why early return here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RouterService.isActive casts routeName as string and passes it to routerMicrolib.isActiveIntent, which doesn't account for undefined/null (there's a comment to that effect in router-service.ts). This early return guards the loading state so we return false instead of throwing/misbehaving when the route name or a model isn't resolved yet — same guard <LinkTo>'s isLoading uses.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we control all of these methods and can verify if the old comment from over 4 years ago is still accurate :p

return false;
}

// Also track currentRouteName: during loading/error substates the URL
// doesn't change but the active route name does, so isActive() alone
// (which only consumes currentURL) would miss those transitions.
void this.router.currentRouteName;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this is truely needed shouldn't isActive already handle this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally yes, but isActive only entangles with the currentURL tag (consumeTag(tagFor(this._router, 'currentURL')), see #19004). During loading/error substates the URL doesn't change but currentRouteName does, so without separately consuming currentRouteName the helper wouldn't recompute on those transitions. Entangling it manually here is the workaround; fixing it in RouterService.isActive itself would be a separate change.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it sounds like you're describing isActive as having correct behavior tho?

I don't understand


const args = queryParams ? [...models, { queryParams }] : [...models];
return this.router.isActive(routeName, ...args);
}
}
26 changes: 26 additions & 0 deletions packages/@ember/-internals/glimmer/lib/helpers/is-loading.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
The `{{isLoading}}` helper returns `true` if the route name or any of the
passed models are null or undefined, mirroring the loading state that
`<LinkTo>` detects when it renders `#` as the href.

```javascript
import { isLoading, urlFor } from '@ember/routing';
```

```gjs
{{#if (isLoading "post" this.post)}}
Loading…
{{else}}
<a href={{urlFor "post" this.post}}>{{this.post.title}}</a>
{{/if}}
```

@method is-loading
@for Ember.Templates.helpers
@public
*/
import { isMissing } from './-router-helpers-utils';

export default function isLoading(routeName: unknown, ...models: unknown[]): boolean {
return !routeName || models.some(isMissing);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't this help need the Router service in some way?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No — "loading" here is purely "is the route name or any model still null/undefined". This mirrors <LinkTo>'s isLoading getter, which likewise only inspects route/models and never touches the router. There's nothing to ask the router service.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that doesn't make sense -- this is exported as public API, which means it needs to be reactive based on the routerservice -- given the jsdoc comments above, there is implied reactivity, hich this function presently has none of

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
The `{{isTransitioningIn}}` helper returns `true` when the application is
currently transitioning *into* the specified route — i.e., the route is not
yet active but will become active when the in-flight transition settles.

This corresponds to the `ember-transitioning-in` CSS class that `<LinkTo>`
applies during such transitions.

```javascript
import { isTransitioningIn } from '@ember/routing';
```

```gjs
<a class={{if (isTransitioningIn "about") "entering"}}>About</a>
```

Returns `false` when no transition is in flight or the route is already active.

@method is-transitioning-in
@for Ember.Templates.helpers
@public
*/
import type { Maybe } from '@glimmer/interfaces';
import type Route from '@ember/routing/route';
import type { RouterState, RoutingService } from '@ember/routing/-internals';
import { service } from '@ember/service';
import Helper from '@ember/component/helper';
import { isMissing } from './-router-helpers-utils';

export default class IsTransitioningInHelper extends Helper {
@service('-routing') declare private routing: RoutingService<Route>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why the private routing thing?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RouterService.isActive can only answer "is this route active in the current state" — it consumes currentURL and checks the current router state. isTransitioningIn needs to compare the current state against the in-flight targetState and ask whether the route would be active in that target. The public service exposes neither currentState/targetState nor isActiveForRoute(..., state), so we go through the private -routing service. This is exactly what <LinkTo> does for its ember-transitioning-in/-out classes (willBeActive/isActiveForState in link-to.ts).


compute(
[routeName, ...models]: [string | null | undefined, ...unknown[]],
{ queryParams }: { queryParams?: Record<string, unknown> }
): boolean {
if (isMissing(routeName) || models.some(isMissing)) {
return false;
}

const current = this.routing.currentState as Maybe<RouterState>;
const target = this.routing.targetState as Maybe<RouterState>;

// No transition in flight.
if (isMissing(target) || current === target) {
return false;
}

// eslint-disable-next-line @typescript-eslint/no-empty-object-type
const castedModels = models as {}[];
const isCurrentlyActive =
!isMissing(current) &&
this.routing.isActiveForRoute(castedModels, queryParams, routeName, current);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to use private APIs for these isActive checks? what about isActive on the router service?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

router.isActive only evaluates against the current router state, so it can't express "will this be active once the in-flight transition settles." That requires running the active-check against targetState specifically, which is what isActiveForRoute(models, qp, route, state) does. Those APIs aren't on the public RouterService today, so we mirror <LinkTo> and use the private -routing service.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only evaluates against the current router state

i mean... if the route isn't active, isActive is false -- this is correct, yea?

here is the implementation -- which looks pretty thorough:

isActive(...args: RouteArgs) {

const willBeActive = this.routing.isActiveForRoute(castedModels, queryParams, routeName, target);

return !isCurrentlyActive && willBeActive;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
The `{{isTransitioningOut}}` helper returns `true` when the application is
currently transitioning *away from* the specified route — i.e., the route is
active now but will no longer be active when the in-flight transition settles.

This corresponds to the `ember-transitioning-out` CSS class that `<LinkTo>`
applies during such transitions.

```javascript
import { isTransitioningOut } from '@ember/routing';
```

```gjs
<a class={{if (isTransitioningOut "about") "leaving"}}>About</a>
```

Returns `false` when no transition is in flight or the route is not currently active.

@method is-transitioning-out
@for Ember.Templates.helpers
@public
*/
import type { Maybe } from '@glimmer/interfaces';
import type Route from '@ember/routing/route';
import type { RouterState, RoutingService } from '@ember/routing/-internals';
import { service } from '@ember/service';
import Helper from '@ember/component/helper';
import { isMissing } from './-router-helpers-utils';

export default class IsTransitioningOutHelper extends Helper {
@service('-routing') declare private routing: RoutingService<Route>;

compute(
[routeName, ...models]: [string | null | undefined, ...unknown[]],
{ queryParams }: { queryParams?: Record<string, unknown> }
): boolean {
if (isMissing(routeName) || models.some(isMissing)) {
return false;
}

const current = this.routing.currentState as Maybe<RouterState>;
const target = this.routing.targetState as Maybe<RouterState>;

// No transition in flight.
if (isMissing(target) || current === target) {
return false;
}

// eslint-disable-next-line @typescript-eslint/no-empty-object-type
const castedModels = models as {}[];
const isCurrentlyActive =
!isMissing(current) &&
this.routing.isActiveForRoute(castedModels, queryParams, routeName, current);
const willBeActive = this.routing.isActiveForRoute(castedModels, queryParams, routeName, target);

return isCurrentlyActive && !willBeActive;
}
}
26 changes: 26 additions & 0 deletions packages/@ember/-internals/glimmer/lib/helpers/root-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
The `{{rootUrl}}` helper returns the application's configured `rootURL`.

```javascript
import { rootUrl } from '@ember/routing';
```

```gjs
<a href="{{rootUrl}}profile">Profile</a>
```

@method root-url
@for Ember.Templates.helpers
@public
*/
import type RouterService from '@ember/routing/router-service';
import { service } from '@ember/service';
import Helper from '@ember/component/helper';

export default class RootUrlHelper extends Helper {
Comment thread
knownasilya marked this conversation as resolved.
@service('router') declare private router: RouterService;

compute(): string {
return this.router.rootURL;
}
}
46 changes: 46 additions & 0 deletions packages/@ember/-internals/glimmer/lib/helpers/url-for.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
The `{{urlFor}}` helper returns a URL string for a given route, matching the
same arguments as `<LinkTo>`. Unlike `<LinkTo>`, it returns a string value
rather than rendering an anchor element.

```javascript
import { urlFor } from '@ember/routing';
```

```gjs
<a href={{urlFor "profile" this.user}}>Profile</a>
```

With query params:

```gjs
<a href={{urlFor "posts" queryParams=(hash page=2)}}>Page 2</a>
```

Returns `undefined` if the route name or any model is null/undefined (loading state).

@method url-for
@for Ember.Templates.helpers
@public
*/
import type RouterService from '@ember/routing/router-service';
import { service } from '@ember/service';
import Helper from '@ember/component/helper';
import { isMissing } from './-router-helpers-utils';

export default class UrlForHelper extends Helper {
@service('router') declare private router: RouterService;

compute(
[routeName, ...models]: [string | null | undefined, ...unknown[]],
{ queryParams }: { queryParams?: Record<string, unknown> }
): string | undefined {
if (isMissing(routeName) || models.some(isMissing)) {
return undefined;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const args: any[] = queryParams ? [...models, { queryParams }] : [...models];
return this.router.urlFor(routeName, ...args);
}
}
Loading
Loading