Skip to content

Commit

Permalink
feat: <DocumentTitleHandler /> should populate label #6159
Browse files Browse the repository at this point in the history
Co-authored-by: Ali Emir Şen <[email protected]>
Co-authored-by: Batuhan Wilhelm <[email protected]>
  • Loading branch information
3 people authored Jul 30, 2024
1 parent 603c73e commit ad401d5
Show file tree
Hide file tree
Showing 16 changed files with 1,075 additions and 7 deletions.
44 changes: 44 additions & 0 deletions .changeset/five-pianos-march.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
"@refinedev/react-router-v6": minor
---

feat: [`<DocumentTitleHandler/>`](https://refine.dev/docs/routing/integrations/react-router/#documenttitlehandler) should populated `resource.meta.label` field if it's not provided on the Refine's resource definition.
From now on, users be able to use the `resource.meta.label` field to customize document title more easily.

```tsx
import {
BrowserRouter,
DocumentTitleHandler,
} from "@refinedev/react-router-v6";
import { Refine } from "@refinedev/core";

const App = () => {
return (
<BrowserRouter>
<Refine
/* ... */
>
{/* ... */}
<DocumentTitleHandler
handler={({ action, params, resource }) => {
const id = params?.id ?? "";

const actionPrefixMatcher = {
create: "Create new ",
clone: `#${id} Clone ${resource?.meta?.label}`,
edit: `#${id} Edit ${resource?.meta?.label}`,
show: `#${id} Show ${resource?.meta?.label}`,
list: `${resource?.meta?.label}`,
};

const suffix = " | <Company Name>";
const title = actionPrefixMatcher[action || "list"] + suffix;

return title;
}}
/>
</Refine>
</BrowserRouter>
);
};
```
44 changes: 44 additions & 0 deletions .changeset/four-carpets-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
"@refinedev/nextjs-router": minor
---

feat: [`<DocumentTitleHandler/>`](https://refine.dev/docs/routing/integrations/next-js/#documenttitlehandler) should populated `resource.meta.label` field if it's not provided on the Refine's resource definition.
From now on, users be able to use the `resource.meta.label` field to customize document title more easily.

```tsx
import {
BrowserRouter,
DocumentTitleHandler,
} from "@refinedev/react-router-v6";
import { Refine } from "@refinedev/core";

const App = () => {
return (
<BrowserRouter>
<Refine
/* ... */
>
{/* ... */}
<DocumentTitleHandler
handler={({ action, params, resource }) => {
const id = params?.id ?? "";

const actionPrefixMatcher = {
create: "Create new ",
clone: `#${id} Clone ${resource?.meta?.label}`,
edit: `#${id} Edit ${resource?.meta?.label}`,
show: `#${id} Show ${resource?.meta?.label}`,
list: `${resource?.meta?.label}`,
};

const suffix = " | <Company Name>";
const title = actionPrefixMatcher[action || "list"] + suffix;

return title;
}}
/>
</Refine>
</BrowserRouter>
);
};
```
42 changes: 42 additions & 0 deletions documentation/docs/routing/integrations/next-js/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -1563,6 +1563,48 @@ Default paths are:
- `edit`: `/resources/edit/:id`
- `show`: `/resources/show/:id`

### How to change the document title?

By default [`<DocumentTitleHandler/>`](#documenttitlehandler) component will generate the document title based on current resource and action with the "Refine" suffix. You can customize the title generation process by providing a custom `handler` function.

```tsx
import {
BrowserRouter,
DocumentTitleHandler,
} from "@refinedev/react-router-v6";
import { Refine } from "@refinedev/core";

