Skip to content

Commit

Permalink
Merge pull request #58 from atellmer/feature/router-link
Browse files Browse the repository at this point in the history
Link & NavLink
  • Loading branch information
atellmer authored Apr 14, 2024
2 parents 4708acf + 5e54d32 commit 8c4c972
Show file tree
Hide file tree
Showing 20 changed files with 260 additions and 117 deletions.
12 changes: 6 additions & 6 deletions examples/router/index.tsx
Original file line number Diff line number Diff line change
@@ -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'));
Expand Down Expand Up @@ -35,9 +35,9 @@ const Shell = component<ShellProps>(({ slot }) => {
return (
<>
<header>
<RouterLink to='/home'>Home</RouterLink>
<RouterLink to='/about'>About</RouterLink>
<RouterLink to='/contacts'>Contacts</RouterLink>
<NavLink to='/home'>Home</NavLink>
<NavLink to='/about'>About</NavLink>
<NavLink to='/contacts'>Contacts</NavLink>
</header>
<Suspense fallback={<Spinner />}>
<main key={key} class='fade'>
Expand Down Expand Up @@ -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;
}
Expand Down
8 changes: 4 additions & 4 deletions examples/server-side-rendering/frontend/components/app.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -102,9 +102,9 @@ const App = component<AppProps>(({ url, api }) => {
<Root>
<Header>
<Menu>
<RouterLink to='/products'>Products</RouterLink>
<RouterLink to='/operations'>Operations</RouterLink>
<RouterLink to='/invoices'>Invoices</RouterLink>
<NavLink to='/products'>Products</NavLink>
<NavLink to='/operations'>Operations</NavLink>
<NavLink to='/invoices'>Invoices</NavLink>
</Menu>
</Header>
<Content>{slot}</Content>
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -29,10 +29,10 @@ const ProductCard = component<{ slot: DarkElement }>(({ slot }) => {
<Card>
<h3>{data.name}</h3>
<p>{data.description}</p>
<Button as={RouterLink} {...{ to: urlToEdit }}>
<Button as={Link} {...{ to: urlToEdit }}>
Edit
</Button>
<Button as={RouterLink} {...{ to: urlToRemove }}>
<Button as={Link} {...{ to: urlToRemove }}>
Remove
</Button>
</Card>
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -21,7 +21,7 @@ const ProductList = component<{ slot: DarkElement }>(({ slot }) => {
{[...data].reverse().map(x => {
return (
<ListItem key={x.id}>
<RouterLink to={`${url}/${x.id}`}>{x.name}</RouterLink>
<Link to={`${url}/${x.id}`}>{x.name}</Link>
</ListItem>
);
})}
Expand All @@ -36,7 +36,7 @@ const ProductList = component<{ slot: DarkElement }>(({ slot }) => {
<AnimationFade>
<Header>
{isList ? (
<Button as={RouterLink} {...{ to: urlToAdd }}>
<Button as={Link} {...{ to: urlToAdd }}>
Add product
</Button>
) : (
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -15,9 +15,9 @@ const Products = component<ProductsProps>(({ slot }) => {
<Sticky>
<h1>Products 📈</h1>
<Menu $isSecondary>
<RouterLink to={`${url}/list`}>List</RouterLink>
<RouterLink to={`${url}/analytics`}>Analytics</RouterLink>
<RouterLink to={`${url}/balance`}>Balance</RouterLink>
<NavLink to={`${url}/list`}>List</NavLink>
<NavLink to={`${url}/analytics`}>Analytics</NavLink>
<NavLink to={`${url}/balance`}>Balance</NavLink>
</Menu>
</Sticky>
{slot}
Expand Down
2 changes: 1 addition & 1 deletion examples/server-side-rendering/frontend/components/ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
`;
Expand Down
2 changes: 1 addition & 1 deletion packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
19 changes: 13 additions & 6 deletions packages/web-router/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ import {
type Routes,
type RouterRef,
Router,
RouterLink,
Link,
NavLink,
useLocation,
useHistory,
useParams,
Expand All @@ -63,8 +64,8 @@ const App = component(() => {
return (
<>
<header>
<RouterLink to='/first-component'>first-component</RouterLink>
<RouterLink to='/second-component'>second-component</RouterLink>
<NavLink to='/first-component'>first-component</NavLink>
<NavLink to='/second-component'>second-component</NavLink>
</header>
<main>{slot}</main> {/*<-- a route content will be placed here*/}
</>
Expand Down Expand Up @@ -222,12 +223,18 @@ const routes: Routes = [

## Navigation

### via `RouterLink`
### via `Link` or `NavLink`

```tsx
<RouterLink to='/home'>Home</RouterLink>
<Link to='/user/50'>Go to profile</Link>
<NavLink to='/home'>Home</NavLink>
```

`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
Expand Down Expand Up @@ -317,7 +324,7 @@ const App = component<AppProps>(({ 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) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/web-router/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
3 changes: 2 additions & 1 deletion packages/web-router/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
1 change: 1 addition & 0 deletions packages/web-router/src/link/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './link';
Original file line number Diff line number Diff line change
@@ -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();

Expand All @@ -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`
<header>
<a href="/first"${active === '/first' ? ` class="${ACTIVE_LINK_CLASSNAME}"` : ''}>first</a>
<a href="/second"${active === '/second' ? ` class="${ACTIVE_LINK_CLASSNAME}"` : ''}>second</a>
<a href="/third"${active === '/third' ? ` class="${ACTIVE_LINK_CLASSNAME}"` : ''}>third</a>
</header>
<main>${value}</main>
`;

const routes: Routes = [
{
path: 'first',
Expand All @@ -54,9 +44,9 @@ describe('@web-router/router-link', () => {
return (
<>
<header>
<RouterLink to='/first'>first</RouterLink>
<RouterLink to='/second'>second</RouterLink>
<RouterLink to='/third'>third</RouterLink>
<Link to='/first'>first</Link>
<Link to='/second'>second</Link>
<Link to='/third'>third</Link>
</header>
<main>{slot}</main>
</>
Expand All @@ -67,23 +57,33 @@ describe('@web-router/router-link', () => {
});

render(<App />);
expect(host.innerHTML).toBe(content('', replacer));
expect(host.innerHTML).toMatchInlineSnapshot(
`"<header><a href="/first">first</a><a href="/second">second</a><a href="/third">third</a></header><main><!--dark:matter--></main>"`,
);

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', `<div>first</div>`));
expect(host.innerHTML).toMatchInlineSnapshot(
`"<header><a href="/first">first</a><a href="/second">second</a><a href="/third">third</a></header><main><div>first</div></main>"`,
);

click(link1);
expect(host.innerHTML).toBe(content('/first', `<div>first</div>`));
expect(host.innerHTML).toMatchInlineSnapshot(
`"<header><a href="/first">first</a><a href="/second">second</a><a href="/third">third</a></header><main><div>first</div></main>"`,
);

click(link2);
expect(host.innerHTML).toBe(content('/second', `<div>second</div>`));
expect(host.innerHTML).toMatchInlineSnapshot(
`"<header><a href="/first">first</a><a href="/second">second</a><a href="/third">third</a></header><main><div>second</div></main>"`,
);

click(link3);
expect(host.innerHTML).toBe(content('/third', `<div>third</div>`));
expect(host.innerHTML).toMatchInlineSnapshot(
`"<header><a href="/first">first</a><a href="/second">second</a><a href="/third">third</a></header><main><div>third</div></main>"`,
);
});

test('can work with custom classes correctly', () => {
Expand All @@ -99,17 +99,17 @@ describe('@web-router/router-link', () => {
<Router routes={routes}>
{() => {
return (
<RouterLink to='/' className='my-link' activeClassName='custom-active-link'>
<Link to='/' className='my-link'>
first
</RouterLink>
</Link>
);
}}
</Router>
);
});

render(<App />);
expect(host.innerHTML).toBe(`<a href="/" class="my-link custom-active-link">first</a>`);
expect(host.innerHTML).toMatchInlineSnapshot(`"<a href="/" class="my-link">first</a>"`);
});

test('prevent default click event', () => {
Expand All @@ -131,17 +131,17 @@ describe('@web-router/router-link', () => {
<Router routes={routes}>
{() => {
return (
<RouterLink to='/' onClick={handleClick}>
<Link to='/' onClick={handleClick}>
first
</RouterLink>
</Link>
);
}}
</Router>
);
});

render(<App />);
expect(host.innerHTML).toBe(`<a href="/" class="${ACTIVE_LINK_CLASSNAME}">first</a>`);
expect(host.innerHTML).toMatchInlineSnapshot(`"<a href="/">first</a>"`);

click(host.querySelector('a'));
expect(defaultPrevented).toBe(true);
Expand Down
35 changes: 35 additions & 0 deletions packages/web-router/src/link/link.tsx
Original file line number Diff line number Diff line change
@@ -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<DarkJSX.Elements['a'], 'href'>;

const Link = forwardRef<LinkProps, HTMLAnchorElement>(
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<MouseEvent, HTMLAnchorElement>) => {
e.preventDefault();
history.push(to);
detectIsFunction(onClick) && onClick(e);
});

return (
<a ref={ref} {...rest} href={to} class={className} onClick={handleClick}>
{slot}
</a>
);
},
{
displayName: 'Link',
},
),
);

export { Link };
1 change: 1 addition & 0 deletions packages/web-router/src/nav-link/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './nav-link';
Loading

0 comments on commit 8c4c972

Please sign in to comment.