From 6787494614c6b674fe3794a5ebd4905b7a931166 Mon Sep 17 00:00:00 2001 From: DStyleZ Date: Sat, 25 Aug 2018 05:47:20 +0200 Subject: [PATCH] Add a link API that navigates without duplicating paths --- CHANGES.md | 5 ++ README.md | 2 + modules/LocationUtils.js | 10 +++ modules/__tests__/BrowserHistory-test.js | 6 ++ modules/__tests__/HashHistory-test.js | 6 ++ modules/__tests__/MemoryHistory-test.js | 6 ++ .../__tests__/TestSequences/LinkSamePath.js | 63 +++++++++++++++++++ .../TestSequences/LinkSamePathWarning.js | 54 ++++++++++++++++ modules/__tests__/TestSequences/index.js | 2 + modules/createBrowserHistory.js | 7 ++- modules/createHashHistory.js | 5 ++ modules/createMemoryHistory.js | 7 ++- 12 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 modules/__tests__/TestSequences/LinkSamePath.js create mode 100644 modules/__tests__/TestSequences/LinkSamePathWarning.js diff --git a/CHANGES.md b/CHANGES.md index 8e63ba0d8..771260a05 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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 diff --git a/README.md b/README.md index 34abd7f5a..1c8f85ffa 100644 --- a/README.md +++ b/README.md @@ -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 }` diff --git a/modules/LocationUtils.js b/modules/LocationUtils.js index 311a659f8..367494504 100644 --- a/modules/LocationUtils.js +++ b/modules/LocationUtils.js @@ -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 && diff --git a/modules/__tests__/BrowserHistory-test.js b/modules/__tests__/BrowserHistory-test.js index b77c82197..b386002a5 100644 --- a/modules/__tests__/BrowserHistory-test.js +++ b/modules/__tests__/BrowserHistory-test.js @@ -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); diff --git a/modules/__tests__/HashHistory-test.js b/modules/__tests__/HashHistory-test.js index e4979695d..d62abfc24 100644 --- a/modules/__tests__/HashHistory-test.js +++ b/modules/__tests__/HashHistory-test.js @@ -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); diff --git a/modules/__tests__/MemoryHistory-test.js b/modules/__tests__/MemoryHistory-test.js index 59bd56cd0..36d94099c 100644 --- a/modules/__tests__/MemoryHistory-test.js +++ b/modules/__tests__/MemoryHistory-test.js @@ -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); diff --git a/modules/__tests__/TestSequences/LinkSamePath.js b/modules/__tests__/TestSequences/LinkSamePath.js new file mode 100644 index 000000000..198f3ea26 --- /dev/null +++ b/modules/__tests__/TestSequences/LinkSamePath.js @@ -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); +} diff --git a/modules/__tests__/TestSequences/LinkSamePathWarning.js b/modules/__tests__/TestSequences/LinkSamePathWarning.js new file mode 100644 index 000000000..93f7b8b1d --- /dev/null +++ b/modules/__tests__/TestSequences/LinkSamePathWarning.js @@ -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); + }); +} diff --git a/modules/__tests__/TestSequences/index.js b/modules/__tests__/TestSequences/index.js index 4550522a4..342c2bd40 100644 --- a/modules/__tests__/TestSequences/index.js +++ b/modules/__tests__/TestSequences/index.js @@ -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 diff --git a/modules/createBrowserHistory.js b/modules/createBrowserHistory.js index 8a3b2de00..9ec278792 100644 --- a/modules/createBrowserHistory.js +++ b/modules/createBrowserHistory.js @@ -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, @@ -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); } @@ -320,6 +324,7 @@ function createBrowserHistory(props = {}) { createHref, push, replace, + link, go, goBack, goForward, diff --git a/modules/createHashHistory.js b/modules/createHashHistory.js index 43fc1fc0e..27b7184a4 100644 --- a/modules/createHashHistory.js +++ b/modules/createHashHistory.js @@ -273,6 +273,10 @@ function createHashHistory(props = {}) { ); } + function link(path){ + push(path); + } + function go(n) { warning( canGoWithoutReload, @@ -339,6 +343,7 @@ function createHashHistory(props = {}) { createHref, push, replace, + link, go, goBack, goForward, diff --git a/modules/createMemoryHistory.js b/modules/createMemoryHistory.js index d4bce89b7..0b4cc11e6 100644 --- a/modules/createMemoryHistory.js +++ b/modules/createMemoryHistory.js @@ -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) { @@ -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); @@ -174,6 +178,7 @@ function createMemoryHistory(props = {}) { createHref, push, replace, + link, go, goBack, goForward,