Skip to content

Commit

Permalink
[PageContainer] Make PageContainer customizable for dynamic routes (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
Janpot authored Sep 20, 2024
1 parent ff8e7a5 commit df8c4b7
Show file tree
Hide file tree
Showing 19 changed files with 478 additions and 117 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import { PageContainer } from '@toolpad/core/PageContainer';
import { AppProvider } from '@toolpad/core/AppProvider';
import { Link, useDemoRouter } from '@toolpad/core/internals';
import { useActivePage } from '@toolpad/core/useActivePage';
import { useTheme } from '@mui/material/styles';
import Box from '@mui/material/Box';
import Paper from '@mui/material/Paper';
import invariant from 'invariant';

const NAVIGATION = [
{
segment: 'inbox',
title: 'Orders',
pattern: '/inbox/:id',
},
];

function Content({ router }) {
const id = Number(router.pathname.replace('/inbox/', ''));

const activePage = useActivePage();
invariant(activePage, 'No navigation match');

const title = `Item ${id}`;
const path = `${activePage.path}/${id}`;

const breadCrumbs = [...activePage.breadCrumbs, { title, path }];

return (
// preview-start
<PageContainer title={title} breadCrumbs={breadCrumbs}>
{/* preview-end */}
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Link href={`/inbox/${id - 1}`}>previous</Link>
<Link href={`/inbox/${id + 1}`}>next</Link>
</Box>
</PageContainer>
);
}

Content.propTypes = {
router: PropTypes.shape({
navigate: PropTypes.func.isRequired,
pathname: PropTypes.string.isRequired,
searchParams: PropTypes.instanceOf(URLSearchParams).isRequired,
}).isRequired,
};

export default function CustomPageContainer() {
const router = useDemoRouter('/inbox/123');

const theme = useTheme();

let content = (
<PageContainer>
<Link href={`/inbox/123`}>Item 123</Link>
</PageContainer>
);

if (router.pathname.startsWith('/inbox/')) {
content = <Content router={router} />;
}

return (
<AppProvider navigation={NAVIGATION} router={router} theme={theme}>
<Paper sx={{ width: '100%' }}>{content}</Paper>
</AppProvider>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import * as React from 'react';
import { PageContainer } from '@toolpad/core/PageContainer';
import { AppProvider, Router } from '@toolpad/core/AppProvider';
import { Link, useDemoRouter } from '@toolpad/core/internals';
import { useActivePage } from '@toolpad/core/useActivePage';
import { useTheme } from '@mui/material/styles';
import Box from '@mui/material/Box';
import Paper from '@mui/material/Paper';
import invariant from 'invariant';

const NAVIGATION = [
{
segment: 'inbox',
title: 'Orders',
pattern: '/inbox/:id',
},
];

interface ContentProps {
router: Router;
}

function Content({ router }: ContentProps) {
const id = Number(router.pathname.replace('/inbox/', ''));

const activePage = useActivePage();
invariant(activePage, 'No navigation match');

const title = `Item ${id}`;
const path = `${activePage.path}/${id}`;

const breadCrumbs = [...activePage.breadCrumbs, { title, path }];

return (
// preview-start
<PageContainer title={title} breadCrumbs={breadCrumbs}>
{/* preview-end */}
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Link href={`/inbox/${id - 1}`}>previous</Link>
<Link href={`/inbox/${id + 1}`}>next</Link>
</Box>
</PageContainer>
);
}

export default function CustomPageContainer() {
const router = useDemoRouter('/inbox/123');

const theme = useTheme();

let content = (
<PageContainer>
<Link href={`/inbox/123`}>Item 123</Link>
</PageContainer>
);

if (router.pathname.startsWith('/inbox/')) {
content = <Content router={router} />;
}

return (
<AppProvider navigation={NAVIGATION} router={router} theme={theme}>
<Paper sx={{ width: '100%' }}>{content}</Paper>
</AppProvider>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<PageContainer title={title} breadCrumbs={breadCrumbs}>
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useTheme } from '@mui/material/styles';
import Paper from '@mui/material/Paper';

const NAVIGATION = [
{ segment: '', title: 'ACME' },
{
segment: 'inbox',
title: 'Home',
Expand All @@ -24,12 +25,7 @@ export default function TitleBreadcrumbsPageContainer() {
const theme = useTheme();

return (
<AppProvider
branding={{ title: 'ACME' }}
navigation={NAVIGATION}
router={router}
theme={theme}
>
<AppProvider navigation={NAVIGATION} router={router} theme={theme}>
<Paper sx={{ width: '100%' }}>
<PageContainer />
</Paper>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useTheme } from '@mui/material/styles';
import Paper from '@mui/material/Paper';

const NAVIGATION = [
{ segment: '', title: 'ACME' },
{
segment: 'inbox',
title: 'Home',
Expand All @@ -24,12 +25,7 @@ export default function TitleBreadcrumbsPageContainer() {
const theme = useTheme();

return (
<AppProvider
branding={{ title: 'ACME' }}
navigation={NAVIGATION}
router={router}
theme={theme}
>
<AppProvider navigation={NAVIGATION} router={router} theme={theme}>
<Paper sx={{ width: '100%' }}>
<PageContainer />
</Paper>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ For example, under the following navigation structure:

```tsx
<AppProvider
branding={{ title: 'ACME' }}
navigation={[
{
segment: 'home',
Expand All @@ -44,6 +43,59 @@ The breadcrumbs contains **ACME / Home / Orders** when you visit the path **/hom

{{"demo": "TitleBreadcrumbsPageContainer.js", "height": 300, "hideToolbar": true}}

## Dynamic Routes

When you use the `PageContainer` on a dynamic route, you'll likely want to set a title and breadcrumbs belonging to the specific path. You can achieve this with the `title` and `breadCrumbs` property of the `PageContainer`

{{"demo": "CustomPageContainer.js", "height": 300}}

You can use the `useActivePage` hook to retrieve the title and breadcrumbs of the active page. This way you can extend the existing values.

```tsx
import { useActivePage } from '@toolpad/core/useActivePage';
import { BreadCrumb } from '@toolpad/core/PageContainer';

// Pass the id from your router of choice
function useDynamicBreadCrumbs(id: string): BreadCrumb[] {
const activePage = useActivePage();
invariant(activePage, 'No navigation match');

const title = `Item ${id}`;
const path = `${activePage.path}/${id}`;

return [...activePage.breadCrumbs, { title, path }];
}
```

For example, under the Next.js app router you would be able to obtain breadcrumbs for a dynamic route as follows:

```tsx
// ./src/app/example/[id]/page.tsx
'use client';

import { useParams } from 'next/navigation';
import { PageContainer } from '@toolpad/core/PageContainer';
import invariant from 'invariant';
import { useActivePage } from '@toolpad/core/useActivePage';

export default function Example() {
const params = useParams<{ id: string }>();
const activePage = useActivePage();
invariant(activePage, 'No navigation match');

const title = `Item ${params.id}`;
const path = `${activePage.path}/${params.id}`;

const breadCrumbs = [...activePage.breadCrumbs, { title, path }];

return (
<PageContainer title={title} breadCrumbs={breadCrumbs}>
...
</PageContainer>
);
}
```

## Actions

You can configure additional actions in the area that is reserved on the right. To do so provide the `toolbar` slot to the `PageContainer` component. You can wrap the `PageContainerToolbar` component to create a custom toolbar component, as shown here:
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/toolpad/core/api/app-provider.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"router": {
"type": {
"name": "shape",
"description": "{ navigate: func, pathname: string, searchParams?: URLSearchParams }"
"description": "{ navigate: func, pathname: string, searchParams: URLSearchParams }"
},
"default": "null"
},
Expand Down
12 changes: 11 additions & 1 deletion docs/pages/toolpad/core/api/page-container.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
{
"props": {},
"props": {
"breadCrumbs": {
"type": { "name": "arrayOf", "description": "Array&lt;{ path: string, title: string }&gt;" }
},
"slotProps": { "type": { "name": "shape", "description": "{ toolbar: { children?: node } }" } },
"slots": {
"type": { "name": "shape", "description": "{ toolbar?: elementType }" },
"additionalInfo": { "slotsApi": true }
},
"title": { "type": { "name": "string" } }
},
"name": "PageContainer",
"imports": [
"import { PageContainer } from '@toolpad/core/PageContainer';",
Expand Down
11 changes: 9 additions & 2 deletions docs/translations/api-docs/page-container/page-container.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
{
"componentDescription": "",
"propDescriptions": {},
"componentDescription": "A container component to provide a title and breadcrumbs for your pages.",
"propDescriptions": {
"breadCrumbs": {
"description": "The breadcrumbs of the page. Leave blank to use the active page breadcrumbs."
},
"slotProps": { "description": "The props used for each slot inside." },
"slots": { "description": "The components used for each slot inside." },
"title": { "description": "The title of the page. Leave blank to use the active page title." }
},
"classDescriptions": {
"disableGutters": {
"description": "Styles applied to {{nodeName}} if {{conditions}}.",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
"@mui/internal-babel-plugin-resolve-imports": "1.0.18",
"@mui/internal-docs-utils": "1.0.13",
"@mui/internal-markdown": "1.0.14",
"@mui/internal-scripts": "1.0.21",
"@mui/internal-scripts": "1.0.21-dev.20240919-130050-82a6448768",
"@mui/monorepo": "github:mui/material-ui#3732be30e0ae3287b55dc6a96cf1ffcfdc194906",
"@mui/x-charts": "7.17.0",
"@next/eslint-plugin-next": "14.2.11",
Expand Down
4 changes: 2 additions & 2 deletions packages/toolpad-core/src/AppProvider/AppProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -239,10 +239,10 @@ AppProvider.propTypes /* remove-proptypes */ = {
* Router implementation used inside Toolpad components.
* @default null
*/
router: PropTypes /* @typescript-to-proptypes-ignore */.shape({
router: PropTypes.shape({
navigate: PropTypes.func.isRequired,
pathname: PropTypes.string.isRequired,
searchParams: PropTypes.instanceOf(URLSearchParams),
searchParams: PropTypes.instanceOf(URLSearchParams).isRequired,
}),
/**
* Session info about the current user.
Expand Down
33 changes: 33 additions & 0 deletions packages/toolpad-core/src/PageContainer/PageContainer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ describe('PageContainer', () => {

test('renders nested', async () => {
const navigation = [
{ segment: '', title: 'ACME' },
{
segment: 'home',
title: 'Home',
Expand Down Expand Up @@ -79,4 +80,36 @@ describe('PageContainer', () => {
expect(within(breadCrumbs).getByText('Home')).toBeTruthy();
expect(within(breadCrumbs).getByText('Orders')).toBeTruthy();
});

test('renders dynamic correctly', async () => {
const user = await userEvent.setup();
const router = {
pathname: '/orders/123',
searchParams: new URLSearchParams(),
navigate: vi.fn(),
};
render(
<AppProvider
navigation={[
{ segment: '', title: 'Home' },
{ segment: 'orders', title: 'Orders', pattern: '/orders/:id' },
]}
router={router}
>
<PageContainer />
</AppProvider>,
);

const breadCrumbs = screen.getByRole('navigation', { name: 'breadcrumb' });

const homeLink = within(breadCrumbs).getByRole('link', { name: 'Home' });
await user.click(homeLink);

expect(router.navigate).toHaveBeenCalledWith('/', expect.objectContaining({}));
router.navigate.mockClear();

expect(within(breadCrumbs).getByText('Orders')).toBeTruthy();

expect(screen.getByText('Orders', { ignore: 'nav *' }));
});
});
Loading

0 comments on commit df8c4b7

Please sign in to comment.