Skip to content

Commit

Permalink
Add a link API that navigates without duplicating paths
Browse files Browse the repository at this point in the history
  • Loading branch information
sximba committed Nov 2, 2018
1 parent c25c063 commit 6787494
Show file tree
Hide file tree
Showing 12 changed files with 171 additions and 2 deletions.
5 changes: 5 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## HEAD
> Nov 02, 2018
- Add `history.link` which navigates and prevents same paths in the history stack

## [v4.6.3]
> Jun 20, 2017
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,10 @@ unlisten();
- `history.goBack()`
- `history.goForward()`
- `history.canGo(n)` (only in `createMemoryHistory`)
- `history.link(path, [state])`

When using `push` or `replace` you can either specify both the URL path and state as separate arguments or include everything in a single location-like object as the first argument.
When requiring an action to behave like a link (not pushing duplicate paths to the stack) you can use the `link` method.

1. A URL path _or_
2. A location-like object with `{ pathname, search, hash, state }`
Expand Down
10 changes: 10 additions & 0 deletions modules/LocationUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@ export function createLocation(path, state, key, currentLocation) {
return location;
}

export function shouldReplace(location, newPath, newState) {
const nextLocation = createLocation(newPath, newState, null, location);

return (
location.pathname === nextLocation.pathname &&
location.search === nextLocation.search &&
location.hash === nextLocation.hash
);
}

export function locationsAreEqual(a, b) {
return (
a.pathname === b.pathname &&
Expand Down
6 changes: 6 additions & 0 deletions modules/__tests__/BrowserHistory-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ describe("a browser history", () => {
});
});

describe("navigate with link to the same path", () => {
it("does not add a new location onto the stack, unless the state has change", done => {
TestSequences.LinkSamePath(history, done);
});
});

