Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update navigation and implement onLocationUpdate logic #43

Merged
merged 4 commits into from
Jan 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 17 additions & 27 deletions src/utilities/createRouter.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import { readonly } from 'vue'
import { Resolved, Route, RouteMethods, Routes } from '@/types'
import { createRouteMethods, createRouterNavigation, resolveRoutes } from '@/utilities'
import { createRouteMethods, createRouterNavigation, resolveRoutes, routeMatch } from '@/utilities'
import { resolveRoutesRegex } from '@/utilities/resolveRoutesRegex'
import { routeMatch } from '@/utilities/routeMatch'
import { updateBrowserUrl } from '@/utilities/updateBrowserUrl'

type RouterPush = (url: string, options?: { replace: boolean }) => Promise<void>
type RouterReplace = (url: string) => Promise<void>
type RouterBackForward = () => Promise<void>
type RouterGo = (delta: number) => Promise<void>

export type Router<
TRoutes extends Routes
Expand All @@ -17,53 +13,47 @@ export type Router<
route: Readonly<Resolved<Route>>,
push: RouterPush,
replace: RouterReplace,
back: RouterBackForward,
forward: RouterBackForward,
go: RouterGo,
back: () => void,
forward: () => void,
go: (delta: number) => void,
}

export function createRouter<T extends Routes>(routes: T): Router<T> {
const resolved = resolveRoutes(routes)
const resolvedWithRegex = resolveRoutesRegex(resolved)
const routerNavigation = createRouterNavigation()
const navigation = createRouterNavigation({
onLocationUpdate,
})

// todo: implement this
const route: Router<T>['route'] = readonly({} as any)

const push: RouterPush = async (url, options) => {
async function onLocationUpdate(url: string): Promise<void> {
const match = routeMatch(resolvedWithRegex, url)

if (!match) {
return updateBrowserUrl(url, options)
throw 'not implemented'
}

await routerNavigation.update(url, options)
}

const replace: RouterReplace = (url) => {
return push(url, { replace: true })
throw 'not implemented'
}

const forward: RouterBackForward = async () => {
await routerNavigation.forward()
}

const back: RouterBackForward = async () => {
await routerNavigation.back()
const push: RouterPush = async (url, options) => {
await navigation.update(url, options)
}

const go: RouterGo = async (delta) => {
await routerNavigation.go(delta)
const replace: RouterReplace = async (url) => {
await navigation.update(url, { replace: true })
}

const router = {
routes: createRouteMethods<T>(resolved),
route,
push,
replace,
forward,
back,
go,
forward: navigation.forward,
back: navigation.back,
go: navigation.go,
}

return router
Expand Down
38 changes: 33 additions & 5 deletions src/utilities/routerNavigation.browser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ describe('createRouterNavigation', () => {
test('when go is called, forwards call to window history', () => {
vi.spyOn(window.history, 'go')

const onLocationUpdate = vi.fn()
const delta = random.number({ min: 0, max: 100 })
const history = createRouterNavigation()
const history = createRouterNavigation({ onLocationUpdate })

history.go(delta)

Expand All @@ -18,7 +19,8 @@ describe('createRouterNavigation', () => {
test('when back is called, forwards call to window history', () => {
vi.spyOn(window.history, 'back')

const history = createRouterNavigation()
const onLocationUpdate = vi.fn()
const history = createRouterNavigation({ onLocationUpdate })

history.back()

Expand All @@ -28,7 +30,8 @@ describe('createRouterNavigation', () => {
test('when forward is called, forwards call to window history', () => {
vi.spyOn(window.history, 'forward')

const history = createRouterNavigation()
const onLocationUpdate = vi.fn()
const history = createRouterNavigation({ onLocationUpdate })

history.forward()

Expand All @@ -38,11 +41,36 @@ describe('createRouterNavigation', () => {
test('when update is called, calls updateBrowserUrl', () => {
vi.spyOn(utilities, 'updateBrowserUrl')

const onLocationUpdate = vi.fn()
const url = random.number().toString()
const history = createRouterNavigation()
const history = createRouterNavigation({ onLocationUpdate })

history.update(url)

expect(utilities.updateBrowserUrl).toHaveBeenCalledWith(url)
expect(utilities.updateBrowserUrl).toHaveBeenCalledWith(url, undefined)
})

test('when update is called and same origin calls onLocationUpdate', async () => {
vi.spyOn(utilities, 'isSameOrigin').mockReturnValue(true)

const onLocationUpdate = vi.fn()
const url = random.number().toString()
const history = createRouterNavigation({ onLocationUpdate })

await history.update(url)

expect(onLocationUpdate).toHaveBeenCalledWith(url)
})

test('when update is called and not same origin does not call onLocationUpdate ', () => {
const onLocationUpdate = vi.fn()
vi.spyOn(utilities, 'isSameOrigin').mockReturnValue(true)

const url = random.number().toString()
const history = createRouterNavigation({ onLocationUpdate })

history.update(url)

expect(onLocationUpdate).not.toHaveBeenCalled()
})
})
22 changes: 19 additions & 3 deletions src/utilities/routerNavigation.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
import { describe, expect, test } from 'vitest'
import { describe, expect, test, vi } from 'vitest'
import { createRouterNavigation } from '@/utilities/routerNavigation'
import { random } from '@/utilities/testHelpers'

describe('createRouterNavigation', () => {
test('is not implemented, and throws exception', () => {
expect(() => createRouterNavigation()).toThrowError('not implemented')
test('Browser like navigation is not supported', () => {
const onLocationUpdate = vi.fn()
const navigation = createRouterNavigation({ onLocationUpdate })

expect(() => navigation.back()).toThrowError()
expect(() => navigation.forward()).toThrowError()
expect(() => navigation.go(1)).toThrowError()
})

test('when update is called and same origin calls onLocationUpdate', () => {
const onLocationUpdate = vi.fn()
const url = random.number().toString()
const history = createRouterNavigation({ onLocationUpdate })

history.update(url)

expect(onLocationUpdate).toHaveBeenCalledWith(url)
})
})
77 changes: 63 additions & 14 deletions src/utilities/routerNavigation.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,79 @@
import { isBrowser } from '@/utilities/isBrowser'
import { updateBrowserUrl } from '@/utilities/updateBrowserUrl'

type RouterNavigationOptions = {
onLocationUpdate: (url: string) => Promise<void>,
}

type RouterNavigationUpdateOptions = {
replace?: boolean,
}

type NavigationForward = () => void
type NavigationBack = () => void
type NavigationGo = (delta: number) => void
type NavigationUpdate = (url: string, options?: RouterNavigationUpdateOptions) => Promise<void>
type NavigationCleanup = () => void

type RouterNavigation = {
forward: () => void,
back: () => void,
go: (delta: number) => void,
update: (url: string, options?: RouterNavigationUpdateOptions) => void,
forward: NavigationForward,
back: NavigationBack,
go: NavigationGo,
update: NavigationUpdate,
cleanup?: () => void,
}

export function createRouterNavigation(): RouterNavigation {
return isBrowser() ? createWebNavigation() : createMemoryNavigation()
export function createRouterNavigation(options: RouterNavigationOptions): RouterNavigation {
if (isBrowser()) {
return createBrowserNavigation(options)
}

return createNodeNavigation(options)
}

function createWebNavigation(): RouterNavigation {
function createBrowserNavigation({ onLocationUpdate }: RouterNavigationOptions): RouterNavigation {

const update: NavigationUpdate = async (url, options) => {
await updateBrowserUrl(url, options)

return await onLocationUpdate(url)
}

const cleanup: NavigationCleanup = () => {
removeEventListener('popstate', onPopstate)
}

const onPopstate = (): void => {
onLocationUpdate(window.location.toString())
}

addEventListener('popstate', onPopstate)

return {
go: window.history.go,
back: window.history.back,
forward: window.history.forward,
update: updateBrowserUrl,
forward: history.forward,
back: history.back,
go: history.go,
update,
cleanup,
}
}

function createMemoryNavigation(): RouterNavigation {
throw 'not implemented'
}
function createNodeNavigation({ onLocationUpdate }: RouterNavigationOptions): RouterNavigation {
const notSupported = (): void => {
throw new Error('Browser like navigation is not supported outside of a browser context')
}

const update: NavigationUpdate = async (url) => {
return await onLocationUpdate(url)
}

const cleanup: NavigationCleanup = () => {}

return {
forward: notSupported,
back: notSupported,
go: notSupported,
cleanup,
update,
}
}
6 changes: 0 additions & 6 deletions src/utilities/updateBrowserUrl.spec.ts
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deleted this test because we decided updateBrowserUrl shouldn't even get called if isBrowser() is false.

This file was deleted.

20 changes: 10 additions & 10 deletions src/utilities/updateBrowserUrl.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { isBrowser } from '@/utilities/isBrowser'

type UpdateBrowserUrlOptions = {
replace?: boolean,
}

export function updateBrowserUrl(url: string, options: UpdateBrowserUrlOptions = {}): void {
if (!isBrowser()) {
return
}

export function updateBrowserUrl(url: string, options: UpdateBrowserUrlOptions = {}): Promise<void> {
if (isSameOrigin(url)) {
return updateHistory(url, options)
return new Promise(resolve => {
updateHistory(url, options)
resolve()
})
}

return updateWindow(url, options)
// intentionally never resolves because we want the router to just stall until window.location takes over
return new Promise(() => {
updateWindow(url, options)
})
}

function updateHistory(url: string, options: UpdateBrowserUrlOptions): void {
Expand All @@ -32,7 +32,7 @@ function updateWindow(url: string, options: UpdateBrowserUrlOptions): void {
return window.location.assign(url)
}

function isSameOrigin(url: string): boolean {
export function isSameOrigin(url: string): boolean {
const { origin } = new URL(url, window.location.origin)

return origin === window.location.origin
Expand Down