const App = () => {
return (
<BrowserRouter>
<Refine
/* ... */
>
{/* ... */}
<DocumentTitleHandler
handler={({ action, params, resource }) => {
const id = params?.id ?? "";

const actionPrefixMatcher = {
create: "Create new ",
clone: `#${id} Clone ${resource?.meta?.label}`,
edit: `#${id} Edit ${resource?.meta?.label}`,
show: `#${id} Show ${resource?.meta?.label}`,
list: `${resource?.meta?.label}`,
};

const suffix = " | <Company Name>";
const title = actionPrefixMatcher[action || "list"] + suffix;

return title;
}}
/>
</Refine>
</BrowserRouter>
);
};
```

## Example (`/app`)

<CodeSandboxExample path="with-nextjs" />
Expand Down
40 changes: 39 additions & 1 deletion documentation/docs/routing/integrations/react-router/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -1377,6 +1377,44 @@ Default paths are:
[routerprovider]: /docs/routing/router-provider
[resources]: /docs/guides-concepts/general-concepts/#resource-concept

```
### How to change the document title?

By default [`<DocumentTitleHandler/>`](#documenttitlehandler) component will generate the document title based on current resource and action with the "Refine" suffix. You can customize the title generation process by providing a custom `handler` function.

```tsx
import {
BrowserRouter,
DocumentTitleHandler,
} from "@refinedev/react-router-v6";
import { Refine } from "@refinedev/core";

const App = () => {
return (
<BrowserRouter>
<Refine
/* ... */
>
{/* ... */}
<DocumentTitleHandler
handler={({ action, params, resource }) => {
const id = params?.id ?? "";

const actionPrefixMatcher = {
create: "Create new ",
clone: `#${id} Clone ${resource?.meta?.label}`,
edit: `#${id} Edit ${resource?.meta?.label}`,
show: `#${id} Show ${resource?.meta?.label}`,
list: `${resource?.meta?.label}`,
};

const suffix = " | <Company Name>";
const title = actionPrefixMatcher[action || "list"] + suffix;

