diff --git a/CHANGES.md b/CHANGES.md index 8e63ba0d8..4bb1166ff 100644 --- a/CHANGES.md +++ b/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 diff --git a/README.md b/README.md index 4046bdf50..c94dc32bf 100644 --- a/README.md +++ b/README.md @@ -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 }` diff --git a/modules/LocationUtils.js b/modules/LocationUtils.js index f46cf8e4a..9781b8336 100644 --- a/modules/LocationUtils.js +++ b/modules/LocationUtils.js @@ -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.hash === nextLocation.hash && + valueEqual(location.state, nextLocation.state) + ); +}; + export const locationsAreEqual = (a, b) => a.pathname === b.pathname && a.search === b.search && diff --git a/modules/__tests__/BrowserHistory-test.js b/modules/__tests__/BrowserHistory-test.js index 6520b22a1..0cf8ba6da 100644 --- a/modules/__tests__/BrowserHistory-test.js +++ b/modules/__tests__/BrowserHistory-test.js @@ -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); diff --git a/modules/__tests__/HashHistory-test.js b/modules/__tests__/HashHistory-test.js index 7d399c53e..a4042962e 100644 --- a/modules/__tests__/HashHistory-test.js +++ b/modules/__tests__/HashHistory-test.js @@ -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); diff --git a/modules/__tests__/MemoryHistory-test.js b/modules/__tests__/MemoryHistory-test.js index 4765bc2c7..c26a305ea 100644 --- a/modules/__tests__/MemoryHistory-test.js +++ b/modules/__tests__/MemoryHistory-test.js @@ -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); diff --git a/modules/__tests__/TestSequences/LinkSamePath.js b/modules/__tests__/TestSequences/LinkSamePath.js new file mode 100644 index 000000000..7c63981c9 --- /dev/null +++ b/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); +}; diff --git a/modules/__tests__/TestSequences/LinkSamePathWarning.js b/modules/__tests__/TestSequences/LinkSamePathWarning.js new file mode 100644 index 000000000..0d17f992d --- /dev/null +++ b/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); + }); +}; diff --git a/modules/__tests__/TestSequences/index.js b/modules/__tests__/TestSequences/index.js index ecd908f3e..eb73a7e24 100644 --- a/modules/__tests__/TestSequences/index.js +++ b/modules/__tests__/TestSequences/index.js @@ -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"; diff --git a/modules/createBrowserHistory.js b/modules/createBrowserHistory.js index 66e39a1e5..32ffd39ed 100644 --- a/modules/createBrowserHistory.js +++ b/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, @@ -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); }; @@ -315,6 +318,7 @@ const createBrowserHistory = (props = {}) => { createHref, push, replace, + link, go, goBack, goForward, diff --git a/modules/createHashHistory.js b/modules/createHashHistory.js index bce546f87..cb1d5e3c7 100644 --- a/modules/createHashHistory.js +++ b/modules/createHashHistory.js @@ -272,6 +272,9 @@ const createHashHistory = (props = {}) => { ); }; + const link = path => + push(path); + const go = n => { warning( canGoWithoutReload, @@ -334,6 +337,7 @@ const createHashHistory = (props = {}) => { createHref, push, replace, + link, go, goBack, goForward, diff --git a/modules/createMemoryHistory.js b/modules/createMemoryHistory.js index f30bac3fe..428776c51 100644 --- a/modules/createMemoryHistory.js +++ b/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) => @@ -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); @@ -165,6 +168,7 @@ const createMemoryHistory = (props = {}) => { createHref, push, replace, + link, go, goBack, goForward,