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 Aug 25, 2018
1 parent 05e541e commit 4f7765c
Show file tree
Hide file tree
Showing 12 changed files with 169 additions and 2 deletions.
5 changes: 5 additions & 0 deletions CHANGES.md
@@ -1,3 +1,8 @@
## HEAD
> Aug 25, 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
Expand Up @@ -138,8 +138,10 @@ The `action` is one of `PUSH`, `REPLACE`, or `POP` depending on how the user got
* `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
11 changes: 11 additions & 0 deletions modules/LocationUtils.js
Expand Up @@ -68,6 +68,17 @@ export const createLocation = (path, state, key, currentLocation) => {
return location;
};

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

return (
location.pathname === nextLocation.pathname &&
location.search === nextLocation.search &&
location.search === nextLocation.search &&
valueEqual(location.state, nextLocation.state)
);
};

export const locationsAreEqual = (a, b) =>
a.pathname === b.pathname &&
a.search === b.search &&
Expand Down
6 changes: 6 additions & 0 deletions modules/__tests__/BrowserHistory-test.js
Expand Up @@ -101,6 +101,12 @@ describeHistory("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
Expand Up @@ -103,6 +103,12 @@ describeHistory("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
Expand Up @@ -92,6 +92,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
@@ -0,0 +1,63 @@
import expect from "expect";
import execSteps from "./execSteps";

export default (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("PUSH");
expect(location).toMatchObject({
pathname: "/home",
state: {the: "state"}
});

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

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

export default (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 consoleError = console.error; // eslint-disable-line no-console
let warningMessage;

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

execSteps(steps, history, (...args) => {
console.error = consoleError; // eslint-disable-line no-console
done(...args);
});
};
2 changes: 2 additions & 0 deletions modules/__tests__/TestSequences/index.js
Expand Up @@ -11,6 +11,8 @@ export HashbangHashPathCoding from "./HashbangHashPathCoding";
export HashChangeTransitionHook from "./HashChangeTransitionHook";
export InitialLocationNoKey from "./InitialLocationNoKey";
export InitialLocationHasKey from "./InitialLocationHasKey";
export LinkSamePath from "./LinkSamePath";
export LinkSamePathWarning from "./LinkSamePathWarning";
export Listen from "./Listen";
export LocationPathnameAlwaysDecoded from "./LocationPathnameAlwaysDecoded";
export NoslashHashPathCoding from "./NoslashHashPathCoding";
Expand Down
6 changes: 5 additions & 1 deletion modules/createBrowserHistory.js
@@ -1,6 +1,6 @@
import warning from "warning";
import invariant from "invariant";
import { createLocation } from "./LocationUtils";
import { createLocation, shouldReplace } from "./LocationUtils";
import {
addLeadingSlash,
stripTrailingSlash,
Expand Down Expand Up @@ -252,6 +252,9 @@ const createBrowserHistory = (props = {}) => {
);
};

const link = (path, state) =>
shouldReplace(history.location, path, state) ? replace(path, state) : push(path, state);

const go = n => {
globalHistory.go(n);
};
Expand Down Expand Up @@ -315,6 +318,7 @@ const createBrowserHistory = (props = {}) => {
createHref,
push,
replace,
link,
go,
goBack,
goForward,
Expand Down
4 changes: 4 additions & 0 deletions modules/createHashHistory.js
Expand Up @@ -272,6 +272,9 @@ const createHashHistory = (props = {}) => {
);
};

const link = path =>
push(path);

const go = n => {
warning(
canGoWithoutReload,
Expand Down Expand Up @@ -334,6 +337,7 @@ const createHashHistory = (props = {}) => {
createHref,
push,
replace,
link,
go,
goBack,
goForward,
Expand Down
6 changes: 5 additions & 1 deletion modules/createMemoryHistory.js
@@ -1,6 +1,6 @@
import warning from "warning";
import { createPath } from "./PathUtils";
import { createLocation } from "./LocationUtils";
import { createLocation, shouldReplace } from "./LocationUtils";
import createTransitionManager from "./createTransitionManager";

const clamp = (n, lowerBound, upperBound) =>
Expand Down Expand Up @@ -117,6 +117,9 @@ const createMemoryHistory = (props = {}) => {
);
};

const link = (path, state) =>
shouldReplace(history.location, path, state) ? replace(path, state) : push(path, state);

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

Expand Down Expand Up @@ -165,6 +168,7 @@ const createMemoryHistory = (props = {}) => {
createHref,
push,
replace,
link,
go,
goBack,
goForward,
Expand Down

0 comments on commit 4f7765c

Please sign in to comment.