Skip to content

Commit

Permalink
feat(core): create a <Link /> component to navigate to a given reso…
Browse files Browse the repository at this point in the history
…urce. (#6330)
  • Loading branch information
alicanerdurmaz committed Sep 19, 2024
1 parent 6e5d97c commit 5a81b35
Show file tree
Hide file tree
Showing 11 changed files with 380 additions and 76 deletions.
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)
97 changes: 97 additions & 0 deletions documentation/docs/routing/components/link/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
---
title: <Link />
---

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

It 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.

## 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 uses these filters to fetch data if `syncWithLocation` is true.
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. In addition to these props, it also accepts the `go`
and `to` props to navigate to a specific `resource` defined in the `<Refine />` component.

### 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"
// highlight-start
replace={true}
unstable_viewTransition={true}
preventScrollReset={true}
// highlight-end
// Props from "@refinedev/core"
go={{
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
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

0 comments on commit 5a81b35

Please sign in to comment.