Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
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
- Host: GitHub
- URL: https://github.com/hazae41/chemin
- Owner: hazae41
- Created: 2024-07-20T05:40:53.000Z (5 months ago)
- Default Branch: main
- Last Pushed: 2024-08-30T03:48:41.000Z (3 months ago)
- Last Synced: 2024-10-07T21:36:12.323Z (2 months ago)
- Language: TypeScript
- Homepage:
- Size: 92.8 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 nullreturn
}
```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 } = propsconst [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 nullreturn
}
```#### 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 nullreturn
}
```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: Nullableexport 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")
returnreturn
}
```### 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(/^\/$/)))
returnif (matches = path.url.pathname.match(/^\/home(\/)?$/))
returnif (matches = path.url.pathname.match(/^\/home\/settings(\/)?$/))
returnif (matches = path.url.pathname.match(/^\/user\/([^\/]+)(\/)?$/))
returnreturn
}
```### 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")
returnreturn 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")
returnif (path.url.pathname === "/baseball")
returnreturn
}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 } = propsconst path = usePathContext().unwrap()
const x = path.url.searchParams.get("x") || 0
const y = path.url.searchParams.get("x") || 0return
{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 } = propsconst close = useCloseContext().unwrap()
const onClose = useCallback(() => {
close()
}, [close])
return
Close
{children}
}
```