return title;
}}
/>
</Refine>
</BrowserRouter>
);
};
```
6 changes: 6 additions & 0 deletions packages/nextjs-router/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,10 @@ module.exports = {
rootDir: "./",
displayName: "nextjs-router",
testEnvironment: "jsdom",
setupFilesAfterEnv: ["<rootDir>/src/test/jest.setup.ts"],
testPathIgnorePatterns: [
"<rootDir>/node_modules/",
"<rootDir>/example/",
"<rootDir>/dist/",
],
};
202 changes: 202 additions & 0 deletions packages/nextjs-router/src/pages/document-title-handler.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import React, { type ReactNode } from "react";

import { DocumentTitleHandler } from "./document-title-handler";
import { render, TestWrapper, type ITestWrapperProps } from "../test/index";
import { mockRouterBindings } from "../test/dataMocks";

jest.mock("next/head", () => {
return {
__esModule: true,
default: ({ children }: { children: Array<React.ReactElement> }) => {
return <>{children}</>;
},
};
});

const assertNextHeadTitle = (text: string) => {
const title = document.querySelector("title");
expect(title?.textContent).toBe(text);
};

const renderDocumentTitleHandler = (
children: ReactNode,
wrapperProps: ITestWrapperProps = {},
) => {
return render(<>{children}</>, {
wrapper: TestWrapper(wrapperProps),
});
};

describe("<DocumentTitleHandler />", () => {
it("should render default list title", async () => {
renderDocumentTitleHandler(<DocumentTitleHandler />, {
resources: [{ name: "posts", list: "/posts" }],
routerInitialEntries: ["/posts"],
routerProvider: mockRouterBindings({
action: "list",
resource: { name: "posts", list: "/posts" },
}),
});

assertNextHeadTitle("Posts | Refine");
});

it("should render default create title", async () => {
renderDocumentTitleHandler(<DocumentTitleHandler />, {
resources: [{ name: "posts", create: "/posts/create" }],
routerInitialEntries: ["/posts/create"],
routerProvider: mockRouterBindings({
action: "create",
resource: { name: "posts", create: "/posts/create" },
}),
});

assertNextHeadTitle("Create new Post | Refine");
});

it("should render default edit title", async () => {
renderDocumentTitleHandler(<DocumentTitleHandler />, {
resources: [{ name: "posts", edit: "/posts/edit/:id" }],
routerInitialEntries: ["/posts/edit/1"],
routerProvider: mockRouterBindings({
action: "edit",
resource: { name: "posts", edit: "/posts/edit/1" },
id: "1",
}),
});

assertNextHeadTitle("#1 Edit Post | Refine");
});

it("should render default show title", async () => {
renderDocumentTitleHandler(<DocumentTitleHandler />, {
resources: [{ name: "posts", show: "/posts/show/:id" }],
routerInitialEntries: ["/posts/show/1"],
routerProvider: mockRouterBindings({
action: "show",
resource: { name: "posts", show: "/posts/show/1" },
id: "1",
}),
});

assertNextHeadTitle("#1 Show Post | Refine");
});

it("should render default clone title", async () => {
renderDocumentTitleHandler(<DocumentTitleHandler />, {
resources: [{ name: "posts", clone: "/posts/clone/:id" }],
routerInitialEntries: ["/posts/clone/1"],
routerProvider: mockRouterBindings({
action: "clone",
resource: { name: "posts", clone: "/posts/clone/1" },
id: "1",
}),
});

assertNextHeadTitle("#1 Clone Post | Refine");
});

it("should render default title for unknown resource", async () => {
renderDocumentTitleHandler(<DocumentTitleHandler />, {
resources: [{ name: "posts" }],
routerInitialEntries: ["/unknown"],
routerProvider: mockRouterBindings({
action: "list",
resource: undefined,
}),
});

assertNextHeadTitle("Refine");
});

it("should render default title for unknown action", async () => {
renderDocumentTitleHandler(<DocumentTitleHandler />, {
resources: [{ name: "posts" }],
routerInitialEntries: ["/posts/unknown"],
routerProvider: mockRouterBindings({
action: undefined,
resource: {
name: "posts",
},
}),
});

assertNextHeadTitle("Refine");
});

it("should use identifier", async () => {
renderDocumentTitleHandler(<DocumentTitleHandler />, {
resources: [
{ name: "posts", list: "/posts", identifier: "Awesome Posts" },
],
routerInitialEntries: ["/posts"],
routerProvider: mockRouterBindings({
action: "list",
resource: {
name: "posts",
list: "/posts",
identifier: "Awesome Posts",
},
}),
});

assertNextHeadTitle("Awesome posts | Refine");
});

it("should render custom title", async () => {
renderDocumentTitleHandler(
<DocumentTitleHandler
handler={() => {
return "Custom Title";
}}
/>,
{
resources: [{ name: "posts", list: "/posts" }],
routerInitialEntries: ["/posts"],
routerProvider: mockRouterBindings({
action: "list",
resource: { name: "posts", list: "/posts" },
}),
},
);

assertNextHeadTitle("Custom Title");
});

it("should label be populated with friendly resource name on handler function", async () => {
renderDocumentTitleHandler(
<DocumentTitleHandler
handler={({ resource, autoGeneratedTitle }) => {
const label = resource?.label;
const labelMeta = resource?.meta?.label;

expect(labelMeta).toBe(label);
expect(labelMeta).toBe("Posts");

return autoGeneratedTitle;
}}
/>,
{
resources: [{ name: "posts", list: "/posts" }],
routerInitialEntries: ["/posts"],
routerProvider: mockRouterBindings({
action: "list",
resource: { name: "posts", list: "/posts" },
}),
},
);
});

it("should use label from resource if its provided", async () => {
renderDocumentTitleHandler(<DocumentTitleHandler />, {
resources: [{ name: "posts", list: "/posts", label: "Labeled Posts" }],
routerInitialEntries: ["/posts"],
routerProvider: mockRouterBindings({
action: "list",
resource: { name: "posts", list: "/posts", label: "Labeled Posts" },
}),
});

assertNextHeadTitle("Labeled Posts | Refine");
});
});
Loading

0 comments on commit ad401d5

Please sign in to comment.