Skip to content
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

feat: Keep (Parent Route) Query Parameters #749

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
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
1 change: 1 addition & 0 deletions addon/helpers/link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ export default class LinkHelper extends Helper {
) as RouteModel[])
: undefined,
query: named.query ?? positionalQueryParameters,
mode: named.mode,
onTransitionTo: named.onTransitionTo,
onReplaceWith: named.onReplaceWith
};
Expand Down
154 changes: 145 additions & 9 deletions addon/link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Transition from '@ember/routing/-private/transition';
import RouterService from '@ember/routing/router-service';
import { DEBUG } from '@glimmer/env';
import { tracked } from '@glimmer/tracking';
import { next } from '@ember/runloop';

import LinkManagerService from './services/link-manager';

Expand Down Expand Up @@ -46,6 +47,16 @@ export interface LinkParams {
*/
query?: QueryParams;

/**
* Sets the mode for the link
* - `all`: all params are included in the link
* - `known`: only params that are known to the target route are included, respects the passed in params
* - `tracked-all`: all params are included in the link, and the link will be updated when any of the params change
* - `tracked-known`: only params that are known to the target route are included, and the link will be updated when any of these params change, it also respects the passed in params
* - `none`: no parent params are included in the link, just the ones passed in
*/
mode?: 'known' | 'all' | 'tracked-all' | 'tracked-known' | 'none';

/**
* An optional callback that will be fired when the Link is transitioned to.
*
Expand Down Expand Up @@ -81,6 +92,13 @@ function isMouseEvent(event: unknown): event is MouseEvent {
return typeof event === 'object' && event !== null && 'button' in event;
}

interface Qp {
urlKey: string;
}
interface Qps {
qps: Qp[];
}

export default class Link {
@tracked
// eslint-disable-next-line @typescript-eslint/naming-convention
Expand All @@ -89,10 +107,36 @@ export default class Link {
// eslint-disable-next-line @typescript-eslint/naming-convention
protected _linkManager: LinkManagerService;

@tracked
private _knownRouteQps?: Qps | (() => Qps);

constructor(linkManager: LinkManagerService, params: LinkParams) {
setOwner(this, getOwner(linkManager));
this._linkManager = linkManager;
this._params = freezeParams(params);

//In order to support `known` we can't read Route._qp inside the getter, because it will entangle autotracking
//Is there a better way to do this?, router.currentRouteName is not always available
//Maybe this._linkManager.router._router.url instead of window.location.pathname?
if (this._linkManager.router.currentRouteName) {
this._knownRouteQps = getOwner(this).lookup(
`route:${this._linkManager.router.currentRouteName}`
)?._qp;
} else {
const routeName = this._linkManager.router.recognize(
window?.location?.pathname || this._linkManager.router._router.url
)?.name;
const cb = () => {
this._knownRouteQps = getOwner(this).lookup(`route:${routeName}`)?._qp;
};
if (routeName) {
next(this, cb);
}
}
}

get mode() {
return this._params.mode ?? 'none';
}

private get _routeArgs(): RouteArgs {
Expand All @@ -103,7 +147,7 @@ export default class Link {
...models,
// Cloning `queryParams` is necessary, since we freeze it, but Ember
// wants to mutate it.
{ queryParams: { ...queryParams } }
{ queryParams: { ...queryParams } },
] as unknown as RouteArgs;
}
return [routeName, ...models] as RouteArgs;
Expand All @@ -128,7 +172,7 @@ export default class Link {
this._linkManager.currentTransitionStack; // eslint-disable-line @typescript-eslint/no-unused-expressions
return this._linkManager.router.isActive(
this.routeName,
// Unfortunately TypeScript is not clever enough to support "rest"
// Unfortunately TypeScript is not clever enough to support 'rest'
// parameters in the middle.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
Expand Down Expand Up @@ -164,11 +208,103 @@ export default class Link {
* The URL for this link that you can pass to an `<a>` tag as the `href`
* attribute.
*/
get url(): string {
private createNoneUrl(): string {
if (!this._linkManager.isRouterInitialized) return '';
return this._linkManager.router.urlFor(...this._routeArgs);
}

private createKnownUrl(routeQps?: Qps | (() => Qps)): string {
if (!this._linkManager.isRouterInitialized) return '';
const { routeName, models, queryParams = {} } = this;

let mergedQps: {
[key: string]: unknown;
} = {};
const cloned = { ...queryParams };
let final: {
[key: string]: unknown;
} = {};

mergedQps = { ...queryParams };
this._linkManager.routing.normalizeQueryParams(
routeName,
models,
mergedQps
);

const qp = typeof routeQps === 'function' ? routeQps() : routeQps;

if (qp && qp.qps.length > 0) {
const { qps } = qp;
for (const q of qps) {
final[q.urlKey] = mergedQps[q.urlKey];
}
final = {
...final,
...cloned,
};
} else {
final = { ...cloned };
}

final = { queryParams: { ...final } };

return this._linkManager.router.urlFor(routeName, ...models, final);
}

private createAllUrl() {
if (!this._linkManager.isRouterInitialized) return '';
const { routeName, models, queryParams = {} } = this;
return this._linkManager.routing.generateURL(
routeName,
models,
queryParams
);
}

get trackedAllUrl(): string {
this._linkManager.currentTransitionStack;
return this.createAllUrl();
}

get trackedKnownUrl(): string {
this._linkManager.currentTransitionStack;
return this.createKnownUrl(() => {
return getOwner(this).lookup(
`route:${this._linkManager.router.currentRouteName}`
)?._qp;
});
}

get knownUrl(): string {
return this.createKnownUrl(this._knownRouteQps);
}

get noneUrl(): string {
return this.createNoneUrl();
}

get allUrl(): string {
return this.createAllUrl();
}

get url(): string {
switch (this.mode) {
case 'known':
return this.knownUrl;
case 'all':
return this.allUrl;
case 'tracked-known':
return this.trackedKnownUrl;
case 'tracked-all':
return this.trackedAllUrl;
case 'none':
return this.noneUrl;
default:
return this.noneUrl;
}
}

/**
* Deprecated alias for `url`.
*/
Expand All @@ -179,8 +315,8 @@ export default class Link {
for: 'ember-link',
since: {
available: '1.1.0',
enabled: '1.1.0'
}
enabled: '1.1.0',
},
});
return this.url;
}
Expand All @@ -191,7 +327,7 @@ export default class Link {
* Allows for more ergonomic composition as query parameters.
*
* ```hbs
* {{link "foo" query=(hash bar=(link "bar"))}}
* {{link 'foo' query=(hash bar=(link 'bar'))}}
* ```
*/
toString() {
Expand Down Expand Up @@ -240,7 +376,7 @@ export default class Link {

private _isTransitioning(direction: 'from' | 'to') {
return (
this._linkManager.currentTransitionStack?.some(transition => {
this._linkManager.currentTransitionStack?.some((transition) => {
return transition[direction]?.name === this.qualifiedRouteName;
}) ?? false
);
Expand All @@ -258,7 +394,7 @@ export default class Link {

this._params.onTransitionTo?.();

return this._linkManager.router.transitionTo(...this._routeArgs);
return this._linkManager.router.transitionTo(this.url);
}

/**
Expand All @@ -274,7 +410,7 @@ export default class Link {

this._params.onReplaceWith?.();

return this._linkManager.router.replaceWith(...this._routeArgs);
return this._linkManager.router.replaceWith(this.url);
}
}

Expand Down
11 changes: 7 additions & 4 deletions addon/services/link-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ export default class LinkManagerService extends Service {
@service('router')
readonly router!: RouterServiceWithRecognize;

@service('-routing')
readonly routing!: any;

/**
* Whether the router has been initialized.
* This will be `false` in render tests.
Expand Down Expand Up @@ -79,19 +82,19 @@ export default class LinkManagerService extends Service {
* Converts a `RouteInfo` object into `LinkParams`.
*/
static getLinkParamsFromRouteInfo(routeInfo: RouteInfo): LinkParams {
const models = routeInfo.paramNames.map(name => routeInfo.params[name]!);
const models = routeInfo.paramNames.map((name) => routeInfo.params[name]!);
return {
route: routeInfo.name,
query: routeInfo.queryParams,
models
models,
};
}

// eslint-disable-next-line @typescript-eslint/ban-types
constructor(properties?: object) {
super(properties);

// Ignore `Argument of type '"routeWillChange"' is not assignable to parameter of type ...`
// Ignore `Argument of type ''routeWillChange'' is not assignable to parameter of type ...`

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
Expand All @@ -118,7 +121,7 @@ export default class LinkManagerService extends Service {
handleRouteWillChange(transition: Transition) {
this._currentTransitionStack = [
...(this._currentTransitionStack || []),
transition
transition,
];
}

Expand Down
14 changes: 14 additions & 0 deletions tests/dummy/app/controllers/application.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';

export default class ApplicationController extends Controller {
queryParams = ['applicationCategory', 'applicationColor'];

@tracked applicationCategory = 'all';
@tracked applicationColor = 'red';

update =(name: string , e: InputEvent) => {
//@ts-expect-error
this[name] = (e.target as HTMLInputElement).value;
}
}
17 changes: 17 additions & 0 deletions tests/dummy/app/controllers/parent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';

export default class ApplicationController extends Controller {
queryParams = [
'parentCategory',
'parentColor'
];

@tracked parentCategory = 'all';
@tracked parentColor = 'red';

update =(name: string , e: InputEvent) => {
//@ts-expect-error
this[name] = (e.target as HTMLInputElement).value;
}
}
Loading