Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): create a <Link /> component to navigate to a given resource. #6330

Merged
merged 8 commits into from
Sep 19, 2024
43 changes: 43 additions & 0 deletions .changeset/chilly-bottles-grab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
"@refinedev/core": minor
---

feat: add [`<Link />`](https://refine.dev/docs/routing/components/link/) component to navigate to a resource with a specific action. Under the hood, It uses [`useGo`](https://refine.dev/docs/routing/hooks/use-go/) to generate the URL.

## Usage

```tsx
import { Link } from "@refinedev/core";

const MyComponent = () => {
return (
<>
{/* simple usage, navigates to `/posts` */}
<Link to="/posts">Posts</Link>
{/* complex usage with more control, navigates to `/posts` with query filters */}
<Link
go={{
query: {
// `useTable` or `useDataGrid` automatically use this filters to fetch data if `syncWithLocation` is true.
filters: [
{
operator: "eq",
value: "published",
field: "status",
},
],
},
to: {
resource: "posts",
action: "list",
},
}}
>
Posts
</Link>
</>
);
};
```

[Fixes #6329](https://github.com/refinedev/refine/issues/6329)
9 changes: 9 additions & 0 deletions .changeset/itchy-moose-reflect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@refinedev/core": minor
---

chore: From now on, [`useLink`](https://refine.dev/docs/routing/hooks/use-link/) returns [`<Link />`](https://refine.dev/docs/routing/components/link/) component instead of returning [`routerProvider.Link`](https://refine.dev/docs/routing/router-provider/#link).

Since the `<Link />` component uses `routerProvider.Link` under the hood with leveraging `useGo` hook to generate the URL there is no breaking change. It's recommended to use the `<Link />` component from the `@refinedev/core` package instead of `useLink` hook. This hook is used mostly for internal purposes and is only exposed for customization needs.

[Fixes #6329](https://github.com/refinedev/refine/issues/6329)
94 changes: 94 additions & 0 deletions documentation/docs/routing/components/link/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
---
title: <Link />
---

`<Link />` is a component that is used to navigate to different pages in your application.

It's uses [`routerProvider.Link`](/docs/routing/router-provider/#link) under the hood, if [`routerProvider`](/docs/routing/router-provider) is not provided, it will be use [`<a>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a) HTML element.
alicanerdurmaz marked this conversation as resolved.
Show resolved Hide resolved

## Usage

```tsx
import { Link } from "@refinedev/core";

const MyComponent = () => {
return (
<>
{/* simple usage, navigates to `/posts` */}
<Link to="/posts">Posts</Link>
{/* complex usage with more control, navigates to `/posts` with query filters */}
<Link
go={{
query: {
// `useTable` or `useDataGrid` automatically use this filters to fetch data if `syncWithLocation` is true.
alicanerdurmaz marked this conversation as resolved.
Show resolved Hide resolved
filters: [
{
operator: "eq",
value: "published",
field: "status",
},
],
},
to: {
resource: "posts",
action: "list",
},
}}
>
Posts
</Link>
</>
);
};
```

## Props

The `<Link />` component takes all the props from the [`routerProvider.Link`](/docs/routing/router-provider/#link) and the props that an `<a>` HTML element uses.
alicanerdurmaz marked this conversation as resolved.
Show resolved Hide resolved

### go

When `go` prop is provided, this component will use [`useGo`](/docs/routing/hooks/use-go/) to create the URL to navigate to. It's accepts all the props that `useGo.go` accepts.

It's useful to use this prop when you want to navigate to a resource with a specific action.

:::caution

- `routerProvider` is required to use this prop.
- When `to` prop is provided, `go` will be ignored.

:::

### to

The URL to navigate to.

## Type support with generics

`<Link />` works with any routing library because it uses [`routerProvider.Link`](/docs/routing/router-provider/#link) internally. However, when importing it from `@refinedev/core`, it doesn't provide type support for your specific routing library. To enable full type support, you can use generics.

```tsx
import type { LinkProps } from "react-router-dom";
import { Link } from "@refinedev/core";

const MyComponent = () => {
return (
// Omit 'to' prop from LinkProps (required by react-router-dom) since we use the 'go' prop
<Link<Omit<LinkProps, "to">>
// Props from "react-router-dom"
replace={true}
unstable_viewTransition={true}
preventScrollReset={true}
// Props from "@refinedev/core"
go={{
alicanerdurmaz marked this conversation as resolved.
Show resolved Hide resolved
to: {
resource: "posts",
action: "list",
},
}}
>
Posts
</Link>
);
};
```
31 changes: 18 additions & 13 deletions documentation/docs/routing/hooks/use-link/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@
title: useLink
---

`useLink` is a hook that leverages the `Link` property of the [`routerProvider`][routerprovider] to create links compatible with the user's router library.
`useLink` is a hook that returns [`<Link />`](/docs/routing/components/link/) component. It is used to navigate to different pages in your application.

:::simple Good to know

It's recommended to use the `Link` component from your router library instead of this hook. This hook is used mostly for internal purposes and is only exposed for customization needs.

The `Link` components or the equivalents from the router libraries has better type support and lets you use the full power of the router library.
- It's recommended to use the `<Link />` component from the `@refinedev/core` package instead of this hook. This hook is used mostly for internal purposes and is only exposed for customization needs.

:::

Expand All @@ -20,14 +18,21 @@ import { useLink } from "@refinedev/core";
const MyComponent = () => {
const Link = useLink();

return <Link to="/posts">Posts</Link>;
return (
<>
<Link to="/posts">Posts</Link>
{/* or */}
<Link
go={{
to: {
resource: "posts",
action: "list",
},
}}
>
Posts
</Link>
</>
);
};
```

## Parameters

### to

This is the path that the link will navigate to. It should be a string.

[routerprovider]: /docs/routing/router-provider
6 changes: 6 additions & 0 deletions documentation/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,12 @@ module.exports = {
"routing/integrations/remix/index",
],
},
{
type: "category",
collapsed: false,
label: "Components",
items: ["routing/components/link/index"],
},
{
type: "category",
collapsed: false,
Expand Down
3 changes: 2 additions & 1 deletion packages/antd/src/hooks/table/useTable/paginationLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ export const PaginationLink = ({ to, element }: PaginationLinkProps) => {
const routerType = useRouterType();
const Link = useLink();

const ActiveLink = routerType === "legacy" ? LegacyLink : Link;
const ActiveLink =
BatuhanW marked this conversation as resolved.
Show resolved Hide resolved
routerType === "legacy" ? LegacyLink : (Link as unknown as React.FC<any>);

return (
<ActiveLink
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const srcDirPath = `${__dirname}/../../../..`;

describe("add", () => {
beforeAll(() => {
// usefull for speed up the tests.
// useful for speed up the tests.
jest.spyOn(console, "log").mockImplementation();

jest.spyOn(testTargetModule, "installInferencer").mockImplementation();
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export { RouteChangeHandler } from "./routeChangeHandler";
export { CanAccess, CanAccessProps } from "./canAccess";
export { GitHubBanner } from "./gh-banner";
export { AutoSaveIndicator, AutoSaveIndicatorProps } from "./autoSaveIndicator";
export { Link, LinkProps } from "./link";
112 changes: 112 additions & 0 deletions packages/core/src/components/link/index.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import React from "react";
import { TestWrapper, render } from "@test/index";
import { Link } from "./index";

describe("Link", () => {
describe("with `to`", () => {
it("should render a tag without router provider", () => {
const { getByText } = render(<Link to="/test">Test</Link>);

const link = getByText("Test");
expect(link.tagName).toBe("A");
expect(link.getAttribute("href")).toBe("/test");
});

it("should render a tag with router provider", () => {
const { getByTestId } = render(
<Link<{ foo: "bar" }> foo="bar" to="/test" aria-label="test-label">
Test
</Link>,
{
wrapper: TestWrapper({
routerProvider: {
Link: ({ to, children, ...props }) => (
<a href={to} data-testid="test-link" {...props}>
{children}
</a>
),
},
}),
},
);

const link = getByTestId("test-link");
expect(link.tagName).toBe("A");
expect(link.getAttribute("href")).toBe("/test");
expect(link.getAttribute("aria-label")).toBe("test-label");
expect(link.getAttribute("foo")).toBe("bar");
});
});

describe("with `go`", () => {
it("should render a tag go.to as object", () => {
const { getByTestId } = render(
<Link
go={{
to: {
resource: "test",
action: "show",
id: 1,
},
options: { keepQuery: true },
}}
aria-label="test-label"
>
Test
</Link>,
{
wrapper: TestWrapper({
resources: [{ name: "test", show: "/test/:id" }],
routerProvider: {
go: () => () => {
return "/test/1";
},
Link: ({ to, children, ...props }) => (
<a href={to} data-testid="test-link" {...props}>
{children}
</a>
),
},
}),
},
);

const link = getByTestId("test-link");
expect(link.tagName).toBe("A");
expect(link.getAttribute("href")).toBe("/test/1");
expect(link.getAttribute("aria-label")).toBe("test-label");
});

it("should render a tag go.to as string", () => {
const { getByTestId } = render(
<Link
go={{
to: "/test/1",
}}
aria-label="test-label"
>
Test
</Link>,
{
wrapper: TestWrapper({
routerProvider: {
go: () => () => {
return "/test/1";
},
Link: ({ to, children, ...props }) => (
<a href={to} data-testid="test-link" {...props}>
{children}
</a>
),
},
}),
},
);

const link = getByTestId("test-link");
expect(link.tagName).toBe("A");
expect(link.getAttribute("href")).toBe("/test/1");
expect(link.getAttribute("aria-label")).toBe("test-label");
});
});
});
Loading
Loading