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: add PrismicTable component #219

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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
112 changes: 112 additions & 0 deletions src/PrismicTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
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>,
};

export type PrismicTableProps = {
field: TableField;
components?: JSXMapSerializer & TableComponents;
fallback?: ReactNode;
};

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
36 changes: 36 additions & 0 deletions tests/PrismicTable.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { test, expect } from "./infra";

test.beforeEach(async ({ page }) => {
await page.goto("/PrismicTable");
});

test("renders filled table elements", async ({ page }) => {
const output = page.getByTestId("filled");
expect(await output.innerHTML()).toBe(
"<table><thead><tr><th><p>Method</p></th><th><p>Usage</p></th></tr></thead><tbody><tr><th><p>GET</p></th><td><p>For <strong>basic retrieval</strong> of information…</p></td></tr><tr><th><p>DELETE</p></th><td><p>To <em>dest</em>roy a resource and remove…</p></td></tr></tbody></table>",
);
});

test("renders null when passed an empty field", async ({ page }) => {
const output = page.getByTestId("empty");
await expect(output).toBeEmpty();
});

test("renders fallback when passed an empty field", async ({ page }) => {
const output = page.getByTestId("fallback");
expect(await output.innerHTML()).toBe("<div>Table</div>");
});

test("renders custom table elements", async ({ page }) => {
const output = page.getByTestId("custom-table");
expect(await output.innerHTML()).toBe(
'<div class="table"><div class="head"><div class="row"><div class="header"><p>Method</p></div><div class="header"><p>Usage</p></div></div></div><div class="body"><div class="row"><div class="header"><p>GET</p></div><div class="data"><p>For <strong>basic retrieval</strong> of information…</p></div></div><div class="row"><div class="header"><p>DELETE</p></div><div class="data"><p>To <em>dest</em>roy a resource and remove…</p></div></div></div></div>',
);
});

test("renders custom table cell content", async ({ page }) => {
const output = page.getByTestId("custom-cell-content");
expect(await output.innerHTML()).toBe(
'<table class="table"><thead><tr><th><p class="paragraph">Method</p></th><th><p class="paragraph">Usage</p></th></tr></thead><tbody><tr><th><p class="paragraph">GET</p></th><td><p class="paragraph">For <span class="strong">basic retrieval</span> of information…</p></td></tr><tr><th><p class="paragraph">DELETE</p></th><td><p class="paragraph">To <span class="em">dest</span>roy a resource and remove…</p></td></tr></tbody></table>',
);
});
1 change: 1 addition & 0 deletions tests/infra/content/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * as image from "./image";
export * as link from "./link";
export * as page from "./page";
export * as richText from "./richtext";
export * as table from "./table";
Loading
Loading