Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
77 changes: 77 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,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
Comment thread
knownasilya marked this conversation as resolved.
Outdated
Comment thread
knownasilya marked this conversation as resolved.
Outdated
<a class={{if (is-active "about") "active"}}>About</a>
```

With a dynamic segment:

```handlebars
Comment thread
knownasilya marked this conversation as resolved.
Outdated
{{is-active "post" this.post}}
```

With query params:

```handlebars
Comment thread
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 packages/@ember/-internals/glimmer/lib/helpers/is-loading.ts
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
Comment thread
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'
);
});
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
Comment thread
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 {
Comment thread
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'));
Comment thread
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'
);
}
);
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
Comment thread
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 packages/@ember/-internals/glimmer/lib/helpers/root-url.ts
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
Comment thread
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');
});
68 changes: 68 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,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
Comment thread
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'
);
}
);
12 changes: 12 additions & 0 deletions packages/@ember/-internals/glimmer/lib/resolver.ts
Comment thread
knownasilya marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,16 @@ import { default as normalizeClassHelper } from './helpers/-normalize-class';
import { default as resolve } from './helpers/-resolve';
import { default as trackArray } from './helpers/-track-array';
import { default as eachIn } from './helpers/each-in';
import { default as isActive } from './helpers/is-active';
import { default as isLoading } from './helpers/is-loading';
import { default as isTransitioningIn } from './helpers/is-transitioning-in';
import { default as isTransitioningOut } from './helpers/is-transitioning-out';
import { default as mut } from './helpers/mut';
import { default as readonly } from './helpers/readonly';
import { default as rootUrl } from './helpers/root-url';
import { default as unbound } from './helpers/unbound';
import { default as uniqueId } from './helpers/unique-id';
import { default as urlFor } from './helpers/url-for';

import { mountHelper } from './syntax/mount';
import { outletHelper } from './syntax/outlet';
Expand Down Expand Up @@ -109,6 +115,12 @@ const BUILTIN_HELPERS: Record<string, object> = {
get,
hash,
'unique-id': uniqueId,
'url-for': urlFor,
'root-url': rootUrl,
'is-active': isActive,
'is-loading': isLoading,
'is-transitioning-in': isTransitioningIn,
'is-transitioning-out': isTransitioningOut,
};

if (DEBUG) {
Expand Down
Loading
Loading