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

Form fields #2

Closed
wants to merge 7 commits into from
Closed
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
2 changes: 1 addition & 1 deletion packages/layout/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"@react-pdf/fns": "3.0.0",
"@react-pdf/image": "^3.0.1",
"@react-pdf/pdfkit": "^4.0.0",
"@react-pdf/primitives": "^4.0.0",
"@react-pdf/primitives": "link:../primitives",
"@react-pdf/stylesheet": "^5.1.0",
"@react-pdf/textkit": "^5.0.1",
"@react-pdf/types": "^2.7.0",
Expand Down
Binary file added packages/layout/react-pdf-layout-v4.1.3.tgz
Binary file not shown.
Binary file not shown.
2 changes: 2 additions & 0 deletions packages/primitives/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,5 @@ export const ClipPath = 'CLIP_PATH';
export const TextInstance = 'TEXT_INSTANCE';
export const LinearGradient = 'LINEAR_GRADIENT';
export const RadialGradient = 'RADIAL_GRADIENT';
export const FormField = 'FORM_FIELD';
export const TextInput = 'TEXT_INPUT';
8 changes: 8 additions & 0 deletions packages/primitives/tests/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ 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 stop', () => {
expect(primitives.Stop).toBeTruthy();
});
Expand Down
2 changes: 1 addition & 1 deletion packages/render/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"dependencies": {
"@babel/runtime": "^7.20.13",
"@react-pdf/fns": "3.0.0",
"@react-pdf/primitives": "^4.0.0",
"@react-pdf/primitives": "link:../primitives",
"@react-pdf/textkit": "^5.0.1",
"@react-pdf/types": "^2.7.0",
"abs-svg-path": "^0.1.1",
Expand Down
Binary file added packages/render/react-pdf-render-v4.0.2.tgz
Binary file not shown.
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;
24 changes: 24 additions & 0 deletions packages/render/src/primitives/form/renderTextInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { parseTextFieldOptions } from '../../utils/parseFormOptions';

const renderTextInput = (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.formText(
name,
left,
top,
width,
height,
parseTextFieldOptions(node, formFieldOptions),
);
};

export default renderTextInput;
13 changes: 13 additions & 0 deletions packages/render/src/primitives/renderNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import setLink from '../operations/setLink';
import clipNode from '../operations/clipNode';
import transform from '../operations/transform';
import setDestination from '../operations/setDestination';
import renderTextInput from './form/renderTextInput';
import renderFormField, { cleanUpFormField } from './form/renderFormField';

const isRecursiveNode = (node) => node.type !== P.Text && node.type !== P.Svg;

Expand All @@ -23,6 +25,7 @@ const renderChildren = (ctx, node, options) => {
}

const children = node.children || [];
// eslint-disable-next-line no-use-before-define
const renderChild = (child) => renderNode(ctx, child, options);

children.forEach(renderChild);
Expand All @@ -37,6 +40,12 @@ const renderFns = {
[P.Canvas]: renderCanvas,
[P.Svg]: renderSvg,
[P.Link]: setLink,
[P.FormField]: renderFormField,
[P.TextInput]: renderTextInput,
};

const cleanUpFns = {
[P.FormField]: cleanUpFormField,
};

const renderNode = (ctx, node, options) => {
Expand All @@ -59,6 +68,10 @@ const renderNode = (ctx, node, options) => {

if (shouldRenderChildren) renderChildren(ctx, node, options);

const cleanUpFn = cleanUpFns[node.type];

if (cleanUpFn) cleanUpFn(ctx, node, options);

setDestination(ctx, node);
renderDebug(ctx, node);

Expand Down
38 changes: 38 additions & 0 deletions packages/render/src/utils/parseFormOptions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const clean = (options) => {
const opt = { ...options };

// We need to ensure the elements are no present if not true
Object.entries(opt).forEach((pair) => {
if (!pair[1]) {
delete opt[pair[0]];
}
});

return opt;
};

const parseCommonFormOptions = (node) => {
// Common Options
return {
required: node.props?.required || false,
noExport: node.props?.noExport || false,
readOnly: node.props?.readOnly || false,
value: node.props?.value || undefined,
defaultValue: node.props?.defaultValue || undefined,
};
};

const parseTextFieldOptions = (node, formField) => {
return clean({
...parseCommonFormOptions(node),
parent: formField || undefined,
align: node.props?.align || 'left',
multiline: node.props?.multiline || undefined,
password: node.props?.password || false,
noSpell: node.props?.noSpell || false,
format: node.props?.format || undefined,
});
};

// eslint-disable-next-line import/prefer-default-export
export { parseTextFieldOptions };
3 changes: 3 additions & 0 deletions packages/render/tests/ctx.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ const createCTX = () => {
instance.lineCap = vi.fn().mockReturnValue(instance);
instance.text = vi.fn().mockReturnValue(instance);
instance.font = vi.fn().mockReturnValue(instance);
instance._root = { data: { AcroForm: {} } };
instance.textInput = vi.fn().mockReturnValue(instance);
instance.formField = vi.fn().mockReturnValue(instance);

return instance;
};
Expand Down
49 changes: 49 additions & 0 deletions packages/render/tests/primitives/renderForm.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { describe, expect, test } from 'vitest';
import * as P from '@react-pdf/primitives';
import createCTX from '../ctx';
import renderFormField from '../../src/primitives/form/renderFormField';

describe('primitive renderFormField', () => {
test('should render FormField correctly', () => {
const ctx = createCTX();
const args = 'example';
const props = { name: args };
const node = { type: P.FormField, props };

renderFormField(ctx, node);

expect(ctx.formField.mock.calls).toHaveLength(1);
expect(ctx.formField.mock.calls[0]).toHaveLength(1);
expect(ctx.formField.mock.calls[0][0]).toBe(args);
});

test.todo('FormField with one textInput direct child', () => {
const ctx = createCTX();
const node = { type: P.FormField, children: [{ type: P.TextInput }] };

renderFormField(ctx, node);

expect(ctx.textInput.mock.calls).toHaveLength(1);
});

test.todo('FormField with one TextInput indirect child', () => {
const ctx = createCTX();
const node = {
type: P.TextInput,
children: [
{
type: P.View,
children: [
{
type: P.TextInput,
},
],
},
],
};

renderFormField(ctx, node);

expect(ctx.textInput.mock.calls).toHaveLength(1);
});
});
47 changes: 47 additions & 0 deletions packages/renderer/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,53 @@ declare namespace ReactPDF {
React.PropsWithChildren<LinkProps>
> {}

interface FormCommonProps extends NodeProps {
name?: string;
required?: boolean;
noExport?: boolean;
readOnly?: boolean;
value?: number | string;
defaultValue?: number | string;
}

interface FormFieldProps extends NodeProps {
name: string;
}

export class FormField extends React.Component<
React.PropsWithChildren<FormFieldProps>
> {}

// see http://pdfkit.org/docs/forms.html#text_field_formatting
interface TextInputFormatting {
type:
| 'date'
| 'time'
| 'percent'
| 'number'
| 'zip'
| 'zipPlus4'
| 'phone'
| 'ssn';
param?: string;
nDec?: number;
sepComma?: boolean;
negStyle?: 'MinusBlack' | 'Red' | 'ParensBlack' | 'ParensRed';
currency?: string;
currencyPrepend?: boolean;
}

// see http://pdfkit.org/docs/forms.html#text_field_formatting
interface TextInputProps extends FormCommonProps {
align?: 'left' | 'center' | 'right';
multiline?: boolean;
password?: boolean;
noSpell?: boolean;
format?: TextInputFormatting;
}

export class TextInput extends React.Component<TextInputProps> {}

interface NoteProps extends NodeProps {
children: string;
}
Expand Down
4 changes: 2 additions & 2 deletions packages/renderer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@
"@react-pdf/font": "^3.0.1",
"@react-pdf/layout": "^4.1.3",
"@react-pdf/pdfkit": "^4.0.0",
"@react-pdf/primitives": "^4.0.0",
"@react-pdf/primitives": "link:../primitives",
"@react-pdf/reconciler": "^1.1.3",
"@react-pdf/render": "^4.0.2",
"@react-pdf/render": "link:../render",
"@react-pdf/types": "^2.7.0",
"events": "^3.3.0",
"object-assign": "^4.1.1",
Expand Down
Binary file added packages/renderer/react-pdf-renderer-v4.1.5.tgz
Binary file not shown.
Loading