From 7c0bfecc6f573a918e9cf8a0e44c504dae458bef Mon Sep 17 00:00:00 2001
From: Matthias Giger
Date: Sun, 11 Aug 2024 10:26:34 +0200
Subject: [PATCH] feat(router): refactor interface to allow imports and track
parameters
release-npm
BREAKING CHANGE: Interface retains same features but has been overhauled and needs migration, see updated documentation.
---
README.md | 97 ++++++++++-----------
demo/configuration.ts | 3 +
demo/index.tsx | 25 +++---
demo/package.json | 4 +-
demo/preact/index.tsx | 22 ++---
demo/preact/package.json | 4 +-
index.tsx | 138 ++++++++++++++++++-----------
test/epic/router.test.tsx | 148 ++++++++++++++++---------------
test/preact/router.test.tsx | 169 ++++++++++++++++++------------------
types.ts | 10 ++-
10 files changed, 330 insertions(+), 290 deletions(-)
diff --git a/README.md b/README.md
index eb3bd03..e26e455 100644
--- a/README.md
+++ b/README.md
@@ -13,36 +13,32 @@ Router for the Web.
## Installation
```sh
-npm install epic-router
+bun install epic-router
# Install a JSX rendering framework.
-npm install preact
+bun install epic-jsx / bun install preact
```
## Usage
```jsx
-import React from 'react'
-import { createRoot } from 'react-dom/client'
-import { Router, Page } from 'epic-router'
+import { Page, addPage, back, configure, go, forward } from 'epic-router'
+import { connect } from 'epic-state/connect'
+import { render } from 'epic-jsx'
+
+const { router } = configure<{ id: number }>('overview', undefined, undefined, connect)
// Declare some components used as pages.
const Overview = () =>
Router.go('nested/about')}>About
Router.go('article', { id: 2 })}>Article 2
@@ -66,36 +62,23 @@ Use the `
` component anywhere in your layout to display the current page
## Router
-```js
-import { Router } from 'epic-router'
-```
-
-The `Router`-Store can be accessed from anywhere to access, configure and modify the state of the Router.
-
```ts
-Router.setPages(pages: { [key: string]: React.ReactNode }, initialRoute: string)
+import { configure, addPage, go, back, forward, initial } from 'epic-router'
+
+// Setup and configure the Router.
+const { router } = configure(initialRoute?: string, homeRoute?: string, initialParameters?: Parameters, connect?: typeof preactConnect)
+// Register a page for a route.
+addPage(route: string | number, component: JSX | ReactNode)
+// Navigates to a route. Parameters will be added to the URL search query and together with the state (both optional) will be passed to the page component as props. If replace is true, `back()` will not lead to the previous page.
+go(route: string, parameters: object = {}, state: object = {}, replace = false)
+// go back one step in the history.
+back()
+// Go forward one step in the history.
+forward()
+// Go to the initial route.
+initial()
```
-Configure the route keys and their associated components, plus the route to be displayed initially.
-
-```ts
-Router.go(route: string, parameters: object = {}, state: object = {}, replace = false)
-```
-
-Navigates to a route. Parameters will be added to the URL search query and together with the state (both optional) will be passed to the page component as props. If replace is true, `back()` will not lead to the previous page.
-
-`Router.back()` go back one step in the history.
-
-`Router.forward()` go forward one step.
-
-`Router.initial()` go to the initial route.
-
-```ts
-addPage(route: string, component: React.ReactNode)
-```
-
-Add a single page after initialization. This can be useful when pages are loaded on request.
-
```ts
// Currently active route.
Router.route => string
@@ -114,13 +97,27 @@ Router.history => History
The `404` page can be set to show a custom error page when a route is not found.
```tsx
-import { Router } from 'epic-router'
+import { addPage } from 'epic-router'
const Custom404 = () =>
Page Not Found!
-Router.setPages({
- 404: Custom404,
-})
+addPage(404: Custom404)
+```
+
+### Parameters
+
+Parameters will automatically be added to the URL and are accessible in different ways as shown below.
+
+```tsx
+import { configure, getRouter, type WithRouter } from 'epic-router'
+
+type Parameters = { id: number }
+
+const { router } = configure
(...)
+
+const Article = () => Article: {router.parameters.id}
+const ArticleGlobal = () => Article: {getRouter().parameters.id}
+const ArticleProps = ({ router }: WithRouter) => Article: {router.parameters.id}
```
## Notes
diff --git a/demo/configuration.ts b/demo/configuration.ts
index 06e3833..32d24d8 100644
--- a/demo/configuration.ts
+++ b/demo/configuration.ts
@@ -7,6 +7,9 @@ export const rsbuild = defineConfig({
entry: {
index: './index.tsx',
},
+ define: {
+ 'process.env.PUBLIC_URL': '"/"',
+ },
},
html: {
title: 'epic-router Demo with epic-jsx',
diff --git a/demo/index.tsx b/demo/index.tsx
index 51f6d0b..bea53b6 100644
--- a/demo/index.tsx
+++ b/demo/index.tsx
@@ -1,25 +1,24 @@
-import { Page, back, create, forward, go } from 'epic-router'
+import { Page, addPage, back, configure, go, forward } from 'epic-router'
import { connect } from 'epic-state/connect'
import { Exmpl } from 'exmpl'
import { render } from 'epic-jsx'
+// TODO not working with globally registered plugin.
+// plugin(connect)
+
+const { router } = configure<{ id: number }>('overview', undefined, undefined, connect)
+
const Overview = () => Overview
const About = () => About
-const Article = ({ id }: { id: string }) => Article: {id}
+const Article = () => Article: {router.parameters.id}
const Nested = () => Nested
const Custom404 = () => Page not found!
-create(
- {
- overview: Overview,
- about: About,
- article: Article,
- 'nested/overview': Nested,
- 404: Custom404,
- },
- 'overview',
- connect,
-)
+addPage('overview', Overview)
+addPage('about', About)
+addPage('article', Article)
+addPage('nested/overview', Nested)
+addPage('404', Custom404)
const Button = ({ text, onClick }) => (
('overview', undefined, undefined, connect)
+
const Overview = () => Overview
const About = () => About
-const Article = ({ id }: { id: string }) => Article: {id}
+const Article = () => Article: {router.parameters.id}
const Nested = () => Nested
const Custom404 = () => Page not found!
-create(
- {
- overview: Overview,
- about: About,
- article: Article,
- 'nested/overview': Nested,
- 404: Custom404,
- },
- 'overview',
- connect,
-)
+addPage('overview', Overview)
+addPage('about', About)
+addPage('article', Article)
+addPage('nested/overview', Nested)
+addPage('404', Custom404)
const Button = ({ text, onClick }) => (
= {} as RouterState
+const pages: Pages = {}
const createHistory = () => {
if (typeof window !== 'undefined' && process.env.NODE_ENV !== 'test') {
@@ -18,7 +19,9 @@ const createHistory = () => {
}
export const history = createHistory()
-export const getRouter = () => Router
+export const getRouter = () => router
+
+export type WithRouter = { router: { route: string; parameters: T } }
const removeLeadingSlash = (path: string) => path.replace(/^\/*/, '')
@@ -55,101 +58,134 @@ function pathnameToRoute(location = history.location) {
name = removeLeadingSlash(name.replace(publicUrl, ''))
}
- if (name === '' && Router && Router.initialRoute) {
- return Router.initialRoute
+ if (name === '' && router && router.initialRoute) {
+ return router.initialRoute
}
return name !== '' ? name : undefined
}
-function getSearchParameters(location = history.location) {
+function getSearchParameters(location = history.location) {
const { search } = location
if (!search || search.length === 0) {
- return {}
+ return {} as T
}
- return queryString.parse(search)
+ return queryString.parse(search) as T
}
-function writePath(path: string) {
+function writePath(route: string) {
const publicUrl = removeLeadingSlash(process.env.PUBLIC_URL ?? '')
+ if (route === getHomeRoute()) {
+ // biome-ignore lint/style/noParameterAssign: Much easier in this case.
+ route = '/'
+ }
+
if (publicUrl) {
- return join('/', publicUrl, path)
+ return join('/', publicUrl, route)
}
// join will not work properly in this case.
- if (path === '') {
+ if (route === '') {
return '/'
}
- return join('/', path)
+ return join('/', route)
}
-export function create(pages: { [key: string]: PageComponent }, initialRoute?: string, connect?: typeof preactConnect) {
- if (!pages || Object.keys(pages).length === 0) {
- // biome-ignore lint/suspicious/noConsoleLog: Validation error for user.
- console.log('Invalid pages argument provided to create().')
- return {}
- }
-
- Router = state({
+const getInitialRoute = () => (router.initialRoute || Object.keys(pages)[0]) ?? ''
+const getHomeRoute = () => (router.homeRoute || Object.keys(pages)[0]) ?? ''
+
+export function configure(
+ initialRoute?: string,
+ homeRoute?: string,
+ initialParameters?: T,
+ connect?: typeof preactConnect,
+) {
+ router = state>({
+ // Configuration.
+ initialRoute, // First rendered if URL empty.
+ homeRoute: homeRoute ?? initialRoute, // Home route where URL === '/'.
// State
- initialRoute: initialRoute ?? Object.keys(pages)[0] ?? '', // Use the first page as the initial route if not provided.
- pages,
route: pathnameToRoute() ?? initialRoute ?? '',
- parameters: getSearchParameters(),
+ parameters: initialParameters ?? getSearchParameters(),
// Plugins, connect state to React.
plugin: connect,
// Retrieve current state from history, was private.
listener({ location }) {
- Router.parameters = Object.assign(getSearchParameters(location), location.state ?? {})
- Router.route = pathnameToRoute(location) ?? ''
+ // TODO can this lead to unnecessary rerenders?
+ router.parameters = Object.assign(getSearchParameters(location), location.state ?? {})
+ router.route = pathnameToRoute(location) ?? ''
},
// Derivations
get page() {
- if (process.env.NODE_ENV !== 'production' && (!Router.pages || Router.initialRoute === undefined)) {
+ if (process.env.NODE_ENV !== 'production' && !getInitialRoute()) {
return ErrorPage(
- No pages
or initialRoute
configured, configure with Router.setPages(pages, initialRoute)
.
+ No pages
or initialRoute
configured, configure with router.setPages(pages, initialRoute)
.
,
)
}
- if (Router.route === '') {
- return Router.pages[Router.initialRoute] as PageComponent
+ if (router.route === '') {
+ return pages[getInitialRoute()] as PageComponent
}
- if (!Router.pages[Router.route]) {
- const userErrorPage = Router.pages['404']
+ if (!pages[router.route]) {
+ const userErrorPage = pages['404']
if (typeof userErrorPage !== 'undefined') {
- return Router.pages['404'] as PageComponent
+ return pages['404'] as PageComponent
}
return ErrorPage(
process.env.NODE_ENV === 'production' ? (
Page not found!
) : (
- Route /{Router.route}
has no associated page!
+ Route /{router.route}
has no associated page!
),
)
}
- return Router.pages[Router.route] as PageComponent
+ return pages[router.route] as PageComponent
},
})
- const removeListener = history.listen(Router.listener)
+ const removeListener = history.listen(router.listener)
+
+ return { router: router as RouterState, removeListener }
+}
+
+export function addPage(name: string, markup: PageComponent) {
+ if (!name || typeof name !== 'string') {
+ // biome-ignore lint/suspicious/noConsoleLog: Validation error for user.
+ console.log('Invalid page name provided to addPage(name: string, markup: JSX).')
+ return
+ }
+
+ pages[name] = markup
- return { Router, removeListener }
+ // Use the first page as the initial route if not provided.
+ if (!router.initialRoute) {
+ router.initialRoute = name
+ }
}
-export function go(route: string, parameters = {}, historyState: object = {}, replace = false) {
- Router.route = route
- Router.parameters = parameters
+export function go(route: string, parameters?: Parameters, historyState: object = {}, replace = false) {
+ router.route = route
+
+ // NOTE Currently still necessary with epic-state to ensure changes are tracked.
+ if (parameters) {
+ Object.assign(router.parameters, parameters)
+ } else {
+ for (const key of Object.keys(router.parameters)) {
+ delete router.parameters[key]
+ }
+ }
- const searchParameters = Object.keys(parameters).length ? `?${queryString.stringify(parameters)}` : ''
+ const hasParameters = Object.keys(router.parameters).length
+ const searchParameters = hasParameters ? `?${queryString.stringify(router.parameters)}` : ''
- if (route === Router.initialRoute && !Object.keys(parameters).length) {
+ if (route === router.initialRoute && !hasParameters) {
// biome-ignore lint/style/noParameterAssign: Existing logic, might be improved.
route = ''
}
@@ -175,24 +211,26 @@ export function forward() {
}
export function initial() {
- Router.route = Router.initialRoute
- history.push(writePath(Router.route))
+ router.route = getInitialRoute()
+ history.push(writePath(router.route))
}
export function reset() {
- Router.pages = {}
- Router.initialRoute = ''
- Router.route = ''
+ for (const key of Object.keys(pages)) {
+ delete pages[key]
+ }
+ router.initialRoute = ''
+ router.route = ''
}
export function route() {
- return Router.route
+ return router.route
}
-export function addPage(route: string, component: PageComponent) {
- Router.pages[route] = component
+export function parameters() {
+ return router.parameters
}
export function Page(props: ComponentPropsWithoutRef<'div'>): JSX.Element {
- return
+ return
}
diff --git a/test/epic/router.test.tsx b/test/epic/router.test.tsx
index bb9fc24..9c0b834 100644
--- a/test/epic/router.test.tsx
+++ b/test/epic/router.test.tsx
@@ -1,98 +1,108 @@
-/* eslint-disable react/react-in-jsx-scope */
import '../setup-dom'
import { expect, mock, test } from 'bun:test'
import { render, serializeElement } from 'epic-jsx/test'
import { batch, plugin } from 'epic-state'
import { connect } from 'epic-state/connect'
// Import from local folder to avoid using entry tsconfig (different JSX configrations required).
-import { Page, back, create, go, history, reset } from './source/index'
+import { Page, type WithRouter, addPage, back, configure, go, history, reset } from './source/index'
// Clean up rendered content from other suite.
document.body.innerHTML = ''
plugin(connect)
-const Overview = () => Overview
-const { serialized: OverviewMarkup } = render( )
-function About() {
- return About
-}
-const { serialized: AboutMarkup } = render( )
-const Article = ({ id }: { id: string }) => Article: {id}
-const Custom404 = () => Page not found
-const { serialized: Custom404Markup } = render( )
-const { serialized: Article5Markup } = render( )
-const FragmentPage = (name: string, count: number) => (
- <>
- {name}
- {count}
- >
-)
-
-const { Router } = create(
- {
- overview: Overview,
- about: About,
- article: Article,
- static: Hello
,
- fragment: FragmentPage,
- 404: Custom404,
- },
- 'overview',
-)
-
-const page = render( )
-
-test('Intial page is rendered without interaction.', () => {
+test('Sets up and runs the router.', () => {
+ const { router } = configure<{ id: number }>('overview')
+
+ const Overview = () => Overview
+ function About() {
+ return About
+ }
+ const Custom404 = () => Page not found
+ const Article = ({ router }: WithRouter<{ id: number }>) => Article: {router.parameters.id}
+ const ArticleRouterProps = () => Article: {router.parameters.id}
+ const FragmentPage = (name: string, count: number) => (
+ <>
+ {name}
+ {count}
+ >
+ )
+
+ // @ts-expect-error Parameter inference
+ const idParameter: string = router.parameters.id
+ // @ts-expect-error Parameter inference
+ const missingParameter: string | number = router.parameters.missing
+ expect(idParameter).toBeUndefined()
+ expect(missingParameter).toBeUndefined()
+
+ addPage('overview', Overview)
+ addPage('about', About)
+ addPage('article', Article)
+ addPage('article-props', ArticleRouterProps)
+ addPage('fragment', FragmentPage)
+ addPage('static', Hello
)
+ addPage('404', Custom404)
+
+ const OverviewMarkup = render( ).serialized
+ const AboutMarkup = render( ).serialized
+ const Custom404Markup = render( ).serialized
+ const ArticleIdMarkup = {
+ 5: render(Article: 5
).serialized,
+ 6: render(Article: 6
).serialized,
+ 7: render(Article: 7
).serialized,
+ 8: render(Article: 8
).serialized,
+ }
+
+ const page = render( )
+
expect(page.serialized).toEqual(OverviewMarkup)
- expect(Router.route).toBe('overview')
+ expect(router.route).toBe('overview')
expect(history.location.pathname).toEqual('/')
-})
-
-test('go: Switches to page.', () => {
- go('about')
- batch()
- expect(serializeElement()).toEqual(AboutMarkup)
- expect(Router.route).toBe('about')
- expect(history.location.pathname).toEqual('/about')
-})
-test('go: Can rerender already rendered page.', () => {
- // NOTE this used to require a workaround as double rendering lead to an error.
go('about')
batch()
expect(serializeElement()).toEqual(AboutMarkup)
- expect(Router.route).toBe('about')
+ expect(router.route).toBe('about')
expect(history.location.pathname).toEqual('/about')
-})
-test('back: Goes back to the initial page.', () => {
back()
back()
batch()
expect(serializeElement()).toEqual(OverviewMarkup)
expect(history.location.pathname).toEqual('/')
-})
-test('go: Switches to page with parameters.', () => {
go('article', { id: 5 })
batch()
- expect(serializeElement()).toEqual(Article5Markup)
+ expect(serializeElement()).toEqual(ArticleIdMarkup[5])
expect(history.location.pathname).toEqual('/article')
expect(history.location.search).toEqual('?id=5')
-})
-test('go: Initial route is found on /.', () => {
- expect(Router.initialRoute).toEqual('overview')
- go(Router.initialRoute)
+ go('article', { id: 6 })
+ batch()
+ expect(serializeElement()).toEqual(ArticleIdMarkup[6])
+ expect(history.location.pathname).toEqual('/article')
+ expect(history.location.search).toEqual('?id=6')
+
+ go('article-props', { id: 7 })
+ batch()
+ expect(serializeElement()).toEqual(ArticleIdMarkup[7])
+ expect(history.location.pathname).toEqual('/article-props')
+ expect(history.location.search).toEqual('?id=7')
+
+ go('article-props', { id: 8 })
+ batch()
+ expect(serializeElement()).toEqual(ArticleIdMarkup[8])
+ expect(history.location.pathname).toEqual('/article-props')
+ expect(history.location.search).toEqual('?id=8')
+
+ expect(router.initialRoute).toEqual('overview')
+ go(router.initialRoute)
batch()
expect(serializeElement()).toEqual(OverviewMarkup)
- expect(Router.route).toEqual('overview')
+ expect(router.route).toEqual('overview')
expect(history.location.pathname).toEqual('/')
expect(history.location.search).toEqual('')
-})
-test('go: Missing route shows 404 fallback in page.', () => {
go('missing')
batch()
expect(serializeElement()).toEqual(Custom404Markup)
@@ -102,21 +112,15 @@ test('go: Missing route shows 404 fallback in page.', () => {
test('Props handed to Page can be accessed from pages.', () => {
reset()
- const errorMock = mock(() => 'whatt?')
+ const errorMock = mock(() => 'error, oh no!')
const Overview2 = () => Overview
- const ErrorPage = ({ onError }: { onError: (message: string) => string }) => sending an error {onError('whatt?')}
-
- const { serialized: Overview2Markup } = render( )
- const { serialized: ErrorMarkup } = render( value} />)
-
- create(
- {
- overview: Overview,
- error: ErrorPage,
- },
- 'overview',
- )
+ const ErrorPage = ({ onError }: { onError: (message: string) => string }) => sending an error {onError('error, oh no!')}
+ const Overview2Markup = render( ).serialized
+ const ErrorMarkup = render( value} />).serialized
+ configure('overview')
+ addPage('overview', Overview2)
+ addPage('error', ErrorPage)
go('overview')
expect(errorMock.mock.calls.length).toEqual(0)
diff --git a/test/preact/router.test.tsx b/test/preact/router.test.tsx
index 6a6598c..8b85a4b 100644
--- a/test/preact/router.test.tsx
+++ b/test/preact/router.test.tsx
@@ -1,45 +1,15 @@
-/* eslint-disable react/react-in-jsx-scope */
import '../setup-dom'
import { expect, mock, test } from 'bun:test'
import { render } from '@testing-library/preact'
import { connect } from 'epic-state/preact'
// Import from local folder to avoid using entry tsconfig (different JSX configrations required).
-import { Page, back, create, go, history, reset } from './source/index'
+import { Page, type WithRouter, addPage, back, configure, go, history, reset } from './source/index'
// Clean up rendered content from other suite.
document.body.innerHTML = ''
// TODO why doesn't globally connected preact plugin work?
-
-const Overview = () => Overview
-const { asFragment: OverviewMarkup } = render( )
-function About() {
- return About
-}
-const { asFragment: AboutMarkup } = render( )
-const Article = ({ id }: { id: string }) => Article: {id}
-const Custom404 = () => Page not found
-const { asFragment: Custom404Markup } = render( )
-const { asFragment: Article5Markup } = render( )
-const FragmentPage = (name: string, count: number) => (
- <>
- {name}
- {count}
- >
-)
-
-const { Router, removeListener } = create(
- {
- overview: Overview,
- about: About,
- article: Article,
- static: Hello
,
- fragment: FragmentPage,
- 404: Custom404,
- },
- 'overview',
- connect,
-)
+// plugin(connect)
const wait = (time = 1) =>
new Promise((done) => {
@@ -49,93 +19,124 @@ const wait = (time = 1) =>
const serializer = new XMLSerializer()
const serializeFragment = (asFragment: () => DocumentFragment) => serializer.serializeToString(asFragment())
-let page = render( )
+test('Sets up and runs the router.', async () => {
+ const { router } = configure<{ id: number }>('overview', undefined, undefined, connect)
+
+ const Overview = () => Overview
+ function About() {
+ return About
+ }
+ const Custom404 = () => Page not found
+ const Article = ({ router }: WithRouter<{ id: number }>) => Article: {router.parameters.id}
+ const ArticleRouterProps = () => Article: {router.parameters.id}
+ const FragmentPage = (name: string, count: number) => (
+ <>
+ {name}
+ {count}
+ >
+ )
-test('Intial page is rendered without interaction.', () => {
- expect(serializeFragment(page.asFragment)).toEqual(serializeFragment(OverviewMarkup))
- expect(Router.route).toBe('overview')
+ // @ts-expect-error Parameter inference
+ const idParameter: string = router.parameters.id
+ // @ts-expect-error Parameter inference
+ const missingParameter: string | number = router.parameters.missing
+ expect(idParameter).toBeUndefined()
+ expect(missingParameter).toBeUndefined()
+
+ addPage('overview', Overview)
+ addPage('about', About)
+ addPage('article', Article)
+ addPage('article-props', ArticleRouterProps)
+ addPage('fragment', FragmentPage)
+ addPage('static', Hello
)
+ addPage('404', Custom404)
+
+ const OverviewMarkup = serializeFragment(render( ).asFragment)
+ const AboutMarkup = serializeFragment(render( ).asFragment)
+ const Custom404Markup = serializeFragment(render( ).asFragment)
+ const ArticleIdMarkup = {
+ 5: serializeFragment(render(Article: 5
).asFragment),
+ 6: serializeFragment(render(Article: 6
).asFragment),
+ 7: serializeFragment(render(Article: 7
).asFragment),
+ 8: serializeFragment(render(Article: 8
).asFragment),
+ }
+
+ const page = render( )
+
+ expect(serializeFragment(page.asFragment)).toEqual(OverviewMarkup)
+ expect(router.route).toBe('overview')
expect(history.location.pathname).toEqual('/')
-})
-
-test('go: Switches to page.', async () => {
- go('about')
- await wait()
- expect(serializeFragment(page.asFragment)).toEqual(serializeFragment(AboutMarkup))
- expect(Router.route).toBe('about')
- expect(history.location.pathname).toEqual('/about')
-})
-test('go: Can rerender already rendered page.', async () => {
- // NOTE this used to require a workaround as double rendering lead to an error.
go('about')
await wait()
- expect(serializeFragment(page.asFragment)).toEqual(serializeFragment(AboutMarkup))
- expect(Router.route).toBe('about')
+ expect(serializeFragment(page.asFragment)).toEqual(AboutMarkup)
+ expect(router.route).toBe('about')
expect(history.location.pathname).toEqual('/about')
-})
-test('back: Goes back to the initial page.', async () => {
back()
back()
await wait()
- expect(serializeFragment(page.asFragment)).toEqual(serializeFragment(OverviewMarkup))
+ expect(serializeFragment(page.asFragment)).toEqual(OverviewMarkup)
expect(history.location.pathname).toEqual('/')
-})
-test('go: Switches to page with parameters.', async () => {
go('article', { id: 5 })
await wait()
- expect(serializeFragment(page.asFragment)).toEqual(serializeFragment(Article5Markup))
+ expect(serializeFragment(page.asFragment)).toEqual(ArticleIdMarkup[5])
expect(history.location.pathname).toEqual('/article')
expect(history.location.search).toEqual('?id=5')
-})
-test('go: Initial route is found on /.', async () => {
- expect(Router.initialRoute).toEqual('overview')
- go(Router.initialRoute)
+ go('article', { id: 6 })
+ await wait()
+ expect(serializeFragment(page.asFragment)).toEqual(ArticleIdMarkup[6])
+ expect(history.location.pathname).toEqual('/article')
+ expect(history.location.search).toEqual('?id=6')
+
+ go('article-props', { id: 7 })
+ await wait()
+ expect(serializeFragment(page.asFragment)).toEqual(ArticleIdMarkup[7])
+ expect(history.location.pathname).toEqual('/article-props')
+ expect(history.location.search).toEqual('?id=7')
+
+ go('article-props', { id: 8 })
+ await wait()
+ expect(serializeFragment(page.asFragment)).toEqual(ArticleIdMarkup[8])
+ expect(history.location.pathname).toEqual('/article-props')
+ expect(history.location.search).toEqual('?id=8')
+
+ expect(router.initialRoute).toEqual('overview')
+ go(router.initialRoute)
await wait()
- expect(serializeFragment(page.asFragment)).toEqual(serializeFragment(OverviewMarkup))
- expect(Router.route).toEqual('overview')
+ expect(serializeFragment(page.asFragment)).toEqual(OverviewMarkup)
+ expect(router.route).toEqual('overview')
expect(history.location.pathname).toEqual('/')
expect(history.location.search).toEqual('')
-})
-test('go: Missing route shows 404 fallback in page.', async () => {
go('missing')
await wait()
- expect(serializeFragment(page.asFragment)).toEqual(serializeFragment(Custom404Markup))
+ expect(serializeFragment(page.asFragment)).toEqual(Custom404Markup)
expect(history.location.pathname).toEqual('/missing')
})
test('Props handed to Page can be accessed from pages.', async () => {
- removeListener()
reset()
+ const errorMock = mock(() => 'error, oh no!')
const Overview2 = () => Overview
- const ErrorPage = ({ onError }: { onError?: (message: string) => string }) => sending an error {onError?.('whatt?')}
-
- const { asFragment: Overview2Markup } = render( )
- const { asFragment: ErrorMarkup } = render( value} />)
-
- const { Router } = create(
- {
- overview: Overview,
- error: ErrorPage,
- },
- 'overview',
- connect,
- )
+ const ErrorPage = ({ onError }: { onError: (message: string) => string }) => sending an error {onError?.('error, oh no!')}
+ const Overview2Markup = serializeFragment(render( ).asFragment)
+ const ErrorMarkup = serializeFragment(render( value} />).asFragment)
+ configure('overview', undefined, undefined, connect)
+ addPage('overview', Overview2)
+ addPage('error', ErrorPage)
go('overview')
- const errorMock = mock(() => 'whatt?')
expect(errorMock.mock.calls.length).toEqual(0)
- page = render( )
- expect(serializeFragment(page.asFragment)).toEqual(serializeFragment(Overview2Markup))
- expect(Router.route).toBe('overview')
+ const page = render( )
+ expect(serializeFragment(page.asFragment)).toEqual(Overview2Markup)
go('error')
await wait()
+ // NOTE if fails, skip the previous test, first connect still active...
+ expect(serializeFragment(page.asFragment)).toEqual(ErrorMarkup)
expect(errorMock.mock.calls.length).toEqual(1)
- expect(serializeFragment(page.asFragment)).toEqual(serializeFragment(ErrorMarkup))
- expect(Router.route).toBe('error')
})
diff --git a/types.ts b/types.ts
index 8e0b767..9cd8e76 100644
--- a/types.ts
+++ b/types.ts
@@ -4,12 +4,14 @@ import type { FC } from 'react'
// biome-ignore lint/suspicious/noExplicitAny: Generic react component.
export type PageComponent = FC
+export type Pages = { [key: string]: PageComponent }
+export type Parameters = Record
-export type RouterState = {
- initialRoute: string
- pages: { [key: string]: PageComponent }
+export type RouterState = {
+ initialRoute?: string
+ homeRoute?: string
route: string
- parameters: object
+ parameters: T
page: PageComponent
plugin?: typeof preactConnect
listener: Listener