-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
implement router helpers #21447
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
implement router helpers #21447
Changes from all commits
024d56a
e44b2b7
f74f148
b1202d9
4d43a2d
9726040
3b66c95
7d760c1
758d552
c6dc22b
a0559f3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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; | ||
| } |
| 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)) { | ||
| 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; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if this is truely needed shouldn't isActive already handle this?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ideally yes, but
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
| } | ||
| } | ||
| 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); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shouldn't this help need the Router service in some way?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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>; | ||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why the private routing thing?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||
|
|
||||
| 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); | ||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
i mean... if the route isn't active, isActive is false -- this is correct, yea? here is the implementation -- which looks pretty thorough:
|
||||
| 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; | ||
| } | ||
| } |
| 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 { | ||
|
knownasilya marked this conversation as resolved.
|
||
| @service('router') declare private router: RouterService; | ||
|
|
||
| compute(): string { | ||
| return this.router.rootURL; | ||
| } | ||
| } | ||
| 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); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why early return here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RouterService.isActivecastsrouteName as stringand passes it torouterMicrolib.isActiveIntent, which doesn't account forundefined/null(there's a comment to that effect inrouter-service.ts). This early return guards the loading state so we returnfalseinstead of throwing/misbehaving when the route name or a model isn't resolved yet — same guard<LinkTo>'sisLoadinguses.There was a problem hiding this comment.
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