Skip to content

Commit

Permalink
Add hashType option to hash history
Browse files Browse the repository at this point in the history
hashType may be any of "slash" (the default), "hashbang", or "noslash"
to use different styles of hash URLs.

Fixes #278
Fixes #249
  • Loading branch information
mjackson committed Jul 16, 2016
1 parent 9dd1ac3 commit 6be82b0
Show file tree
Hide file tree
Showing 8 changed files with 327 additions and 199 deletions.
30 changes: 17 additions & 13 deletions docs/HashHistoryCaveats.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,30 @@ HTML5 gives us the `pushState` method and the `popstate` event, but in older bro
import createHistory from 'history/lib/createHashHistory'

// Use _key instead of _k.
let history = createHistory({
const history = createHistory({
queryKey: '_key'
})

// Opt-out of persistent state, not recommended.
let history = createHistory({
const history = createHistory({
queryKey: false
})
```

One other thing to keep in mind when using hash history is that you cannot also use `window.location.hash` as it was originally intended, to link an anchor point within your HTML document.

### Using Different Hash Types

Several types of hash URLs are supported if you'd like to customize the characters that appear in your hash fragment. They are:

// Use custom URL transformations, for example for hashbang support
let history = createHistory({
transformPath: (path, encode) => {
// if encoding, we transform the path to our hash value
if (encode)
return path.indexOf('!') !== 0 ? `!${path}` : path
- `slash` (the default, prefixes all hash paths with `/`)
- `hashbang` (Google's deprecated [crawlable recommendation](https://developers.google.com/webmasters/ajax-crawling/docs/learn-more))
- `noslash` (omits the leading `/` in the URL)

// when decoding, we transform the hash value back into the original path
return path.substring(1)
}
Use the `hashType` option to select one of these when you create your hash history.

```js
const history = createHistory({
hashType: 'hashbang'
})
```

One other thing to keep in mind when using hash history is that you cannot also use `window.location.hash` as it was originally intended, to link an anchor point within your HTML document.
60 changes: 26 additions & 34 deletions modules/HashProtocol.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { createLocation } from './LocationUtils'
import { addEventListener, removeEventListener } from './DOMUtils'
import { saveState, readState } from './DOMStateStorage'
import {
isAbsolutePath,
addQueryStringValueToPath,
stripQueryStringValueFromPath,
getQueryStringValueFromPath,
Expand Down Expand Up @@ -32,21 +31,10 @@ const replaceHashPath = (path) => {
)
}

export const ensureSlash = (path, encode) => {
if (encode) {
if (isAbsolutePath(path))
return path

return '/' + path
}

return path
}

export { getUserConfirmation, go } from './BrowserProtocol'

export const getCurrentLocation = (queryKey, transform) => {
let path = transform(getHashPath(), false)
export const getCurrentLocation = (pathCoder, queryKey) => {
let path = pathCoder.decodePath(getHashPath())
const key = getQueryStringValueFromPath(path, queryKey)

let state
Expand All @@ -63,37 +51,41 @@ export const getCurrentLocation = (queryKey, transform) => {

let prevLocation

export const startListener = (listener, queryKey, transform) => {
export const startListener = (listener, pathCoder, queryKey) => {
const handleHashChange = () => {
const transformedPath = transform(getHashPath(), true)
const path = getHashPath()
const encodedPath = pathCoder.encodePath(path)

replaceHashPath(transformedPath)

const currentLocation = getCurrentLocation(queryKey, transform)
if (path !== encodedPath) {
replaceHashPath(encodedPath)
} else {
const currentLocation = getCurrentLocation(pathCoder, queryKey)

if (prevLocation && currentLocation.key && prevLocation.key === currentLocation.key)
return // Ignore extraneous hashchange events
if (prevLocation && currentLocation.key && prevLocation.key === currentLocation.key)
return // Ignore extraneous hashchange events

prevLocation = currentLocation
prevLocation = currentLocation

listener(currentLocation)
listener(currentLocation)
}
}

const path = getHashPath()
const transformedPath = transform(path, true)
const encodedPath = pathCoder.encodePath(path)

if (path !== transformedPath)
replaceHashPath(transformedPath)
if (path !== encodedPath)
replaceHashPath(encodedPath)

addEventListener(window, HashChangeEvent, handleHashChange)

return () =>
removeEventListener(window, HashChangeEvent, handleHashChange)
}

const updateLocation = (location, queryKey, updateHash, transformPath) => {
const updateLocation = (location, pathCoder, queryKey, updateHash) => {
const { state, key } = location
let path = transformPath(createPath(location), true)

let path = pathCoder.encodePath(createPath(location))

if (state !== undefined) {
path = addQueryStringValueToPath(path, queryKey, key)
Expand All @@ -105,17 +97,17 @@ const updateLocation = (location, queryKey, updateHash, transformPath) => {
updateHash(path)
}

export const pushLocation = (location, queryKey, transformPath) =>
updateLocation(location, queryKey, (path) => {
export const pushLocation = (location, pathCoder, queryKey) =>
updateLocation(location, pathCoder, queryKey, (path) => {
if (getHashPath() !== path) {
pushHashPath(path)
} else {
warning(false, 'You cannot PUSH the same path using hash history')
}
}, transformPath)
})

export const replaceLocation = (location, queryKey, transformPath) =>
updateLocation(location, queryKey, (path) => {
export const replaceLocation = (location, pathCoder, queryKey) =>
updateLocation(location, pathCoder, queryKey, (path) => {
if (getHashPath() !== path)
replaceHashPath(path)
}, transformPath)
})
3 changes: 0 additions & 3 deletions modules/PathUtils.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import warning from 'warning'

export const isAbsolutePath = (path) =>
typeof path === 'string' && path.charAt(0) === '/'

export const addQueryStringValueToPath = (path, key, value) => {
const { pathname, search, hash } = parsePath(path)

Expand Down
18 changes: 3 additions & 15 deletions modules/__tests__/HashHistory-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import describeInitialLocation from './describeInitialLocation'
import describeTransitions from './describeTransitions'
import describePush from './describePush'
import describeReplace from './describeReplace'
import describeTransformPath from './describeTransformPath'
import describePathCoding from './describePathCoding'
import describePopState from './describePopState'
import describeQueryKey from './describeQueryKey'
import describeBasename from './describeBasename'
Expand All @@ -24,18 +24,6 @@ describe('hash history', () => {
expect(history.createHref('/a/path')).toEqual('#/a/path')
})

it('knows how to make hrefs with a custom transformPath function', () => {
const history = createHashHistory({
transformPath: (path, encode) => {
if (encode)
return path.indexOf('/prefix') !== 0 ? `/prefix${path}` : path

return path.substring(7)
}
})
expect(history.createHref('/a/path')).toEqual('#/prefix/a/path')
})

describeListen(createHashHistory)
describeInitialLocation(createHashHistory)
describeTransitions(createHashHistory)
Expand All @@ -53,14 +41,14 @@ describe('hash history', () => {
}

if (supportsHistory() && supportsGoWithoutReloadUsingHash()) {
describeTransformPath(createHashHistory)
describeGo(createHashHistory)
describeQueryKey(createHashHistory)
describePathCoding(createHashHistory)
} else {
describe.skip(null, () => {
describeTransformPath(createHashHistory)
describeGo(createHashHistory)
describeQueryKey(createHashHistory)
describePathCoding(createHashHistory)
})
}
})
11 changes: 0 additions & 11 deletions modules/__tests__/PathUtils-test.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,11 @@
import expect from 'expect'
import {
isAbsolutePath,
addQueryStringValueToPath,
getQueryStringValueFromPath,
stripQueryStringValueFromPath,
createPath
} from '../PathUtils'

describe('isAbsolutePath', () => {
it('returns true for absolute paths', () => {
expect(isAbsolutePath('/a/b/c')).toBe(true)
})

it('returns false for relative paths', () => {
expect(isAbsolutePath('a/b/c')).toBe(false)
})
})

describe('addQueryStringValueToPath', () => {
describe('when the path has no query string', () => {
it('creates a new query string', () => {
Expand Down
Loading

0 comments on commit 6be82b0

Please sign in to comment.