Skip to content

Commit

Permalink
fix: in-house history
Browse files Browse the repository at this point in the history
  • Loading branch information
tannerlinsley committed Jan 18, 2023
1 parent bdb0003 commit e0bb22f
Show file tree
Hide file tree
Showing 4 changed files with 275 additions and 41 deletions.
4 changes: 1 addition & 3 deletions packages/router-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,8 @@
"dependencies": {
"@babel/runtime": "^7.16.7",
"@solidjs/reactivity": "^0.0.7",
"history": "^5.2.0",
"immer": "^9.0.15",
"tiny-invariant": "^1.3.1",
"zustand": "^4.3.2"
"tiny-invariant": "^1.3.1"
},
"devDependencies": {
"babel-plugin-transform-async-to-promises": "^0.8.18"
Expand Down
199 changes: 199 additions & 0 deletions packages/router-core/src/history.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
// While the public API was clearly inspired by the "history" npm package,
// This implementation attempts to be more lightweight by
// making assumptions about the way TanStack Router works

export interface RouterHistory {
location: RouterLocation
listen: (cb: () => void) => () => void
push: (path: string, state: any) => void
replace: (path: string, state: any) => void
go: (index: number) => void
back: () => void
forward: () => void
}

export interface ParsedPath {
href: string
pathname: string
search: string
hash: string
}

export interface RouterLocation extends ParsedPath {
state: any
}

const popStateEvent = 'popstate'

function createHistory(opts: {
getLocation: () => RouterLocation
listener: (onUpdate: () => void) => () => void
pushState: (path: string, state: any) => void
replaceState: (path: string, state: any) => void
go: (n: number) => void
back: () => void
forward: () => void
}): RouterHistory {
let currentLocation = opts.getLocation()
let unsub = () => {}
let listeners = new Set<() => void>()

const onUpdate = () => {
currentLocation = opts.getLocation()

listeners.forEach((listener) => listener())
}

return {
get location() {
return currentLocation
},
listen: (cb: () => void) => {
if (listeners.size === 0) {
unsub = opts.listener(onUpdate)
}
listeners.add(cb)

return () => {
listeners.delete(cb)
if (listeners.size === 0) {
unsub()
}
}
},
push: (path: string, state: any) => {
opts.pushState(path, state)
onUpdate()
},
replace: (path: string, state: any) => {
opts.replaceState(path, state)
onUpdate()
},
go: (index) => {
opts.go(index)
onUpdate()
},
back: () => {
opts.back()
onUpdate()
},
forward: () => {
opts.forward()
onUpdate()
},
}
}

export function createBrowserHistory(opts?: {
getHref?: () => string
createHref?: (path: string) => string
}): RouterHistory {
const getHref =
opts?.getHref ??
(() =>
`${window.location.pathname}${window.location.hash}${window.location.search}`)
const createHref = opts?.createHref ?? ((path) => path)
const getLocation = () => parseLocation(getHref(), history.state)

return createHistory({
getLocation,
listener: (onUpdate) => {
window.addEventListener(popStateEvent, onUpdate)
return () => {
window.removeEventListener(popStateEvent, onUpdate)
}
},
pushState: (path, state) => {
window.history.pushState(
{ ...state, key: createRandomKey() },
'',
createHref(path),
)
},
replaceState: (path, state) => {
window.history.replaceState(
{ ...state, key: createRandomKey() },
'',
createHref(path),
)
},
back: () => window.history.back(),
forward: () => window.history.forward(),
go: (n) => window.history.go(n),
})
}

export function createHashHistory(): RouterHistory {
return createBrowserHistory({
getHref: () => window.location.hash.substring(1),
createHref: (path) => `#${path}`,
})
}

export function createMemoryHistory(
opts: {
initialEntries: string[]
initialIndex?: number
} = {
initialEntries: ['/'],
},
): RouterHistory {
const entries = opts.initialEntries
let index = opts.initialIndex ?? entries.length - 1
let currentState = {}

const getLocation = () => parseLocation(entries[index]!, currentState)

return createHistory({
getLocation,
listener: (onUpdate) => {
window.addEventListener(popStateEvent, onUpdate)
// We might need to handle the hashchange event in the future
// window.addEventListener(hashChangeEvent, onUpdate)
return () => {
window.removeEventListener(popStateEvent, onUpdate)
}
},
pushState: (path, state) => {
currentState = {
...state,
key: createRandomKey(),
}
entries.push(path)
index++
},
replaceState: (path, state) => {
currentState = {
...state,
key: createRandomKey(),
}
entries[index] = path
},
back: () => {
index--
},
forward: () => {
index = Math.min(index + 1, entries.length - 1)
},
go: (n) => window.history.go(n),
})
}

function parseLocation(href: string, state: any): RouterLocation {
let hashIndex = href.indexOf('#')
let searchIndex = href.indexOf('?')
const pathEnd = Math.min(hashIndex, searchIndex)

return {
href,
pathname: pathEnd > -1 ? href.substring(0, pathEnd) : href,
hash: hashIndex > -1 ? href.substring(hashIndex, searchIndex) : '',
search: searchIndex > -1 ? href.substring(searchIndex) : '',
state,
}
}

// Thanks co-pilot!
function createRandomKey() {
return (Math.random() + 1).toString(36).substring(7)
}
1 change: 1 addition & 0 deletions packages/router-core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { default as invariant } from 'tiny-invariant'
export * from './history'

export * from './frameworks'
export * from './link'
Expand Down
Loading

0 comments on commit e0bb22f

Please sign in to comment.