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

Fix URL Update Method for QP-replacement during Transition #303

Open
wants to merge 3 commits into
base: master
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
23 changes: 21 additions & 2 deletions lib/router/router.ts
Expand Up @@ -120,9 +120,19 @@ export default abstract class Router<T extends Route> {
// perform a URL update at the end. This gives
// the user the ability to set the url update
// method (default is replaceState).
let newTransition = new InternalTransition(this, undefined, undefined);
let newTransition = new InternalTransition(
this,
undefined,
undefined,
undefined,
this.activeTransition
);
Comment on lines +123 to +129

Choose a reason for hiding this comment

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

I'm attempting to fix a semi-related bug (emberjs/ember.js#18577) in #307.

- let newTransition = new InternalTransition(this, undefined, undefined);
+ let newTransition = new InternalTransition(this, undefined, newState);

newTransition.queryParamsOnly = true;

if (this.activeTransition) {
this.activeTransition.redirect(newTransition);
}

oldState.queryParams = this.finalizeQueryParamChange(
newState.routeInfos,
newState.queryParams,
Expand Down Expand Up @@ -699,7 +709,16 @@ export default abstract class Router<T extends Route> {
let replacingReplace =
urlMethod === 'replace' && transition.isCausedByAbortingReplaceTransition;

if (initial || replaceAndNotAborting || isQueryParamsRefreshTransition || replacingReplace) {
// If this is the initial transition and we cancelled it to add query params, we should
// *not* use the the original `replace` method; we need to fully update the URL to the initial
// URL _plus_ the new query params
let shouldHonorOriginalTransitionUpdateMethod =
urlMethod === 'replace' && transition.isCausedByUpdateTransition;

if (
!shouldHonorOriginalTransitionUpdateMethod &&
(initial || replaceAndNotAborting || isQueryParamsRefreshTransition || replacingReplace)
) {
this.replaceURL!(url);
} else {
this.updateURL(url);
Expand Down
7 changes: 7 additions & 0 deletions lib/router/transition.ts
Expand Up @@ -63,6 +63,7 @@ export default class Transition<T extends Route> implements Partial<Promise<T>>
isCausedByAbortingTransition = false;
isCausedByInitialTransition = false;
isCausedByAbortingReplaceTransition = false;
isCausedByUpdateTransition = false;
_visibleQueryParams: Dict<unknown> = {};

constructor(
Expand Down Expand Up @@ -107,6 +108,12 @@ export default class Transition<T extends Route> implements Partial<Promise<T>>
(!previousTransition.isCausedByAbortingTransition ||
previousTransition.isCausedByAbortingReplaceTransition);

// This transition was caused by one that intended to update the URL if the previous transition
// would have updated the URL
this.isCausedByUpdateTransition =
!!previousTransition &&
(previousTransition.isCausedByUpdateTransition || previousTransition.urlMethod === 'update');

if (state) {
this[PARAMS_SYMBOL] = state.params;
this[QUERY_PARAMS_SYMBOL] = state.queryParams;
Expand Down
10 changes: 1 addition & 9 deletions tests/query_params_test.ts
Expand Up @@ -4,6 +4,7 @@ import { Dict, Maybe } from 'router/core';
import RouteInfo from 'router/route-info';
import { Promise } from 'rsvp';
import {
consumeAllFinalQueryParams,
createHandler,
flushBackburner,
module,
Expand Down Expand Up @@ -78,15 +79,6 @@ scenarios.forEach(function (scenario) {
router.map(fn);
}

function consumeAllFinalQueryParams(params: Dict<unknown>, finalParams: Dict<unknown>[]) {
for (let key in params) {
let value = params[key];
delete params[key];
finalParams.push({ key: key, value: value });
}
return true;
}

test('a change in query params fires a queryParamsDidChange event', function (assert) {
assert.expect(7);

Expand Down
92 changes: 92 additions & 0 deletions tests/router_test.ts
Expand Up @@ -11,6 +11,7 @@ import { TransitionError } from 'router/transition-state';
import { Promise, reject } from 'rsvp';
import {
assertAbort,
consumeAllFinalQueryParams,
createHandler,
flushBackburner,
handleURL,
Expand Down Expand Up @@ -6225,6 +6226,97 @@ scenarios.forEach(function (scenario) {
assert.equal(barModelCount, 1, 'Bar model should be called once');
});

test('Calling transitionTo with the `replace` method during initial transition with only query params changes should use updateURL', function (assert) {
map(assert, function (match) {
match('/first').to('first');
});

routes.first = createHandler('first', {
model(params: Dict<unknown>) {
// Pass query params through to `afterModel` hook
return params.queryParams;
},
afterModel(model: Dict<unknown>) {
// Ensure we only redirect the first time through here
if (!model.foo) {
router
.transitionTo('first', {
queryParams: {
foo: 'bar',
},
})
.method('replace');
}
},

events: {
// Ensure query params are considered in transition
finalizeQueryParamChange: consumeAllFinalQueryParams,
},
});

router.updateURL = (url) => {
assert.step(`Updated the URL to ${url}`);
};

router.replaceURL = (url) => {
assert.step(`Replaced the URL with ${url}`);
};

transitionTo(router, 'first');
flushBackburner();

assert.verifySteps(['Updated the URL to /first?foo=bar']);
});

test('Calling transitionTo with the `replace` method after initial transition with only query params changes should use updateURL', function (assert) {
map(assert, function (match) {
match('/first').to('first');
match('/second').to('second');
});

routes.first = createHandler('first');
routes.second = createHandler('second', {
model(params: Dict<unknown>) {
// Pass query params through to `afterModel` hook
return params.queryParams;
},
afterModel(model: Dict<unknown>) {
// Ensure we only redirect the first time through here
if (!model.foo) {
router
.transitionTo('second', {
queryParams: {
foo: 'bar',
},
})
.method('replace');
}
},

events: {
// Ensure query params are considered in transition
finalizeQueryParamChange: consumeAllFinalQueryParams,
},
});

router.updateURL = (url) => {
assert.step(`Updated the URL to ${url}`);
};

router.replaceURL = (url) => {
assert.step(`Replaced the URL with ${url}`);
};

transitionTo(router, 'first');
flushBackburner();

transitionTo(router, 'second');
flushBackburner();

assert.verifySteps(['Updated the URL to /first', 'Updated the URL to /second?foo=bar']);
});

test('transitioning to the same route with different context should not reenter the route', function (assert) {
map(assert, function (match) {
match('/project/:project_id').to('project');
Expand Down
9 changes: 9 additions & 0 deletions tests/test_helpers.ts
Expand Up @@ -229,3 +229,12 @@ export function trigger(
throw new Error("Nothing handled the event '" + name + "'.");
}
}

export function consumeAllFinalQueryParams(params: Dict<unknown>, finalParams: Dict<unknown>[]) {
for (let key in params) {
let value = params[key];
delete params[key];
finalParams.push({ key: key, value: value });
}
return true;
}