describe("location created by encoded and unencoded pathname", () => {
it("produces the same location.pathname", done => {
TestSequences.LocationPathnameAlwaysDecoded(history, done);
Expand Down
6 changes: 6 additions & 0 deletions modules/__tests__/HashHistory-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@ describe("a hash history", () => {
});
});

describe("navigate with link to the same path", () => {
it("calls change listeners with the same location and emits a warning", done => {
TestSequences.LinkSamePathWarning(history, done);
});
});

describe("location created by encoded and unencoded pathname", () => {
it("produces the same location.pathname", done => {
TestSequences.LocationPathnameAlwaysDecoded(history, done);
Expand Down
6 changes: 6 additions & 0 deletions modules/__tests__/MemoryHistory-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ describe("a memory history", () => {
});
});

describe("navigate with link to the same path", () => {
it("does not add a new location onto the stack, unless the state has change", done => {
TestSequences.LinkSamePath(history, done);
});
});

describe("location created by encoded and unencoded pathname", () => {
it("produces the same location.pathname", done => {
TestSequences.LocationPathnameAlwaysDecoded(history, done);
Expand Down
63 changes: 63 additions & 0 deletions modules/__tests__/TestSequences/LinkSamePath.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import expect from "expect";
import execSteps from "./execSteps";

export default function(history, done) {
const steps = [
location => {
expect(location).toMatchObject({
pathname: "/"
});

history.link("/home");
},
(location, action) => {
expect(action).toBe("PUSH");
expect(location).toMatchObject({
pathname: "/home"
});

history.link("/home");
},
(location, action) => {
expect(action).toBe("REPLACE");
expect(location).toMatchObject({
pathname: "/home"
});

history.goBack();
},
(location, action) => {
expect(action).toBe("POP");
expect(location).toMatchObject({
pathname: "/"
});

history.link("/home");
},
(location, action) => {
expect(action).toBe("PUSH");
expect(location).toMatchObject({
pathname: "/home"
});

history.link("/home", {the: "state"});
},
(location, action) => {
expect(action).toBe("REPLACE");
expect(location).toMatchObject({
pathname: "/home",
state: {the: "state"}
});

history.goBack();
},
(location, action) => {
expect(action).toBe("POP");
expect(location).toMatchObject({
pathname: "/"
});
}
];

execSteps(steps, history, done);
}
54 changes: 54 additions & 0 deletions modules/__tests__/TestSequences/LinkSamePathWarning.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import expect from "expect";
import execSteps from "./execSteps";

export default function(history, done) {
let prevLocation;

const steps = [
location => {
expect(location).toMatchObject({
pathname: "/"
});

history.link("/home");
},
(location, action) => {
expect(action).toBe("PUSH");
expect(location).toMatchObject({
pathname: "/home"
});

prevLocation = location;

history.link("/home");
},
(location, action) => {
expect(action).toBe("PUSH");
expect(location).toMatchObject({
pathname: "/home"
});

// We should get the SAME location object. Nothing
// new was added to the history stack.
expect(location).toBe(prevLocation);

// We should see a warning message.
expect(warningMessage).toMatch(
"Hash history cannot PUSH the same path; a new entry will not be added to the history stack"
);
}
];

let consoleWarn = console.warn; // eslint-disable-line no-console
let warningMessage;

// eslint-disable-next-line no-console
console.warn = message => {
warningMessage = message;
};

execSteps(steps, history, (...args) => {
console.warn = consoleWarn; // eslint-disable-line no-console
done(...args);
});
}
2 changes: 2 additions & 0 deletions modules/__tests__/TestSequences/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export {
} from "./HashChangeTransitionHook";
export { default as InitialLocationNoKey } from "./InitialLocationNoKey";
export { default as InitialLocationHasKey } from "./InitialLocationHasKey";
export { default as LinkSamePath } from "./LinkSamePath";
export { default as LinkSamePathWarning } from "./LinkSamePathWarning";
export { default as Listen } from "./Listen";
export {
default as LocationPathnameAlwaysDecoded
Expand Down
7 changes: 6 additions & 1 deletion modules/createBrowserHistory.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import warning from "tiny-warning";
import invariant from "tiny-invariant";

import { createLocation } from "./LocationUtils";
import { createLocation, shouldReplace } from "./LocationUtils";
import {
addLeadingSlash,
stripTrailingSlash,
Expand Down Expand Up @@ -253,6 +253,10 @@ function createBrowserHistory(props = {}) {
);
}

function link(path, state) {
shouldReplace(history.location, path, state) ? replace(path, state) : push(path, state);
}

function go(n) {
globalHistory.go(n);
}
Expand Down Expand Up @@ -320,6 +324,7 @@ function createBrowserHistory(props = {}) {
createHref,
push,
replace,
link,
go,
goBack,
goForward,
Expand Down
5 changes: 5 additions & 0 deletions modules/createHashHistory.js
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,10 @@ function createHashHistory(props = {}) {
);
}

function link(path){
push(path);
}

function go(n) {
warning(
canGoWithoutReload,
Expand Down Expand Up @@ -339,6 +343,7 @@ function createHashHistory(props = {}) {
createHref,
push,
replace,
link,
go,
goBack,
goForward,
Expand Down
7 changes: 6 additions & 1 deletion modules/createMemoryHistory.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import warning from "tiny-warning";

import { createPath } from "./PathUtils";
import { createLocation } from "./LocationUtils";
import { createLocation, shouldReplace } from "./LocationUtils";
import createTransitionManager from "./createTransitionManager";

function clamp(n, lowerBound, upperBound) {
Expand Down Expand Up @@ -118,6 +118,10 @@ function createMemoryHistory(props = {}) {
);
}

function link(path, state){
shouldReplace(history.location, path, state) ? replace(path, state) : push(path, state);
}

function go(n) {
const nextIndex = clamp(history.index + n, 0, history.entries.length - 1);

Expand Down Expand Up @@ -174,6 +178,7 @@ function createMemoryHistory(props = {}) {
createHref,
push,
replace,
link,
go,
goBack,
goForward,
Expand Down

0 comments on commit 6787494

Please sign in to comment.