PDF Export System¶
Overview¶
The PDF export system provides a flexible, template-based architecture for generating PDF documents from application data. Built on top of @react-pdf/renderer, it supports custom templates, internationalization, and various formatting options.
Architecture¶
packages/app/src/lib/pdfExporter/
├── core/ # Core PDF components and styles
│ ├── PdfDocument.tsx # Base document wrapper
│ ├── PdfPage.tsx # Page component with margins
│ └── PdfStyles.ts # Shared styles and theme
├── components/ # Reusable PDF components
│ ├── PdfHeader.tsx # Header with title/subtitle
│ ├── PdfSection.tsx # Section with title
│ ├── PdfTable.tsx # Table component
│ └── PdfText.tsx # Text with styling options
├── utils/ # Utility functions
│ ├── formatters.ts # Date, currency, number formatting
│ └── generatePdf.ts # PDF generation and download
├── hooks/ # React hooks
│ └── usePdfExport.ts # Export hook with progress
├── templates/ # PDF templates
│ └── DataTableTemplate.tsx # DataTable export template
└── types.ts # TypeScript definitions
Quick Start¶
Basic Usage¶
import { usePdfExport, DataTableTemplate } from '@/lib/pdfExporter';
function MyComponent() {
const { exportPdf, isExporting, progress, error } = usePdfExport();
const handleExport = async () => {
await exportPdf({
template: DataTableTemplate,
data: { table, title: 'My Report' },
filename: 'report.pdf',
options: {
locale: 'en-US',
},
});
};
return (
<button onClick={handleExport} disabled={isExporting}>
{isExporting ? `Exporting... ${progress}%` : 'Export PDF'}
</button>
);
}
DataTable Export¶
For DataTable components, use the specialized hook:
import { useDataTablePdfExport } from '@/components/ui/DataTable/utils/export/pdfExporter';
function DataTableComponent() {
const { exportDataTable, isExporting } = useDataTablePdfExport();
const handleExport = async () => {
await exportDataTable(table, {
filename: 'data-export.pdf',
title: 'Sales Report',
companyName: 'Acme Corp',
includeDate: true,
includeFooter: true,
visibleColumnsOnly: true,
exportSelection: EXPORT_SELECTION.ALL,
locale: 'en-US',
});
};
return (
<button onClick={handleExport} disabled={isExporting}>
Export to PDF
</button>
);
}
Creating Custom Templates¶
Template Structure¶
A PDF template is a React component that returns @react-pdf/renderer elements:
import { ReactElement } from 'react';
import { View, Text } from '@react-pdf/renderer';
import {
PdfDocument,
PdfPage,
PdfHeader,
PdfSection,
PdfTable,
PdfText,
type PdfTemplate,
type PdfDocumentConfig,
} from '@/lib/pdfExporter';
interface MyTemplateData {
title: string;
items: Array<{ name: string; value: number }>;
date: Date;
}
function renderMyTemplate(
data: MyTemplateData,
config: PdfDocumentConfig
): ReactElement {
const { locale = 'en-US', currency = 'USD' } = config;
return (
<PdfDocument title={data.title}>
<PdfPage>
<PdfHeader title={data.title} />
<PdfSection title="Summary">
<PdfText label="Date">
{data.date.toLocaleDateString(locale)}
</PdfText>
</PdfSection>
<PdfTable
headers={['Item', 'Value']}
rows={data.items.map(item => [
item.name,
formatPdfCurrency(item.value, currency, locale),
])}
alignments={['left', 'right']}
/>
</PdfPage>
</PdfDocument>
);
}
export const MyTemplate: PdfTemplate = {
name: 'MyTemplate',
render: renderMyTemplate as (data: unknown, config: PdfDocumentConfig) => ReactElement,
};
Using Custom Templates¶
import { usePdfExport } from '@/lib/pdfExporter';
import { MyTemplate } from './templates/MyTemplate';
function ExportButton() {
const { exportPdf } = usePdfExport();
const handleExport = async () => {
const data = {
title: 'Monthly Report',
items: [
{ name: 'Product A', value: 100 },
{ name: 'Product B', value: 200 },
],
date: new Date(),
};
await exportPdf({
template: MyTemplate,
data,
filename: 'monthly-report.pdf',
options: {
locale: 'en-US',
},
});
};
return <button onClick={handleExport}>Export Report</button>;
}
Component API¶
Core Components¶
PdfDocument¶
Base document wrapper with metadata.
<PdfDocument
title="Report Title"
author="John Doe"
subject="Monthly Sales"
keywords="sales, report, monthly"
>
{/* pages */}
</PdfDocument>
PdfPage¶
Page component with configurable margins.
<PdfPage margin={20}>
{/* content */}
</PdfPage>
Layout Components¶
PdfHeader¶
Header with title and optional subtitle.
<PdfHeader
title="Main Title"
subtitle="Subtitle text"
/>
PdfSection¶
Section with title and content.
<PdfSection title="Section Title">
{/* section content */}
</PdfSection>
Content Components¶
PdfText¶
Text component with styling options.
<PdfText
label="Field Label"
bold={true}
muted={false}
small={false}
large={false}
>
Text content
</PdfText>
PdfTable¶
Table component with headers and rows.
<PdfTable
headers={['Column 1', 'Column 2', 'Column 3']}
rows={[
['Row 1 Col 1', 'Row 1 Col 2', 'Row 1 Col 3'],
['Row 2 Col 1', 'Row 2 Col 2', 'Row 2 Col 3'],
]}
alignments={['left', 'center', 'right']}
columnWidths={[100, 150, 100]}
/>
Utility Functions¶
Formatters¶
import {
formatPdfCurrency,
formatPdfDate,
formatPdfNumber,
formatPdfPercentage,
} from '@/lib/pdfExporter/utils';
// Currency formatting
formatPdfCurrency(1234.56, 'USD', 'en-US'); // "$1,234.56"
formatPdfCurrency(1234.56, 'EUR', 'de-DE'); // "1.234,56 €"
// Date formatting
formatPdfDate(new Date(), 'en-US'); // "1/15/2024"
formatPdfDate(new Date(), 'de-DE'); // "15.1.2024"
// Number formatting
formatPdfNumber(1234.56, 'en-US'); // "1,234.56"
formatPdfNumber(1234.56, 'de-DE'); // "1.234,56"
// Percentage formatting
formatPdfPercentage(0.85, 'en-US'); // "85%"
PDF Generation¶
import {
generatePdf,
downloadPdf,
generateAndDownloadPdf,
} from '@/lib/pdfExporter/utils';
// Generate PDF blob
const blob = await generatePdf(document, {
onProgress: (progress) => console.log(`Progress: ${progress * 100}%`),
onComplete: () => console.log('Complete'),
onError: (error) => console.error('Error:', error),
});
// Download blob as PDF
downloadPdf(blob, 'document.pdf');
// Generate and download in one step
await generateAndDownloadPdf(document, 'document.pdf');
Hooks¶
usePdfExport¶
Main export hook with progress tracking.
const {
exportPdf, // Export function
generatePdfBlob, // Generate blob without downloading
isExporting, // Loading state
progress, // Progress (0-100)
error, // Error state
} = usePdfExport();
Styling¶
Theme Configuration¶
Customize the default theme in PdfStyles.ts:
export const defaultTheme = {
colors: {
primary: '#000000',
secondary: '#666666',
muted: '#999999',
border: '#e5e7eb',
background: '#f9fafb',
},
fontSize: {
small: 8,
normal: 10,
medium: 12,
large: 14,
xlarge: 16,
title: 20,
},
spacing: {
xs: 2,
sm: 4,
md: 8,
lg: 12,
xl: 16,
},
};
Custom Styles¶
Create custom styles using the createStyles function:
import { createStyles } from '@/lib/pdfExporter/core';
const customStyles = createStyles({
colors: {
primary: '#007bff',
secondary: '#6c757d',
},
fontSize: {
normal: 11,
large: 15,
},
});
Internationalization¶
The system supports full internationalization for dates, numbers, and currency:
await exportPdf({
template: MyTemplate,
data: myData,
filename: 'report.pdf',
options: {
locale: 'fr-FR', // French locale
currency: 'EUR', // Euro currency
},
});
Migration from jsPDF¶
If you're migrating from the old jsPDF-based system:
- DataTable exports are automatically compatible - no changes needed
- Custom exports need to be converted to templates:
// Old jsPDF approach
const doc = new jsPDF();
doc.text('Hello World', 10, 10);
doc.save('document.pdf');
// New template approach
const SimpleTemplate: PdfTemplate = {
name: 'Simple',
render: (data) => (
<PdfDocument>
<PdfPage>
<PdfText>Hello World</PdfText>
</PdfPage>
</PdfDocument>
),
};
await exportPdf({
template: SimpleTemplate,
data: {},
filename: 'document.pdf',
});
Best Practices¶
- Template Organization: Keep templates in
/lib/pdfExporter/templates/ - Type Safety: Always type your template data interfaces
- Internationalization: Use locale-aware formatters for dates and numbers
- Performance: For large documents, consider pagination
- Error Handling: Always handle export errors in your UI
- Progress Feedback: Show progress for better UX
- Accessibility: Provide alternative formats when possible
Examples¶
Invoice Template¶
interface InvoiceData {
invoiceNumber: string;
date: Date;
customer: {
name: string;
address: string;
};
items: Array<{
description: string;
quantity: number;
price: number;
}>;
}
function renderInvoice(data: InvoiceData, config: PdfDocumentConfig): ReactElement {
const { locale, currency } = config;
const total = data.items.reduce((sum, item) => sum + item.quantity * item.price, 0);
return (
<PdfDocument title={`Invoice ${data.invoiceNumber}`}>
<PdfPage>
<PdfHeader title={`Invoice #${data.invoiceNumber}`} />
<PdfSection title="Customer">
<PdfText>{data.customer.name}</PdfText>
<PdfText muted>{data.customer.address}</PdfText>
</PdfSection>
<PdfSection title="Items">
<PdfTable
headers={['Description', 'Qty', 'Price', 'Total']}
rows={data.items.map(item => [
item.description,
String(item.quantity),
formatPdfCurrency(item.price, currency, locale),
formatPdfCurrency(item.quantity * item.price, currency, locale),
])}
alignments={['left', 'center', 'right', 'right']}
/>
</PdfSection>
<PdfText bold large>
Total: {formatPdfCurrency(total, currency, locale)}
</PdfText>
</PdfPage>
</PdfDocument>
);
}
export const InvoiceTemplate: PdfTemplate = {
name: 'Invoice',
render: renderInvoice as (data: unknown, config: PdfDocumentConfig) => ReactElement,
};
Troubleshooting¶
Common Issues¶
- Fonts not displaying correctly
- The system uses built-in fonts from @react-pdf/renderer
-
Custom fonts can be registered if needed
-
Large file sizes
- Optimize images before including them
-
Consider compression options
-
Performance issues
- Use pagination for large datasets
-
Implement virtual scrolling for preview
-
TypeScript errors
- Ensure all template data is properly typed
- Cast template render functions as shown in examples
Future Enhancements¶
- [ ] Add support for images and charts
- [ ] Implement PDF preview component
- [ ] Add watermark support
- [ ] Support for digital signatures
- [ ] Advanced layout options (columns, grids)
- [ ] PDF/A compliance for archival