Skip to content

Commit

Permalink
feat: added Form Annotation support (#2845)
Browse files Browse the repository at this point in the history
  • Loading branch information
natterstefan authored Feb 5, 2025
1 parent e5968c6 commit 8cd872f
Show file tree
Hide file tree
Showing 14 changed files with 693 additions and 0 deletions.
136 changes: 136 additions & 0 deletions packages/examples/next-14/app/form/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
'use client';

import dynamic from 'next/dynamic';
import {
Document,
Page,
View,
Text,
Checkbox,
FormField,
TextInput,
Select,
List,
} from '@react-pdf/renderer';

const PDFViewer = dynamic(
() => import('@react-pdf/renderer').then((mod) => mod.PDFViewer),
{
ssr: false,
loading: () => <p>Loading...</p>,
},
);

export default function Form() {
const doc = (
<Document>
<Page>
<View
style={{
backgroundColor: 'rgba(182,28,28,0.62)',
width: '30%',
height: '100%',
}}
>
<FormField name="user-info" style={{ flexDirection: 'column' }}>
<Text>TextInput</Text>
<TextInput
name="username"
value="foo"
align="center"
style={{ height: '50px' }}
/>

{/* Nested works as well */}
<View>
<Text>TextInput</Text>
<TextInput
name="password"
value="bar"
align="center"
style={{ height: '50px' }}
password
/>
</View>

<Text>Checkbox (not checked)</Text>
<Checkbox name="checkbox-default" style={{ height: '20px' }} />

<Text>Checkbox (checked)</Text>
<Checkbox
name="checkbox-checked"
checked
style={{ height: '20px' }}
/>

<Text>Select</Text>
<Select
name="combo"
select={['', 'option 1', 'option 2']}
value=""
defaultValue=""
style={{ height: '20px' }}
/>

<Text>List</Text>
<List
name="list"
select={['', 'option 1', 'option 2']}
value=""
defaultValue=""
style={{ height: '50px' }}
/>
</FormField>
</View>
</Page>

<Page>
<View
style={{
backgroundColor: 'rgba(182,28,28,0.62)',
width: '30%',
height: '100%',
}}
>
<FormField name="user-details" style={{ flexDirection: 'column' }}>
<Text>TextInput (multiline)</Text>
<TextInput
name="details"
value="hello"
align="center"
multiline
style={{ fontSize: 8, height: '100px' }}
/>
</FormField>
</View>
</Page>

<Page>
<View
style={{
backgroundColor: 'rgba(182,28,28,0.62)',
width: '30%',
height: '100%',
}}
>
<Text>TextInput (no FormField)</Text>
<TextInput
name="textinput-no-formfield"
value="no formfield"
align="center"
style={{ height: '50px' }}
/>

<Text>Checkbox (checked, no FormField)</Text>
<Checkbox
name="checkbox-no-formfield"
checked
style={{ height: '20px' }}
/>
</View>
</Page>
</Document>
);

return <PDFViewer className="w-full h-svh">{doc}</PDFViewer>;
}
136 changes: 136 additions & 0 deletions packages/examples/next-15/app/form/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
'use client';

import dynamic from 'next/dynamic';
import {
Document,
Page,
View,
Text,
Checkbox,
FormField,
TextInput,
Select,
List,
} from '@react-pdf/renderer';

const PDFViewer = dynamic(
() => import('@react-pdf/renderer').then((mod) => mod.PDFViewer),
{
ssr: false,
loading: () => <p>Loading...</p>,
},
);

export default function Form() {
const doc = (
<Document>
<Page>
<View
style={{
backgroundColor: 'rgba(182,28,28,0.62)',
width: '30%',
height: '100%',
}}
>
<FormField name="user-info" style={{ flexDirection: 'column' }}>
<Text>TextInput</Text>
<TextInput
name="username"
value="foo"
align="center"
style={{ height: '50px' }}
/>

{/* Nested works as well */}
<View>
<Text>TextInput</Text>
<TextInput
name="password"
value="bar"
align="center"
style={{ height: '50px' }}
password
/>
</View>

<Text>Checkbox (not checked)</Text>
<Checkbox name="checkbox-default" style={{ height: '20px' }} />

<Text>Checkbox (checked)</Text>
<Checkbox
name="checkbox-checked"
checked
style={{ height: '20px' }}
/>

<Text>Select</Text>
<Select
name="combo"
select={['', 'option 1', 'option 2']}
value=""
defaultValue=""
style={{ height: '20px' }}
/>

<Text>List</Text>
<List
name="list"
select={['', 'option 1', 'option 2']}
value=""
defaultValue=""
style={{ height: '50px' }}
/>
</FormField>
</View>
</Page>

<Page>
<View
style={{
backgroundColor: 'rgba(182,28,28,0.62)',
width: '30%',
height: '100%',
}}
>
<FormField name="user-details" style={{ flexDirection: 'column' }}>
<Text>TextInput (multiline)</Text>
<TextInput
name="details"
value="hello"
align="center"
multiline
style={{ fontSize: 8, height: '100px' }}
/>
</FormField>
</View>
</Page>

<Page>
<View
style={{
backgroundColor: 'rgba(182,28,28,0.62)',
width: '30%',
height: '100%',
}}
>
<Text>TextInput (no FormField)</Text>
<TextInput
name="textinput-no-formfield"
value="no formfield"
align="center"
style={{ height: '50px' }}
/>

<Text>Checkbox (checked, no FormField)</Text>
<Checkbox
name="checkbox-no-formfield"
checked
style={{ height: '20px' }}
/>
</View>
</Page>
</Document>
);

return <PDFViewer className="w-full h-svh">{doc}</PDFViewer>;
}
5 changes: 5 additions & 0 deletions packages/primitives/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ export const Note = 'NOTE';
export const Path = 'PATH';
export const Rect = 'RECT';
export const Line = 'LINE';
export const FormField = 'FORM_FIELD';
export const TextInput = 'TEXT_INPUT';
export const Select = 'SELECT';
export const Checkbox = 'CHECKBOX';
export const List = 'LIST';
export const Stop = 'STOP';
export const Defs = 'DEFS';
export const Image = 'IMAGE';
Expand Down
16 changes: 16 additions & 0 deletions packages/primitives/tests/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,22 @@ describe('primitives', () => {
expect(primitives.Line).toBeTruthy();
});

test('should export form field', () => {
expect(primitives.FormField).toBeTruthy();
});

test('should export text input', () => {
expect(primitives.TextInput).toBeTruthy();
});

test('should export form list', () => {
expect(primitives.List).toBeTruthy();
});

test('should export select', () => {
expect(primitives.Select).toBeTruthy();
});

test('should export stop', () => {
expect(primitives.Stop).toBeTruthy();
});
Expand Down
24 changes: 24 additions & 0 deletions packages/render/src/primitives/form/renderCheckbox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { parseCheckboxOptions } from '../../utils/parseFormOptions';

const renderCheckbox = (ctx, node, options = {}) => {
const { top, left, width, height } = node.box || {};

// Element's name
const name = node.props?.name || '';
const formFieldOptions = options.formFields?.at(0);

if (!ctx._root.data.AcroForm) {
ctx.initForm();
}

ctx.formCheckbox(
name,
left,
top,
width,
height,
parseCheckboxOptions(ctx, node, formFieldOptions),
);
};

export default renderCheckbox;
18 changes: 18 additions & 0 deletions packages/render/src/primitives/form/renderFormField.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const renderFormField = (ctx, node, options = {}) => {
const name = node.props?.name || '';

if (!ctx._root.data.AcroForm) {
ctx.initForm();
}

const formField = ctx.formField(name);
const option = options;
if (!option.formFields) option.formFields = [formField];
else option.formFields.push(formField);
};

export const cleanUpFormField = (_ctx, _node, options) => {
options.formFields.pop();
};

export default renderFormField;
23 changes: 23 additions & 0 deletions packages/render/src/primitives/form/renderList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { parseSelectAndListFieldOptions } from '../../utils/parseFormOptions';

const renderList = (ctx, node) => {
const { top, left, width, height } = node.box || {};

// Element's name
const name = node.props?.name || '';

if (!ctx._root.data.AcroForm) {
ctx.initForm();
}

ctx.formList(
name,
left,
top,
width,
height,
parseSelectAndListFieldOptions(node),
);
};

export default renderList;
Loading

0 comments on commit 8cd872f

Please sign in to comment.