From 5e54d326c8140acd5276756cefcf1bb7ce256425 Mon Sep 17 00:00:00 2001 From: atellmer Date: Sun, 14 Apr 2024 15:00:27 +0500 Subject: [PATCH] RouterLink -> Link & NavLink --- examples/router/index.tsx | 12 +- .../frontend/components/app.tsx | 8 +- .../frontend/components/product-card.tsx | 6 +- .../frontend/components/product-list.tsx | 6 +- .../frontend/components/products.tsx | 8 +- .../frontend/components/ui.tsx | 2 +- packages/core/README.md | 2 +- packages/web-router/README.md | 19 ++- packages/web-router/src/constants.ts | 2 +- packages/web-router/src/index.ts | 3 +- packages/web-router/src/link/index.ts | 1 + .../link.spec.tsx} | 54 ++++----- packages/web-router/src/link/link.tsx | 35 ++++++ packages/web-router/src/nav-link/index.ts | 1 + .../web-router/src/nav-link/nav-link.spec.tsx | 113 ++++++++++++++++++ packages/web-router/src/nav-link/nav-link.tsx | 40 +++++++ packages/web-router/src/router-link/index.ts | 1 - .../src/router-link/router-link.tsx | 54 --------- templates/server/frontend/components/app.tsx | 8 +- templates/server/frontend/components/ui.tsx | 2 +- 20 files changed, 260 insertions(+), 117 deletions(-) create mode 100644 packages/web-router/src/link/index.ts rename packages/web-router/src/{router-link/router-link.spec.tsx => link/link.spec.tsx} (60%) create mode 100644 packages/web-router/src/link/link.tsx create mode 100644 packages/web-router/src/nav-link/index.ts create mode 100644 packages/web-router/src/nav-link/nav-link.spec.tsx create mode 100644 packages/web-router/src/nav-link/nav-link.tsx delete mode 100644 packages/web-router/src/router-link/index.ts delete mode 100644 packages/web-router/src/router-link/router-link.tsx diff --git a/examples/router/index.tsx b/examples/router/index.tsx index c794d89d..585de83d 100644 --- a/examples/router/index.tsx +++ b/examples/router/index.tsx @@ -1,6 +1,6 @@ import { component, lazy, Suspense, type DarkElement } from '@dark-engine/core'; import { createRoot } from '@dark-engine/platform-browser'; -import { type Routes, Router, RouterLink, useLocation } from '@dark-engine/web-router'; +import { type Routes, Router, NavLink, useLocation } from '@dark-engine/web-router'; import { createGlobalStyle, keyframes } from '@dark-engine/styled'; const Home = lazy(() => import('./home')); @@ -35,9 +35,9 @@ const Shell = component(({ slot }) => { return ( <>
- Home - About - Contacts + Home + About + Contacts
}>
@@ -162,12 +162,12 @@ const GlobalStyle = createGlobalStyle` grid-template-rows: 48px 1fr; } - .router-link-active { + .active-link { color: #ffeb3b; text-decoration: underline; } - .router-link-active:hover { + .active-link:hover { color: #ffeb3b; } diff --git a/examples/server-side-rendering/frontend/components/app.tsx b/examples/server-side-rendering/frontend/components/app.tsx index c5c1aefb..1059f51e 100644 --- a/examples/server-side-rendering/frontend/components/app.tsx +++ b/examples/server-side-rendering/frontend/components/app.tsx @@ -1,5 +1,5 @@ import { component, Suspense, lazy, useMemo, useEffect } from '@dark-engine/core'; -import { type Routes, Router, RouterLink } from '@dark-engine/web-router'; +import { type Routes, Router, NavLink } from '@dark-engine/web-router'; import { DataClient, DataClientProvider, InMemoryCache } from '@dark-engine/data'; import { type Api, Key } from '../../contract'; @@ -102,9 +102,9 @@ const App = component(({ url, api }) => {
- Products - Operations - Invoices + Products + Operations + Invoices
{slot} diff --git a/examples/server-side-rendering/frontend/components/product-card.tsx b/examples/server-side-rendering/frontend/components/product-card.tsx index c676447c..f03323fc 100644 --- a/examples/server-side-rendering/frontend/components/product-card.tsx +++ b/examples/server-side-rendering/frontend/components/product-card.tsx @@ -1,5 +1,5 @@ import { type DarkElement, component } from '@dark-engine/core'; -import { RouterLink, useMatch, useParams } from '@dark-engine/web-router'; +import { Link, useMatch, useParams } from '@dark-engine/web-router'; import { useProduct } from '../hooks'; import { Spinner, Error, Card, Button } from './ui'; @@ -29,10 +29,10 @@ const ProductCard = component<{ slot: DarkElement }>(({ slot }) => {

{data.name}

{data.description}

- -
diff --git a/examples/server-side-rendering/frontend/components/product-list.tsx b/examples/server-side-rendering/frontend/components/product-list.tsx index bde242f4..1c72c8d1 100644 --- a/examples/server-side-rendering/frontend/components/product-list.tsx +++ b/examples/server-side-rendering/frontend/components/product-list.tsx @@ -1,5 +1,5 @@ import { type DarkElement, component } from '@dark-engine/core'; -import { RouterLink, useMatch, useLocation } from '@dark-engine/web-router'; +import { Link, useMatch, useLocation } from '@dark-engine/web-router'; import { styled } from '@dark-engine/styled'; import { useProducts } from '../hooks'; @@ -21,7 +21,7 @@ const ProductList = component<{ slot: DarkElement }>(({ slot }) => { {[...data].reverse().map(x => { return ( - {x.name} + {x.name} ); })} @@ -36,7 +36,7 @@ const ProductList = component<{ slot: DarkElement }>(({ slot }) => {
{isList ? ( - ) : ( diff --git a/examples/server-side-rendering/frontend/components/products.tsx b/examples/server-side-rendering/frontend/components/products.tsx index e538c287..3b312757 100644 --- a/examples/server-side-rendering/frontend/components/products.tsx +++ b/examples/server-side-rendering/frontend/components/products.tsx @@ -1,5 +1,5 @@ import { type DarkElement, component } from '@dark-engine/core'; -import { RouterLink, useMatch } from '@dark-engine/web-router'; +import { NavLink, useMatch } from '@dark-engine/web-router'; import { AnimationFade, Menu, Sticky } from './ui'; @@ -15,9 +15,9 @@ const Products = component(({ slot }) => {

Products 📈

- List - Analytics - Balance + List + Analytics + Balance
{slot} diff --git a/examples/server-side-rendering/frontend/components/ui.tsx b/examples/server-side-rendering/frontend/components/ui.tsx index 209b0724..908efd5f 100644 --- a/examples/server-side-rendering/frontend/components/ui.tsx +++ b/examples/server-side-rendering/frontend/components/ui.tsx @@ -65,7 +65,7 @@ const GlobalStyle = createGlobalStyle` text-decoration: underline; } - .router-link-active, .router-link-active:hover { + .active-link, .active-link:hover { text-decoration: underline; } `; diff --git a/packages/core/README.md b/packages/core/README.md index 70ae405a..49bcaa98 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -936,7 +936,7 @@ Allows you to avoid reloading the entire interface when changing code in develop ```tsx // index.tsx -import { h, hot } from '@dark-engine/core'; +import { hot } from '@dark-engine/core'; import { createRoot } from '@dark-engine/platform-browser'; import { App } from './app'; diff --git a/packages/web-router/README.md b/packages/web-router/README.md index 54cbe4aa..82cfe7b5 100644 --- a/packages/web-router/README.md +++ b/packages/web-router/README.md @@ -39,7 +39,8 @@ import { type Routes, type RouterRef, Router, - RouterLink, + Link, + NavLink, useLocation, useHistory, useParams, @@ -63,8 +64,8 @@ const App = component(() => { return ( <>
- first-component - second-component + first-component + second-component
{slot}
{/*<-- a route content will be placed here*/} @@ -222,12 +223,18 @@ const routes: Routes = [ ## Navigation -### via `RouterLink` +### via `Link` or `NavLink` ```tsx -Home +Go to profile +Home ``` +`NavLink` internally uses `Link`, but at the same time provides a CSS class `.active-link` if the current URL is equal to or contains the `to` parameter of `NavLink`. +`NavLink` can be used for headers and menus, which will continue to be on the page when it is clicked and the content is changed. +`Link` means that it will disappear from the screen after you click it and go to another page. Of course you can create your own logic based on `Link`, using it as a base component. + + ### via `history` ```tsx @@ -317,7 +324,7 @@ const App = component(({ url, routes }) => { ## Server-Side Rendering (SSR) -If you are rendering the application on the server, then you must pass the request url to the router to emulate routing when rendering to a string. +If you are rendering the application on the server, then you must pass the request URL to the router to emulate routing when rendering to a string. ```tsx server.get('*', async (req, res) => { diff --git a/packages/web-router/src/constants.ts b/packages/web-router/src/constants.ts index ba36e048..01db6aa3 100644 --- a/packages/web-router/src/constants.ts +++ b/packages/web-router/src/constants.ts @@ -6,4 +6,4 @@ export const PROTOCOL_MARK = '://'; export const SEARCH_MARK = '?'; export const HASH_MARK = '#'; export const ROOT_MARK = '__ROOT__'; -export const ACTIVE_LINK_CLASSNAME = 'router-link-active'; +export const ACTIVE_LINK_CLASSNAME = 'active-link'; diff --git a/packages/web-router/src/index.ts b/packages/web-router/src/index.ts index 19c83d09..4a93a4e6 100644 --- a/packages/web-router/src/index.ts +++ b/packages/web-router/src/index.ts @@ -1,8 +1,9 @@ export { type Routes } from './create-routes'; export { type RouterRef, Router } from './router'; export { useLocation } from './use-location'; -export { RouterLink } from './router-link'; export { useHistory } from './use-history'; export { useParams } from './use-params'; export { useMatch } from './use-match'; export { VERSION } from './constants'; +export { NavLink } from './nav-link'; +export { Link } from './link'; diff --git a/packages/web-router/src/link/index.ts b/packages/web-router/src/link/index.ts new file mode 100644 index 00000000..e33728e0 --- /dev/null +++ b/packages/web-router/src/link/index.ts @@ -0,0 +1 @@ +export * from './link'; diff --git a/packages/web-router/src/router-link/router-link.spec.tsx b/packages/web-router/src/link/link.spec.tsx similarity index 60% rename from packages/web-router/src/router-link/router-link.spec.tsx rename to packages/web-router/src/link/link.spec.tsx index 1a6adb13..eb8c1db8 100644 --- a/packages/web-router/src/router-link/router-link.spec.tsx +++ b/packages/web-router/src/link/link.spec.tsx @@ -1,11 +1,10 @@ import { component } from '@dark-engine/core'; import { type SyntheticEvent } from '@dark-engine/platform-browser'; -import { createBrowserEnv, replacer, click, dom, resetBrowserHistory } from '@test-utils'; +import { createBrowserEnv, click, resetBrowserHistory } from '@test-utils'; import { type Routes } from '../create-routes'; import { Router } from '../router'; -import { ACTIVE_LINK_CLASSNAME } from '../constants'; -import { RouterLink } from './router-link'; +import { Link } from './link'; let { host, render } = createBrowserEnv(); @@ -17,17 +16,8 @@ afterEach(() => { resetBrowserHistory(); }); -describe('@web-router/router-link', () => { +describe('@web-router/link', () => { test('can navigate by routes correctly', () => { - const content = (active: string, value: string) => dom` -
- first - second - third -
-
${value}
- `; - const routes: Routes = [ { path: 'first', @@ -54,9 +44,9 @@ describe('@web-router/router-link', () => { return ( <>
- first - second - third + first + second + third
{slot}
@@ -67,23 +57,33 @@ describe('@web-router/router-link', () => { }); render(); - expect(host.innerHTML).toBe(content('', replacer)); + expect(host.innerHTML).toMatchInlineSnapshot( + `"
firstsecondthird
"`, + ); const link1 = host.querySelector('a[href="/first"]'); const link2 = host.querySelector('a[href="/second"]'); const link3 = host.querySelector('a[href="/third"]'); click(link1); - expect(host.innerHTML).toBe(content('/first', `
first
`)); + expect(host.innerHTML).toMatchInlineSnapshot( + `"
firstsecondthird
first
"`, + ); click(link1); - expect(host.innerHTML).toBe(content('/first', `
first
`)); + expect(host.innerHTML).toMatchInlineSnapshot( + `"
firstsecondthird
first
"`, + ); click(link2); - expect(host.innerHTML).toBe(content('/second', `
second
`)); + expect(host.innerHTML).toMatchInlineSnapshot( + `"
firstsecondthird
second
"`, + ); click(link3); - expect(host.innerHTML).toBe(content('/third', `
third
`)); + expect(host.innerHTML).toMatchInlineSnapshot( + `"
firstsecondthird
third
"`, + ); }); test('can work with custom classes correctly', () => { @@ -99,9 +99,9 @@ describe('@web-router/router-link', () => { {() => { return ( - + first - + ); }} @@ -109,7 +109,7 @@ describe('@web-router/router-link', () => { }); render(); - expect(host.innerHTML).toBe(`first`); + expect(host.innerHTML).toMatchInlineSnapshot(`"first"`); }); test('prevent default click event', () => { @@ -131,9 +131,9 @@ describe('@web-router/router-link', () => { {() => { return ( - + first - + ); }} @@ -141,7 +141,7 @@ describe('@web-router/router-link', () => { }); render(); - expect(host.innerHTML).toBe(`first`); + expect(host.innerHTML).toMatchInlineSnapshot(`"first"`); click(host.querySelector('a')); expect(defaultPrevented).toBe(true); diff --git a/packages/web-router/src/link/link.tsx b/packages/web-router/src/link/link.tsx new file mode 100644 index 00000000..db5a6bc7 --- /dev/null +++ b/packages/web-router/src/link/link.tsx @@ -0,0 +1,35 @@ +import { type DarkElement, component, forwardRef, useEvent, detectIsFunction } from '@dark-engine/core'; +import { type SyntheticEvent, type DarkJSX } from '@dark-engine/platform-browser'; + +import { useHistory } from '../use-history'; + +export type LinkProps = { + to: string; + slot: DarkElement; +} & Omit; + +const Link = forwardRef( + component( + (props, ref) => { + const { to, class: cn1, className: cn2, slot, onClick, ...rest } = props; + const history = useHistory(); + const className = cn1 || cn2; + const handleClick = useEvent((e: SyntheticEvent) => { + e.preventDefault(); + history.push(to); + detectIsFunction(onClick) && onClick(e); + }); + + return ( + + {slot} + + ); + }, + { + displayName: 'Link', + }, + ), +); + +export { Link }; diff --git a/packages/web-router/src/nav-link/index.ts b/packages/web-router/src/nav-link/index.ts new file mode 100644 index 00000000..f1b68c26 --- /dev/null +++ b/packages/web-router/src/nav-link/index.ts @@ -0,0 +1 @@ +export * from './nav-link'; diff --git a/packages/web-router/src/nav-link/nav-link.spec.tsx b/packages/web-router/src/nav-link/nav-link.spec.tsx new file mode 100644 index 00000000..0c8ad82f --- /dev/null +++ b/packages/web-router/src/nav-link/nav-link.spec.tsx @@ -0,0 +1,113 @@ +import { component } from '@dark-engine/core'; + +import { createBrowserEnv, click, resetBrowserHistory } from '@test-utils'; +import { type Routes } from '../create-routes'; +import { Router } from '../router'; +import { NavLink } from './nav-link'; + +let { host, render } = createBrowserEnv(); + +beforeEach(() => { + ({ host, render } = createBrowserEnv()); +}); + +afterEach(() => { + resetBrowserHistory(); +}); + +describe('@web-router/nav-link', () => { + test('can navigate by routes correctly', () => { + const routes: Routes = [ + { + path: 'first', + component: component(() =>
first
), + }, + { + path: 'second', + component: component(() =>
second
), + }, + { + path: 'third', + component: component(() =>
third
), + }, + { + path: '**', + component: component(() => null), + }, + ]; + + const App = component(() => { + return ( + + {slot => { + return ( + <> +
+ first + second + third +
+
{slot}
+ + ); + }} +
+ ); + }); + + render(); + expect(host.innerHTML).toMatchInlineSnapshot( + `"
firstsecondthird
"`, + ); + + const link1 = host.querySelector('a[href="/first"]'); + const link2 = host.querySelector('a[href="/second"]'); + const link3 = host.querySelector('a[href="/third"]'); + + click(link1); + expect(host.innerHTML).toMatchInlineSnapshot( + `"
firstsecondthird
first
"`, + ); + + click(link1); + expect(host.innerHTML).toMatchInlineSnapshot( + `"
firstsecondthird
first
"`, + ); + + click(link2); + expect(host.innerHTML).toMatchInlineSnapshot( + `"
firstsecondthird
second
"`, + ); + + click(link3); + expect(host.innerHTML).toMatchInlineSnapshot( + `"
firstsecondthird
third
"`, + ); + }); + + test('can work with custom classes correctly', () => { + const routes: Routes = [ + { + path: '', + component: component(() => null), + }, + ]; + + const App = component(() => { + return ( + + {() => { + return ( + + first + + ); + }} + + ); + }); + + render(); + expect(host.innerHTML).toMatchInlineSnapshot(`"first"`); + }); +}); diff --git a/packages/web-router/src/nav-link/nav-link.tsx b/packages/web-router/src/nav-link/nav-link.tsx new file mode 100644 index 00000000..1855a2da --- /dev/null +++ b/packages/web-router/src/nav-link/nav-link.tsx @@ -0,0 +1,40 @@ +import { component, forwardRef, useMemo } from '@dark-engine/core'; + +import { SLASH_MARK, ACTIVE_LINK_CLASSNAME } from '../constants'; +import { type LinkProps, Link } from '../link'; +import { useLocation } from '../use-location'; +import { cm, parseURL } from '../utils'; + +export type NavLinkProps = LinkProps; + +const NavLink = forwardRef( + component( + (props, ref) => { + const { to, class: cn1, className: cn2, slot, ...rest } = props; + const { pathname: url, hash } = useLocation(); + const className = useMemo(() => { + const isMatch = detectIsMatch(url, to, hash); + + return cm(cn1 || cn2, isMatch ? ACTIVE_LINK_CLASSNAME : ''); + }, [cn1, cn2, url, hash, to]); + + return ( + + {slot} + + ); + }, + { + displayName: 'NavLink', + }, + ), +); + +function detectIsMatch(url: string, to: string, hash: string) { + const { pathname: $to, hash: $hash } = parseURL(to); + const isMatch = ($to === url && hash === $hash) || (hash === $hash && $to !== SLASH_MARK && url.indexOf($to) === 0); + + return isMatch; +} + +export { NavLink }; diff --git a/packages/web-router/src/router-link/index.ts b/packages/web-router/src/router-link/index.ts deleted file mode 100644 index 587f0afe..00000000 --- a/packages/web-router/src/router-link/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './router-link'; diff --git a/packages/web-router/src/router-link/router-link.tsx b/packages/web-router/src/router-link/router-link.tsx deleted file mode 100644 index 2b85f011..00000000 --- a/packages/web-router/src/router-link/router-link.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { type DarkElement, component, forwardRef, useMemo, useEvent, detectIsFunction } from '@dark-engine/core'; -import { type SyntheticEvent, type DarkJSX } from '@dark-engine/platform-browser'; - -import { SLASH_MARK, ACTIVE_LINK_CLASSNAME } from '../constants'; -import { normalizePath, cm, parseURL } from '../utils'; -import { useLocation } from '../use-location'; -import { useHistory } from '../use-history'; - -export type RoutreLinkProps = { - to: string; - slot: DarkElement; - activeClassName?: string; -} & Omit; - -const RouterLink = forwardRef( - component( - (props, ref) => { - const { to, activeClassName = ACTIVE_LINK_CLASSNAME, class: cl1, className: cl2, slot, onClick, ...rest } = props; - const history = useHistory(); - const { pathname: path, hash } = useLocation(); - const isActive = useMemo(() => detectIsActiveLink(path, hash, to), [path, hash, to]); - const $className = cl1 || cl2; - const className = useMemo( - () => cm($className, isActive ? activeClassName : ''), - [$className, activeClassName, isActive], - ); - const handleClick = useEvent((e: SyntheticEvent) => { - e.preventDefault(); - history.push(to); - detectIsFunction(onClick) && onClick(e); - }); - - return ( - - {slot} - - ); - }, - { - displayName: 'RouterLink', - }, - ), -); - -function detectIsActiveLink(path: string, hash: string, to: string): boolean { - const { pathname: $to, hash: $hash } = parseURL(to); - const $path = normalizePath(path); - - if ($to === SLASH_MARK) return $path === SLASH_MARK; - - return $path.indexOf($to) !== -1 && hash === $hash; -} - -export { RouterLink }; diff --git a/templates/server/frontend/components/app.tsx b/templates/server/frontend/components/app.tsx index 52c8bd6d..ec5fd17d 100644 --- a/templates/server/frontend/components/app.tsx +++ b/templates/server/frontend/components/app.tsx @@ -1,5 +1,5 @@ import { component, Suspense, lazy, useMemo, useEffect } from '@dark-engine/core'; -import { type Routes, Router, RouterLink } from '@dark-engine/web-router'; +import { type Routes, Router, NavLink } from '@dark-engine/web-router'; import { DataClient, DataClientProvider, InMemoryCache } from '@dark-engine/data'; import { type Api, Key } from '../../contract'; @@ -51,9 +51,9 @@ const App = component(({ url, api }) => {
- Products - Operations - Invoices + Products + Operations + Invoices
{slot} diff --git a/templates/server/frontend/components/ui.tsx b/templates/server/frontend/components/ui.tsx index 400de49f..1662b550 100644 --- a/templates/server/frontend/components/ui.tsx +++ b/templates/server/frontend/components/ui.tsx @@ -43,7 +43,7 @@ const GlobalStyle = createGlobalStyle` text-decoration: underline; } - .router-link-active, .router-link-active:hover { + .active-link, .active-link:hover { text-decoration: underline; } `;