-
-
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
Open
knownasilya
wants to merge
11
commits into
emberjs:main
Choose a base branch
from
knownasilya:feat/router-helpers
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
implement router helpers #21447
Changes from 2 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
024d56a
feat: router helpers
knownasilya e44b2b7
fix: format files
knownasilya f74f148
fix: docs expected
knownasilya b1202d9
fix: strict-mode only
knownasilya 4d43a2d
fix: format
knownasilya 9726040
docs: update to camelcase usage and add import examples
knownasilya 3b66c95
chore: move isMissing util
knownasilya 7d760c1
fix: plain public api helpers
knownasilya 758d552
fix: public service and other feedback
knownasilya c6dc22b
fix: add another test
knownasilya a0559f3
chore: use gjs highlighting
knownasilya File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
77 changes: 77 additions & 0 deletions
77
packages/@ember/-internals/glimmer/lib/helpers/is-active.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| /** | ||
| The `{{is-active}}` 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. | ||
|
|
||
| ```handlebars | ||
|
knownasilya marked this conversation as resolved.
Outdated
|
||
| <a class={{if (is-active "about") "active"}}>About</a> | ||
| ``` | ||
|
|
||
| With a dynamic segment: | ||
|
|
||
| ```handlebars | ||
|
knownasilya marked this conversation as resolved.
Outdated
|
||
| {{is-active "post" this.post}} | ||
| ``` | ||
|
|
||
| With query params: | ||
|
|
||
| ```handlebars | ||
|
knownasilya marked this conversation as resolved.
Outdated
|
||
| {{is-active "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 { CapturedArguments, Maybe } from '@glimmer/interfaces'; | ||
| import type { InternalOwner } from '@ember/-internals/owner'; | ||
| import { assert } from '@ember/debug'; | ||
| import { createComputeRef, valueForRef } from '@glimmer/reference/lib/reference'; | ||
| import { consumeTag } from '@glimmer/validator/lib/tracking'; | ||
| import { tagFor } from '@glimmer/validator/lib/meta'; | ||
| import type Route from '@ember/routing/route'; | ||
| import type { RouterState, RoutingService } from '@ember/routing/-internals'; | ||
| import { internalHelper } from './internal-helper'; | ||
|
|
||
| function isMissing(value: unknown): value is null | undefined { | ||
| return value === null || value === undefined; | ||
| } | ||
|
|
||
| export default internalHelper( | ||
| ({ positional, named }: CapturedArguments, owner: InternalOwner | undefined) => { | ||
| assert('[BUG] missing owner', owner); | ||
| const routing = owner.lookup('service:-routing') as RoutingService<Route>; | ||
|
|
||
| return createComputeRef( | ||
| () => { | ||
| let routeRef = positional[0]; | ||
| let routeName = | ||
| routeRef !== undefined ? (valueForRef(routeRef) as string | null | undefined) : undefined; | ||
| // eslint-disable-next-line @typescript-eslint/no-empty-object-type | ||
| let models = positional.slice(1).map((ref) => valueForRef(ref)) as {}[]; | ||
| let queryParamsRef = named['queryParams']; | ||
| let queryParams = | ||
| queryParamsRef !== undefined | ||
| ? (valueForRef(queryParamsRef) as Record<string, unknown>) | ||
| : undefined; | ||
|
|
||
| consumeTag(tagFor(routing, 'currentState')); | ||
|
|
||
| if (isMissing(routeName) || models.some(isMissing)) { | ||
| return false; | ||
| } | ||
|
|
||
| let state = routing.currentState as Maybe<RouterState>; | ||
| if (isMissing(state)) { | ||
| return false; | ||
| } | ||
|
|
||
| return routing.isActiveForRoute(models, queryParams, routeName, state); | ||
| }, | ||
| null, | ||
| 'is-active' | ||
| ); | ||
| } | ||
| ); | ||
37 changes: 37 additions & 0 deletions
37
packages/@ember/-internals/glimmer/lib/helpers/is-loading.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| /** | ||
| The `{{is-loading}}` 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. | ||
|
|
||
| ```handlebars | ||
|
knownasilya marked this conversation as resolved.
Outdated
|
||
| {{#if (is-loading "post" this.post)}} | ||
| Loading… | ||
| {{else}} | ||
| <a href={{url-for "post" this.post}}>{{this.post.title}}</a> | ||
| {{/if}} | ||
| ``` | ||
|
|
||
| @method is-loading | ||
| @for Ember.Templates.helpers | ||
| @public | ||
| */ | ||
| import type { CapturedArguments } from '@glimmer/interfaces'; | ||
| import { createComputeRef, valueForRef } from '@glimmer/reference/lib/reference'; | ||
| import { internalHelper } from './internal-helper'; | ||
|
|
||
| function isMissing(value: unknown): value is null | undefined { | ||
| return value === null || value === undefined; | ||
| } | ||
|
|
||
| export default internalHelper(({ positional }: CapturedArguments) => { | ||
| return createComputeRef( | ||
| () => { | ||
| let routeRef = positional[0]; | ||
| let routeName = routeRef !== undefined ? valueForRef(routeRef) : undefined; | ||
| let models = positional.slice(1).map((ref) => valueForRef(ref)); | ||
| return isMissing(routeName) || models.some(isMissing); | ||
| }, | ||
| null, | ||
| 'is-loading' | ||
| ); | ||
| }); | ||
76 changes: 76 additions & 0 deletions
76
packages/@ember/-internals/glimmer/lib/helpers/is-transitioning-in.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| /** | ||
| The `{{is-transitioning-in}}` 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. | ||
|
|
||
| ```handlebars | ||
|
knownasilya marked this conversation as resolved.
Outdated
|
||
| <a class={{if (is-transitioning-in "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 { CapturedArguments, Maybe } from '@glimmer/interfaces'; | ||
| import type { InternalOwner } from '@ember/-internals/owner'; | ||
| import { assert } from '@ember/debug'; | ||
| import { createComputeRef, valueForRef } from '@glimmer/reference/lib/reference'; | ||
| import { consumeTag } from '@glimmer/validator/lib/tracking'; | ||
| import { tagFor } from '@glimmer/validator/lib/meta'; | ||
| import type Route from '@ember/routing/route'; | ||
| import type { RouterState, RoutingService } from '@ember/routing/-internals'; | ||
| import { internalHelper } from './internal-helper'; | ||
|
|
||
| function isMissing(value: unknown): value is null | undefined { | ||
|
knownasilya marked this conversation as resolved.
Outdated
|
||
| return value === null || value === undefined; | ||
| } | ||
|
|
||
| export default internalHelper( | ||
| ({ positional, named }: CapturedArguments, owner: InternalOwner | undefined) => { | ||
| assert('[BUG] missing owner', owner); | ||
| const routing = owner.lookup('service:-routing') as RoutingService<Route>; | ||
|
|
||
| return createComputeRef( | ||
| () => { | ||
| let routeRef = positional[0]; | ||
| let routeName = | ||
| routeRef !== undefined ? (valueForRef(routeRef) as string | null | undefined) : undefined; | ||
| // eslint-disable-next-line @typescript-eslint/no-empty-object-type | ||
| let models = positional.slice(1).map((ref) => valueForRef(ref)) as {}[]; | ||
| let queryParamsRef = named['queryParams']; | ||
| let queryParams = | ||
| queryParamsRef !== undefined | ||
| ? (valueForRef(queryParamsRef) as Record<string, unknown>) | ||
| : undefined; | ||
|
|
||
| consumeTag(tagFor(routing, 'currentState')); | ||
| consumeTag(tagFor(routing, 'targetState')); | ||
|
NullVoxPopuli marked this conversation as resolved.
Outdated
|
||
|
|
||
| if (isMissing(routeName) || models.some(isMissing)) { | ||
| return false; | ||
| } | ||
|
|
||
| let current = routing.currentState as Maybe<RouterState>; | ||
| let target = routing.targetState as Maybe<RouterState>; | ||
|
|
||
| // No transition in flight. | ||
| if (isMissing(target) || current === target) { | ||
| return false; | ||
| } | ||
|
|
||
| let isCurrentlyActive = | ||
| !isMissing(current) && routing.isActiveForRoute(models, queryParams, routeName, current); | ||
| let willBeActive = routing.isActiveForRoute(models, queryParams, routeName, target); | ||
|
|
||
| return !isCurrentlyActive && willBeActive; | ||
| }, | ||
| null, | ||
| 'is-transitioning-in' | ||
| ); | ||
| } | ||
| ); | ||
76 changes: 76 additions & 0 deletions
76
packages/@ember/-internals/glimmer/lib/helpers/is-transitioning-out.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| /** | ||
| The `{{is-transitioning-out}}` 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. | ||
|
|
||
| ```handlebars | ||
|
knownasilya marked this conversation as resolved.
Outdated
|
||
| <a class={{if (is-transitioning-out "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 { CapturedArguments, Maybe } from '@glimmer/interfaces'; | ||
| import type { InternalOwner } from '@ember/-internals/owner'; | ||
| import { assert } from '@ember/debug'; | ||
| import { createComputeRef, valueForRef } from '@glimmer/reference/lib/reference'; | ||
| import { consumeTag } from '@glimmer/validator/lib/tracking'; | ||
| import { tagFor } from '@glimmer/validator/lib/meta'; | ||
| import type Route from '@ember/routing/route'; | ||
| import type { RouterState, RoutingService } from '@ember/routing/-internals'; | ||
| import { internalHelper } from './internal-helper'; | ||
|
|
||
| function isMissing(value: unknown): value is null | undefined { | ||
| return value === null || value === undefined; | ||
| } | ||
|
|
||
| export default internalHelper( | ||
| ({ positional, named }: CapturedArguments, owner: InternalOwner | undefined) => { | ||
| assert('[BUG] missing owner', owner); | ||
| const routing = owner.lookup('service:-routing') as RoutingService<Route>; | ||
|
|
||
| return createComputeRef( | ||
| () => { | ||
| let routeRef = positional[0]; | ||
| let routeName = | ||
| routeRef !== undefined ? (valueForRef(routeRef) as string | null | undefined) : undefined; | ||
| // eslint-disable-next-line @typescript-eslint/no-empty-object-type | ||
| let models = positional.slice(1).map((ref) => valueForRef(ref)) as {}[]; | ||
| let queryParamsRef = named['queryParams']; | ||
| let queryParams = | ||
| queryParamsRef !== undefined | ||
| ? (valueForRef(queryParamsRef) as Record<string, unknown>) | ||
| : undefined; | ||
|
|
||
| consumeTag(tagFor(routing, 'currentState')); | ||
| consumeTag(tagFor(routing, 'targetState')); | ||
|
|
||
| if (isMissing(routeName) || models.some(isMissing)) { | ||
| return false; | ||
| } | ||
|
|
||
| let current = routing.currentState as Maybe<RouterState>; | ||
| let target = routing.targetState as Maybe<RouterState>; | ||
|
|
||
| // No transition in flight. | ||
| if (isMissing(target) || current === target) { | ||
| return false; | ||
| } | ||
|
|
||
| let isCurrentlyActive = | ||
| !isMissing(current) && routing.isActiveForRoute(models, queryParams, routeName, current); | ||
| let willBeActive = routing.isActiveForRoute(models, queryParams, routeName, target); | ||
|
|
||
| return isCurrentlyActive && !willBeActive; | ||
| }, | ||
| null, | ||
| 'is-transitioning-out' | ||
| ); | ||
| } | ||
| ); | ||
24 changes: 24 additions & 0 deletions
24
packages/@ember/-internals/glimmer/lib/helpers/root-url.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| /** | ||
| The `{{root-url}}` helper returns the application's configured `rootURL`. | ||
|
|
||
| ```handlebars | ||
|
knownasilya marked this conversation as resolved.
Outdated
|
||
| <a href="{{root-url}}profile">Profile</a> | ||
| ``` | ||
|
|
||
| @method root-url | ||
| @for Ember.Templates.helpers | ||
| @public | ||
| */ | ||
| import type { CapturedArguments } from '@glimmer/interfaces'; | ||
| import type { InternalOwner } from '@ember/-internals/owner'; | ||
| import { assert } from '@ember/debug'; | ||
| import { createConstRef } from '@glimmer/reference/lib/reference'; | ||
| import type RouterService from '@ember/routing/router-service'; | ||
| import { internalHelper } from './internal-helper'; | ||
|
|
||
| export default internalHelper((_args: CapturedArguments, owner: InternalOwner | undefined) => { | ||
| assert('[BUG] missing owner', owner); | ||
| const router = owner.lookup('service:router') as RouterService; | ||
| // rootURL is a static configuration value — safe to use a const ref. | ||
| return createConstRef(router.rootURL, 'root-url'); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| /** | ||
| The `{{url-for}}` 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. | ||
|
|
||
| ```handlebars | ||
|
knownasilya marked this conversation as resolved.
Outdated
|
||
| <a href={{url-for "profile" this.user}}>Profile</a> | ||
| ``` | ||
|
|
||
| With query params: | ||
|
|
||
| ```handlebars | ||
| <a href={{url-for "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 { CapturedArguments } from '@glimmer/interfaces'; | ||
| import type { InternalOwner } from '@ember/-internals/owner'; | ||
| import { assert } from '@ember/debug'; | ||
| import { createComputeRef, valueForRef } from '@glimmer/reference/lib/reference'; | ||
| import { consumeTag } from '@glimmer/validator/lib/tracking'; | ||
| import { tagFor } from '@glimmer/validator/lib/meta'; | ||
| import type Route from '@ember/routing/route'; | ||
| import type { RoutingService } from '@ember/routing/-internals'; | ||
| import { internalHelper } from './internal-helper'; | ||
|
|
||
| function isMissing(value: unknown): value is null | undefined { | ||
| return value === null || value === undefined; | ||
| } | ||
|
|
||
| export default internalHelper( | ||
| ({ positional, named }: CapturedArguments, owner: InternalOwner | undefined) => { | ||
| assert('[BUG] missing owner', owner); | ||
| const routing = owner.lookup('service:-routing') as RoutingService<Route>; | ||
|
|
||
| return createComputeRef( | ||
| () => { | ||
| let routeRef = positional[0]; | ||
| let routeName = | ||
| routeRef !== undefined ? (valueForRef(routeRef) as string | null | undefined) : undefined; | ||
| // eslint-disable-next-line @typescript-eslint/no-empty-object-type | ||
| let models = positional.slice(1).map((ref) => valueForRef(ref)) as {}[]; | ||
| let queryParamsRef = named['queryParams']; | ||
| let queryParams = | ||
| queryParamsRef !== undefined | ||
| ? (valueForRef(queryParamsRef) as Record<string, unknown>) | ||
| : {}; | ||
|
|
||
| // Consume currentState so this ref invalidates when QPs change, matching | ||
| // the same pattern as LinkTo's href getter. | ||
| consumeTag(tagFor(routing, 'currentState')); | ||
|
|
||
| if (isMissing(routeName) || models.some(isMissing)) { | ||
| return undefined; | ||
| } | ||
|
|
||
| return routing.generateURL(routeName, models, queryParams); | ||
| }, | ||
| null, | ||
| 'url-for' | ||
| ); | ||
| } | ||
| ); | ||
|
knownasilya marked this conversation as resolved.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.