Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/hazae41/chemin

Create infinite virtual subpaths for your React webapp
https://github.com/hazae41/chemin

Last synced: about 2 months ago
JSON representation

Create infinite virtual subpaths for your React webapp

Awesome Lists containing this project

README

        

# Chemin

Create infinite virtual subpaths for your React webapp

```bash
npm i @hazae41/chemin
```

[**Node Package 📦**](https://www.npmjs.com/package/@hazae41/chemin)

## Features

### Current features
- 100% TypeScript and ESM
- No external dependencies
- Compatible with your framework
- Uses only web standards
- Infinite virtual subpaths
- Element coordinates to URL
- Search params to React state

## What?

This library allows you to create infinite virtual hash-based and search-based subpaths

e.g. `https://example.com/chat/#/settings/user?left=/tree&right=/page`

All the paths in this URL are easily routed and modified with React components

```
https://example.com/chat ┐
└ # = /settings/user ┐
├ left = /tree
└ right = /page
```

This allows creating dialogs, subdialogs, things on left and right, and many more

## Usage

### Hash-based path

You can use `HashPathProvider` to provide a hash-based path for your app

```tsx
import { HashPathProvider } from "@hazae41/chemin"

export function App() {
const [client, setClient] = useState(false)

useEffect(() => {
setClient(true)
}, [])

if (!client)
return null

return


}
```

e.g. `https://example.com/app/#/this/is/the/pathname`

```tsx
console.log(usePathContext().unwrap().url.pathname)
```

This will display `/this/is/the/pathname`

### Root-based path

You can also provide root-based path for your app

#### Use Next.js router

```tsx
import { useRouter } from "next/router"

export function NextPathProvider(props: { children?: ReactNode }) {
const router = useRouter()
const { children } = props

const [raw, setRaw] = useState()

useEffect(() => {
const onRouteChangeComplete = () => setRaw(location.href)

router.events.on("routeChangeComplete", onRouteChangeComplete)
return () => router.events.off("routeChangeComplete", onRouteChangeComplete)
}, [])

useEffect(() => {
const onHashChangeComplete = () => setRaw(location.href)

router.events.on("hashChangeComplete", onHashChangeComplete)
return () => router.events.off("hashChangeComplete", onHashChangeComplete)
}, [])

const get = useCallback(() => {
return new URL(location.href)
}, [raw])

const url = useMemo(() => {
return get()
}, [get])

const go = useCallback((hrefOrUrl: string | URL) => {
return new URL(hrefOrUrl, location.href)
}, [])

const handle = useMemo(() => {
return { url, get, go } satisfies PathHandle
}, [url, get, go])

return
{children}

}
```

```tsx
export function App() {
const [client, setClient] = useState(false)

useEffect(() => {
setClient(true)
}, [])

if (!client)
return null

return


}
```

#### Use Navigation API

This uses the modern [Navigation API](https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API) that only works on some browsers for now

```tsx
import { RootPathProvider } from "@hazae41/chemin"

export function App() {
const [client, setClient] = useState(false)

useEffect(() => {
setClient(true)
}, [])

if (!client)
return null

return


}
```

e.g. `https://example.com/this/is/the/pathname`

```tsx
console.log(usePathContext().unwrap().url.pathname)
```

This will display `/this/is/the/pathname`

You may need to disable client-side navigation from your framework

```tsx
declare const navigation: Nullable

export default function App({ Component, pageProps, router }: AppProps) {
useEffect(() => {
/**
* Disable Next.js client-side navigation
*/
removeEventListener("popstate", router.onPopState)
}, [router])

useEffect(() => {
/**
* Enable modern client-side navigation
*/
navigation?.addEventListener("navigate", (event: any) => event.intercept())
}, [])

return
}
```

And rewrite all URLs to a common one

```tsx
rewrites() {
return [
{
source: "/:path*",
destination: "/",
},
]
}
```

### Simple router

You can route things with `usePathContext()`

```tsx
import { usePathContext } from "@hazae41/chemin"

function Router() {
const path = usePathContext().unwrap()

if (path.url.pathname === "/home")
return

return
}
```

### Pattern router

You can route things with pattern-matching via regexes

```tsx
import { usePathContext } from "@hazae41/chemin"

function Router() {
const path = usePathContext().unwrap()

let matches: RegExpMatchArray | null

if ((matches = path.url.pathname.match(/^\/$/)))
return

if (matches = path.url.pathname.match(/^\/home(\/)?$/))
return

if (matches = path.url.pathname.match(/^\/home\/settings(\/)?$/))
return

if (matches = path.url.pathname.match(/^\/user\/([^\/]+)(\/)?$/))
return

return
}
```

### Inline router

You can also route things inside a component

```tsx
import { usePathContext } from "@hazae41/chemin"

function FunPage() {
const path = usePathContext().unwrap()

return <>
{path.url.pathname === "/help" &&
}
{path.url.pathname === "/send" &&
}


Have fun!

>
}
```

### Navigation

You can use anchors and buttons to declaratively and imperatively navigate

```tsx
import { usePathContext } from "@hazae41/chemin"

function LandingPage() {
const path = usePathContext().unwrap()

const onHelpClick = useCallback(() => {
location.replace(path.go("/help"))
}, [path])

return <>


Welcome!


Home


Help

>
}
```

### Hash-based subpath

You can create hash-based subroutes

e.g. `https://example.com/home/#/secret`

```tsx
import { usePathContext, useHashSubpath, HashSubpathProvider } from "@hazae41/chemin"

function HomePageSubrouter() {
const path = usePathContext().unwrap()

if (path.url.pathname === "/secret")
return

return null
}

function HomePage() {
const path = usePathContext().unwrap()
const hash = useHashSubpath(path)

const onSecretButtonClick = useCallback(() => {
location.replace(hash.go("/secret"))
}, [hash])

return <>




Hello world!


Secret anchor


Secret button

>
}
```

### Search-based subpath

You can create search-based subroutes

e.g. `https://example.com/home?left=/football&right=/baseball`

```tsx
import { usePathContext, useSearchSubpath, SearchSubpathProvider } from "@hazae41/chemin"

function PanelRouter() {
const path = usePathContext().unwrap()

if (path.url.pathname === "/football")
return

if (path.url.pathname === "/baseball")
return

return
}

function HomePage() {
const path = usePathContext().unwrap()

const left = useSearchSubpath(path, "left")
const right = useSearchSubpath(path, "right")

return <>


Hello world!


Show football on left


Show baseball on right









>
}
```

### Search-based value

You can also create search-based non-path values

```tsx
import { usePathContext, useSearchState } from "@hazae41/chemin"

function Page() {
const path = usePathContext().unwrap()

const user = useSearchValue(path, "user")

if (user.value === "root")
return <>Hello root!>

return
Login as root

}
```

### Search-based state

You can even create search-based non-path React state

```tsx
import { usePathContext, useSearchState } from "@hazae41/chemin"

function Page() {
const path = usePathContext().unwrap()

const [counter, setCounter] = useSearchState(path, "counter")

const onClick = useCallback(() => {
setCounter(previous => String(Number(previous) + 1))
}, [])

return
Add

}
```

### Coords

You can use `useCoords(path, url)` with an HTML element to pass the element's X-Y coordinates to the URL

```tsx
function Page() {
const path = usePathContext().unwrap()
const hash = useHashSubpath(path)

const settings = useCoords(hash, "/settings")

return <>

{hash.url.pathname === "/settings" &&

Settings
}


Open settings

>
}
```

Then you can consume those coordinates to add fancy animations and positioning

```tsx
function Dialog(props: ChildrenProps) {
const { children } = props

const path = usePathContext().unwrap()

const x = path.url.searchParams.get("x") || 0
const y = path.url.searchParams.get("x") || 0

return



{children}


}
```

Hint: `x` is `element.clientX` and `y` is `element.clientY`

You can also navigate on right-click using `onContextMenu`

```tsx
function Page() {
const path = usePathContext().unwrap()
const hash = useHashSubpath(path)

const menu = useCoords(hash, "/menu")

return <>

{hash.url.pathname === "/menu" &&

Share
}


Right-click me

>
}
```

### Closeful

All providers of `PathContext` are also providers of [`CloseContext`](https://github.com/hazae41/react-close-context)

You can use the provided `CloseContext` to go back to the root of the current path

e.g. `https://example.com/home/#/aaa/bbb/ccc` -> `https://example.com/home`

e.g. `https://example.com/home/#/aaa/#/bbb/#/ccc` -> `https://example.com/home/#/aaa/#/bbb`

You can consume `CloseContext` in any component with a `close()` feature

```tsx
import { useCloseContext } from "@hazae41/chemin"

function Dialog(props: ChildrenProps) {
const { children } = props

const close = useCloseContext().unwrap()

const onClose = useCallback(() => {
close()
}, [close])

return



Close


{children}


}
```