diff --git a/packages/core/package.json b/packages/core/package.json index e772e643..369e0637 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@dark-engine/core", - "version": "0.16.1", + "version": "0.17.0", "description": "Dark is lightweight component-and-hook-based UI rendering engine for javascript apps without dependencies and written in TypeScript 💫", "author": "AlexPlex", "license": "MIT", diff --git a/packages/core/src/use-imperative-handle/use-imperative-handle.ts b/packages/core/src/use-imperative-handle/use-imperative-handle.ts index f2821aaf..c4be55b4 100644 --- a/packages/core/src/use-imperative-handle/use-imperative-handle.ts +++ b/packages/core/src/use-imperative-handle/use-imperative-handle.ts @@ -4,7 +4,9 @@ import type { MutableRef } from '../ref'; function useImperativeHandle(ref: MutableRef, createHandle: () => T, deps?: Array) { const current = useMemo(() => createHandle(), deps || [{}]); - ref.current = current; + if (ref) { + ref.current = current; + } } export { useImperativeHandle }; diff --git a/packages/platform-browser/package.json b/packages/platform-browser/package.json index ff414c1b..2ed7b4cf 100644 --- a/packages/platform-browser/package.json +++ b/packages/platform-browser/package.json @@ -1,6 +1,6 @@ { "name": "@dark-engine/platform-browser", - "version": "0.16.1", + "version": "0.17.0", "description": "Dark is lightweight component-and-hook-based UI rendering engine for javascript apps without dependencies and written in TypeScript 💫", "author": "AlexPlex", "license": "MIT", diff --git a/packages/platform-server/package.json b/packages/platform-server/package.json index b6e386e0..5bce0d2d 100644 --- a/packages/platform-server/package.json +++ b/packages/platform-server/package.json @@ -1,6 +1,6 @@ { "name": "@dark-engine/platform-server", - "version": "0.16.1", + "version": "0.17.0", "description": "Dark is lightweight component-and-hook-based UI rendering engine for javascript apps without dependencies and written in TypeScript 💫", "author": "AlexPlex", "license": "MIT", diff --git a/packages/web-router/README.md b/packages/web-router/README.md index a4303091..75c6d806 100644 --- a/packages/web-router/README.md +++ b/packages/web-router/README.md @@ -293,3 +293,25 @@ const App = createComponent(({ url }) => { ``` Full example SSR routing you can see in examples. + +## Imperative access to router + +```tsx +const App = createComponent(({ url, routes }) => { + const ref = useRef(null); + + useEffect(() => { + setTimeout(() => { + ref.current.navigateTo('/about'); + }); + }, []); + + return ( + + {slot => slot} + + ); +}); +``` + + diff --git a/packages/web-router/package.json b/packages/web-router/package.json index 623d8b6e..dc3ef414 100644 --- a/packages/web-router/package.json +++ b/packages/web-router/package.json @@ -1,6 +1,6 @@ { "name": "@dark-engine/web-router", - "version": "0.16.1", + "version": "0.17.0", "description": "Dark is lightweight component-and-hook-based UI rendering engine for javascript apps without dependencies and written in TypeScript 💫", "author": "AlexPlex", "license": "MIT", diff --git a/packages/web-router/src/index.ts b/packages/web-router/src/index.ts index e78df34e..6f532884 100644 --- a/packages/web-router/src/index.ts +++ b/packages/web-router/src/index.ts @@ -1,5 +1,5 @@ export { type Routes } from './create-routes'; -export { Router } from './router'; +export { type RouterRef, Router } from './router'; export { RouterLink } from './router-link'; export { useLocation } from './use-location'; export { useHistory } from './use-history'; diff --git a/packages/web-router/src/router/router.tsx b/packages/web-router/src/router/router.tsx index 91592547..c0aca019 100644 --- a/packages/web-router/src/router/router.tsx +++ b/packages/web-router/src/router/router.tsx @@ -1,9 +1,20 @@ -import { type DarkElement, h, createComponent, useMemo, useEffect, useLayoutEffect, useState } from '@dark-engine/core'; +import { + type DarkElement, + type MutableRef, + h, + createComponent, + useMemo, + useEffect, + useLayoutEffect, + useState, + forwardRef, + useImperativeHandle, +} from '@dark-engine/core'; import { SLASH, PROTOCOL_MARK } from '../constants'; import { normalaizePathname } from '../utils'; import { createRouterHistory } from '../history'; -import { createRouterLocation } from '../location'; +import { type RouterLocation, createRouterLocation } from '../location'; import { type Routes, createRoutes, renderRoot, pathnameFromPath } from '../create-routes'; import { type RouterHistoryContextValue, @@ -20,56 +31,68 @@ export type RouterProps = { slot: (slot: DarkElement) => DarkElement; }; -const Router = createComponent(({ url, baseURL = SLASH, routes: sourceRoutes, slot }) => { - if (useActiveRouteContext()) { - throw new Error('[web-router]: Parent active route context detected!'); - } - const sourceURL = url || window.location.href; - const [location, setLocation] = useState(() => createRouterLocation(sourceURL)); - const history = useMemo(() => createRouterHistory(sourceURL), []); - const routes = useMemo(() => createRoutes(sourceRoutes, normalaizePathname(baseURL)), []); - const { protocol, host, pathname, search } = location; - const { matched, params, rendered } = renderRoot(pathname, routes); - const scope = useMemo(() => ({ location }), []); - const historyContext = useMemo(() => ({ history }), []); - const routerContext = useMemo(() => ({ location, matched, params }), [pathname, search]); - - scope.location = location; +export type RouterRef = { + navigateTo: (pathname: string) => void; + location: RouterLocation; +}; - useLayoutEffect(() => { - if (sourceURL !== scope.location.url) { - setLocation(createRouterLocation(sourceURL)); +const Router = forwardRef( + createComponent(({ url, baseURL = SLASH, routes: sourceRoutes, slot }, ref) => { + if (useActiveRouteContext()) { + throw new Error('[web-router]: Parent active route context detected!'); } - }, [sourceURL]); + const sourceURL = url || window.location.href; + const [location, setLocation] = useState(() => createRouterLocation(sourceURL)); + const history = useMemo(() => createRouterHistory(sourceURL), []); + const routes = useMemo(() => createRoutes(sourceRoutes, normalaizePathname(baseURL)), []); + const { protocol, host, pathname, search } = location; + const { matched, params, rendered } = renderRoot(pathname, routes); + const scope = useMemo(() => ({ location }), []); + const historyContext = useMemo(() => ({ history }), []); + const routerContext = useMemo(() => ({ location, matched, params }), [pathname, search]); - useLayoutEffect(() => { - const unsubscribe = history.subscribe(spathname => { - const url = `${protocol}${PROTOCOL_MARK}${host}${spathname}`; + scope.location = location; - setLocation(createRouterLocation(url)); - }); + useLayoutEffect(() => { + if (sourceURL !== scope.location.url) { + setLocation(createRouterLocation(sourceURL)); + } + }, [sourceURL]); - return () => { - unsubscribe(); - history.dispose(); - }; - }, []); + useLayoutEffect(() => { + const unsubscribe = history.subscribe(spathname => { + const url = `${protocol}${PROTOCOL_MARK}${host}${spathname}`; - useEffect(() => { - if (!matched) return; - const spathname = pathname + search; - const newSpathname = pathnameFromPath(pathname, matched.cursor.fullPath) + search; + setLocation(createRouterLocation(url)); + }); - if (spathname !== newSpathname) { - history.replace(newSpathname); - } - }, [pathname, search]); + return () => { + unsubscribe(); + history.dispose(); + }; + }, []); + + useEffect(() => { + if (!matched) return; + const spathname = pathname + search; + const newSpathname = pathnameFromPath(pathname, matched.cursor.fullPath) + search; + + if (spathname !== newSpathname) { + history.replace(newSpathname); + } + }, [pathname, search]); + + useImperativeHandle(ref as MutableRef, () => ({ + navigateTo: (pathname: string) => history.push(pathname), + location, + })); - return ( - - {slot(rendered)} - - ); -}); + return ( + + {slot(rendered)} + + ); + }), +); export { Router };