Skip to content

feat: add PrismicTable component #219

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

Merged
merged 6 commits into from
Feb 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions e2e-projects/nextjs/app/PrismicTable/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { isFilled } from "@prismicio/client";
import { PrismicTable } from "@prismicio/react";
import assert from "assert";

import { createClient } from "@/prismicio";

export default async function Page() {
const client = await createClient();
const { data: tests } = await client.getSingle("table_test");

assert(isFilled.table(tests.filled));
assert(!isFilled.table(tests.empty));

return (
<>
<div data-testid="filled">
<PrismicTable field={tests.filled} />
</div>

<div data-testid="empty">
<PrismicTable field={tests.empty} />
</div>

<div data-testid="fallback">
<PrismicTable field={tests.empty} fallback={<div>Table</div>} />
</div>

<div data-testid="custom-table">
<PrismicTable
field={tests.filled}
components={{
table: ({ children }) => <div className="table">{children}</div>,
thead: ({ children }) => <div className="head">{children}</div>,
tbody: ({ children }) => <div className="body">{children}</div>,
tr: ({ children }) => <div className="row">{children}</div>,
th: ({ children }) => <div className="header">{children}</div>,
td: ({ children }) => <div className="data">{children}</div>,
}}
/>
</div>

<div data-testid="custom-cell-content">
<PrismicTable
field={tests.filled}
components={{
table: ({ children }) => (
<table className="table">{children}</table>
),
paragraph: ({ children }) => (
<p className="paragraph">{children}</p>
),
strong: ({ children }) => (
<span className="strong">{children}</span>
),
em: ({ children }) => <span className="em">{children}</span>,
}}
/>
</div>
</>
);
}
4 changes: 2 additions & 2 deletions e2e-projects/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
"start": "next start --port=4321"
},
"dependencies": {
"@prismicio/client": "^7.13.1",
"@prismicio/client": "^7.16.0",
"@prismicio/react": "*",
"next": "15.1.2",
"react": "19.0.0",
"react-dom": "19.0.0"
},
"devDependencies": {
"@prismicio/types-internal": "^3.3.0",
"@prismicio/types-internal": "^3.6.0",
"@types/node": "^22",
"@types/react": "^19",
"@types/react-dom": "^19",
Expand Down
44 changes: 43 additions & 1 deletion e2e-projects/nextjs/prismic-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -485,11 +485,53 @@ export type RichTextTestDocument<Lang extends string = string> =
Lang
>;

/** Content for Table Test documents */
interface TableTestDocumentData {
/**
* Empty field in _Table Test_
*
* - **Field Type**: Table
* - **Placeholder**: _None_
* - **API ID Path**: table_test.empty
* - **Tab**: Main
* - **Documentation**: https://prismic.io/docs/field#table
*/
empty: prismic.TableField;

/**
* Filled field in _Table Test_
*
* - **Field Type**: Table
* - **Placeholder**: _None_
* - **API ID Path**: table_test.filled
* - **Tab**: Main
* - **Documentation**: https://prismic.io/docs/field#table
*/
filled: prismic.TableField;
}

/**
* Table Test document from Prismic
*
* - **API ID**: `table_test`
* - **Repeatable**: `false`
* - **Documentation**: https://prismic.io/docs/custom-types
*
* @typeParam Lang - Language API ID of the document.
*/
export type TableTestDocument<Lang extends string = string> =
prismic.PrismicDocumentWithoutUID<
Simplify<TableTestDocumentData>,
"table_test",
Lang
>;

export type AllDocumentTypes =
| ImageTestDocument
| LinkTestDocument
| PageDocument
| RichTextTestDocument;
| RichTextTestDocument
| TableTestDocument;

/** Primary content in _Image → Default → Primary_ */
export interface ImageSliceDefaultPrimary {
Expand Down
20 changes: 11 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"devDependencies": {
"@eslint/js": "^9.19.0",
"@playwright/test": "^1.50.0",
"@prismicio/client": "^7.12.0",
"@prismicio/client": "^7.16.1",
"@rollup/plugin-typescript": "^12.1.2",
"@size-limit/preset-small-lib": "^11.1.6",
"@types/react": "^19.0.8",
Expand Down
165 changes: 165 additions & 0 deletions src/PrismicTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { ReactNode } from "react";
import {
isFilled,
TableField,
TableFieldHead,
TableFieldHeadRow,
TableFieldBody,
TableFieldBodyRow,
TableFieldHeaderCell,
TableFieldDataCell,
} from "@prismicio/client";

import { JSXMapSerializer, PrismicRichText } from "./PrismicRichText.js";

type TableComponents = {
table?: (props: {
table: TableField<"filled">;
children: ReactNode;
}) => ReactNode;
thead?: (props: { head: TableFieldHead; children: ReactNode }) => ReactNode;
tbody?: (props: { body: TableFieldBody; children: ReactNode }) => ReactNode;
tr?: (props: {
row: TableFieldHeadRow | TableFieldBodyRow;
children: ReactNode;
}) => ReactNode;
th?: (props: {
cell: TableFieldHeaderCell;
children: ReactNode;
}) => ReactNode;
td?: (props: { cell: TableFieldDataCell; children: ReactNode }) => ReactNode;
};

const defaultComponents: Required<TableComponents> = {
table: ({ children }) => <table>{children}</table>,
thead: ({ children }) => <thead>{children}</thead>,
tbody: ({ children }) => <tbody>{children}</tbody>,
tr: ({ children }) => <tr>{children}</tr>,
th: ({ children }) => <th>{children}</th>,
td: ({ children }) => <td>{children}</td>,
};

/** Props for `<PrismicTable>`. */
export type PrismicTableProps = {
/** The Prismic table field to render. */
field: TableField;

/**
* An object that maps a table block to a React component.
*
* @example A map serializer.
*
* ```jsx
* {
* table: ({children}) => <table>{children}</table>
* thead: ({children}) => <thead>{children}</thead>
* }
* ```
*/
components?: JSXMapSerializer & TableComponents;

/**
* The value to be rendered when the field is empty. If a fallback is not
* given, `null` will be rendered.
*/
fallback?: ReactNode;
};

/**
* React component that renders content from a Prismic table field. By default,
* HTML elements are rendered for each piece of content. A `tbody` block will
* render a `<tbody>` HTML element, for example.
*
* To customize the components that are rendered, provide a map serializer to
* the `components` prop.
*
* @example Rendering a table field using the default HTMl elements.
*
* ```jsx
* <PrismicTable field={document.data.my_table} />;
* ```
*
* @example Rendering a table field using a custom set of React components.
*
* ```jsx
* <PrismicTable
* field={document.data.my_table}
* components={{
* tbody: ({ children }) => (
* <tbody className="my-class">{children}</tbody>
* ),
* }}
* />;
* ```
*
* @param props - Props for the component.
*
* @returns The table field's content as React components.
*
* @see Learn about table fields {@link https://prismic.io/docs/core-concepts/table}
*/
export function PrismicTable(props: PrismicTableProps) {
const { field, components, fallback = null } = props;

if (!isFilled.table(field)) {
return fallback;
}

const {
table: Table,
thead: Thead,
tbody: Tbody,
} = { ...defaultComponents, ...components };

return (
<Table table={field}>
{field.head && (
<Thead head={field.head}>
{field.head.rows.map((row) => (
<TableRow
key={JSON.stringify(row)}
row={row}
components={components}
/>
))}
</Thead>
)}
<Tbody body={field.body}>
{field.body.rows.map((row) => (
<TableRow
key={JSON.stringify(row)}
row={row}
components={components}
/>
))}
</Tbody>
</Table>
);
}

type TableRowProps = {
row: TableFieldHeadRow | TableFieldBodyRow;
components?: JSXMapSerializer & TableComponents;
};

function TableRow(props: TableRowProps) {
const { row, components } = props;

const { tr: Tr, th: Th, td: Td } = { ...defaultComponents, ...components };

return (
<Tr row={row}>
{row.cells.map((cell) =>
cell.type === "header" ? (
<Th key={JSON.stringify(cell)} cell={cell}>
<PrismicRichText field={cell.content} components={components} />
</Th>
) : (
<Td key={JSON.stringify(cell)} cell={cell}>
<PrismicRichText field={cell.content} components={components} />
</Td>
),
)}
</Tr>
);
}
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
export { PrismicLink } from "./PrismicLink.js";
export type { PrismicLinkProps, LinkProps } from "./PrismicLink.js";

export { PrismicTable } from "./PrismicTable.js";
export type { PrismicTableProps } from "./PrismicTable.js";

export { PrismicText } from "./PrismicText.js";
export type { PrismicTextProps } from "./PrismicText.js";

Expand Down
Loading
Loading