https://github.com/hazae41/chemin
Create infinite virtual subpaths for your React webapp
https://github.com/hazae41/chemin
Last synced: over 1 year ago
JSON representation
Create infinite virtual subpaths for your React webapp
- Host: GitHub
- URL: https://github.com/hazae41/chemin
- Owner: hazae41
- Created: 2024-07-20T05:40:53.000Z (about 2 years ago)
- Default Branch: main
- Last Pushed: 2024-12-07T22:55:43.000Z (over 1 year ago)
- Last Synced: 2025-04-15T13:51:51.917Z (over 1 year ago)
- Language: TypeScript
- Homepage:
- Size: 115 KB
- Stars: 3
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Funding: .github/FUNDING.yml
Awesome Lists containing this project
- awesome-ccamel - hazae41/chemin - Create infinite virtual subpaths for your React webapp (TypeScript)
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}
}
```