npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@hazae41/chemin

v2.0.9

Published

Create infinite virtual subpaths for your React webapp

Downloads

103

Readme

Chemin

Create infinite virtual subpaths for your React webapp

npm i @hazae41/chemin

Node Package 📦

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

import { HashPathProvider } from "@hazae41/chemin"

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

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

  if (!client)
    return null

  return <HashPathProvider>
    <Router />
  </HashPathProvider>
}

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

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

import { useRouter } from "next/router"

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

  const [raw, setRaw] = useState<string>()

  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 <PathContext.Provider value={handle}>
    {children}
  </PathContext.Provider>
}
export function App() {
  const [client, setClient] = useState(false)

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

  if (!client)
    return null

  return <NextPathProvider>
    <Router />
  </NextPathProvider>
}

Use Navigation API

This uses the modern Navigation API that only works on some browsers for now

import { RootPathProvider } from "@hazae41/chemin"

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

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

  if (!client)
    return null

  return <RootPathProvider>
    <Router />
  </RootPathProvider>
}

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

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

This will display /this/is/the/pathname

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

declare const navigation: Nullable<any>

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 <Component {...pageProps} />
}

And rewrite all URLs to a common one

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

Simple router

You can route things with usePathContext()

import { usePathContext } from "@hazae41/chemin"

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

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

  return <NotFoundPage />
}

Pattern router

You can route things with pattern-matching via regexes

import { usePathContext } from "@hazae41/chemin"

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

  let matches: RegExpMatchArray | null

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

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

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

  if (matches = path.url.pathname.match(/^\/user\/([^\/]+)(\/)?$/))
    return <UserPage uuid={matches[1]} />

  return <NotFoundPage />
}

Inline router

You can also route things inside a component

import { usePathContext } from "@hazae41/chemin"

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

  return <>
    {path.url.pathname === "/help" &&
      <HelpDialog />}
    {path.url.pathname === "/send" &&
      <SendDialog />}
    <div>
      Have fun!
    </div>
  </>
}

Navigation

You can use anchors and buttons to declaratively and imperatively navigate

import { usePathContext } from "@hazae41/chemin"

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

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

  return <>
    <div>
      Welcome!
    </div>
    <a href={path.go("/home").href}>
      Home
    </a>
    <button onClick={onHelpClick}>
      Help
    </button>
  </>
}

Hash-based subpath

You can create hash-based subroutes

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

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

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

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

  return null
}

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

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

  return <>
    <HashSubpathProvider>
      <HomePageSubrouter />
    </HashSubpathProvider>
    <div>
      Hello world!
    </div>
    <a href={hash.go("/secret").href}>
      Secret anchor
    </a>
    <button onClick={onSecretButtonClick}>
      Secret button
    </button>
  </>
}

Search-based subpath

You can create search-based subroutes

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

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

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

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

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

  return <EmptyPanel />
}

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

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

  return <>
    <div>
      Hello world!
    </div>
    <a href={left.go("/football").href}>
      Show football on left
    </a>
    <a href={right.go("/baseball").href}>
      Show baseball on right
    </a>
    <div className="flex">
      <SearchSubpathProvider key="left">
        <PanelRouter />
      </SearchSubpathProvider>
      <SearchSubpathProvider key="right">
        <PanelRouter />
      </SearchSubpathProvider>
    </div>
  </>
}

Search-based value

You can also create search-based non-path values

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 <a href={user.set("root").href}>
    Login as root
  </a>
}

Search-based state

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

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 <button onClick={onClick}>
    Add
  </button>
}

Coords

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

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

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

  return <>
    <HashSubpathProvider>
      {hash.url.pathname === "/settings" &&
        <Dialog>
          Settings
        </Dialog>}
    </HashSubpathProvider>
    <a className="anchor"
      href={settings.href}
      onClick={settings.onClick}
      onKeyDown={settings.onKeyDown}>
      Open settings
    </a>
  </>
}

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

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 <div style={{ "--x": `${x}px`, "--y": `${y}px` }}>
    <div className="dialog">
      {children}
    </div>
  </div>
}

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

You can also navigate on right-click using onContextMenu

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

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

  return <>
    <HashSubpathProvider>
      {hash.url.pathname === "/menu" &&
        <Menu>
          <button>Share</button>
        </Menu>}
    </HashSubpathProvider>
    <div className="card"
      onContextMenu={menu.onContextMenu}>
      Right-click me
    </div>
  </>
}

Closeful

All providers of PathContext are also providers of CloseContext

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

import { useCloseContext } from "@hazae41/chemin"

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

  const close = useCloseContext().unwrap()

  const onClose = useCallback(() => {
    close()
  }, [close])
  
  return <div>
    <button onClick={onClose}>
      Close
    </button>
    <div>
      {children}
    </div>
  </div>
}