diff --git a/How to/Create Signable PDF Forms with Dynamic User/package.json b/How to/Create Signable PDF Forms with Dynamic User/package.json new file mode 100644 index 0000000..af580e4 --- /dev/null +++ b/How to/Create Signable PDF Forms with Dynamic User/package.json @@ -0,0 +1,53 @@ +{ + "name": "esigningagreement", + "version": "0.1.0", + "private": true, + "dependencies": { + "@syncfusion/ej2-layouts": "^30.1.40", + "@syncfusion/ej2-react-buttons": "^30.1.37", + "@syncfusion/ej2-react-dropdowns": "^30.1.42", + "@syncfusion/ej2-react-inputs": "^30.1.40", + "@syncfusion/ej2-react-navigations": "^30.1.42", + "@syncfusion/ej2-react-pdfviewer": "^30.1.42", + "@syncfusion/ej2-react-popups": "^30.1.42", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.6.4", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^13.5.0", + "@types/jest": "^27.5.2", + "@types/node": "^16.18.126", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@types/react-router-dom": "^5.3.3", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-router-dom": "^7.7.1", + "react-scripts": "5.0.1", + "typescript": "^4.9.5", + "web-vitals": "^2.1.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/How to/Create Signable PDF Forms with Dynamic User/public/assets/SoftwareLicenseAgreement.pdf b/How to/Create Signable PDF Forms with Dynamic User/public/assets/SoftwareLicenseAgreement.pdf new file mode 100644 index 0000000..8350e8d Binary files /dev/null and b/How to/Create Signable PDF Forms with Dynamic User/public/assets/SoftwareLicenseAgreement.pdf differ diff --git a/How to/Create Signable PDF Forms with Dynamic User/public/assets/User.png b/How to/Create Signable PDF Forms with Dynamic User/public/assets/User.png new file mode 100644 index 0000000..9afed7b Binary files /dev/null and b/How to/Create Signable PDF Forms with Dynamic User/public/assets/User.png differ diff --git a/How to/Create Signable PDF Forms with Dynamic User/public/index.html b/How to/Create Signable PDF Forms with Dynamic User/public/index.html new file mode 100644 index 0000000..aa069f2 --- /dev/null +++ b/How to/Create Signable PDF Forms with Dynamic User/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
+ + + diff --git a/How to/Create Signable PDF Forms with Dynamic User/src/index.css b/How to/Create Signable PDF Forms with Dynamic User/src/index.css new file mode 100644 index 0000000..97c1e88 --- /dev/null +++ b/How to/Create Signable PDF Forms with Dynamic User/src/index.css @@ -0,0 +1,68 @@ + @import '../node_modules/@syncfusion/ej2-base/styles/material.css'; + @import '../node_modules/@syncfusion/ej2-buttons/styles/material.css'; + @import '../node_modules/@syncfusion/ej2-dropdowns/styles/material.css'; + @import '../node_modules/@syncfusion/ej2-inputs/styles/material.css'; + @import '../node_modules/@syncfusion/ej2-navigations/styles/material.css'; + @import '../node_modules/@syncfusion/ej2-popups/styles/material.css'; + @import '../node_modules/@syncfusion/ej2-splitbuttons/styles/material.css'; + @import "../node_modules/@syncfusion/ej2-pdfviewer/styles/material.css"; + @import '../node_modules/@syncfusion/ej2-layouts/styles/material.css'; + + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} + +#e-pv-e-sign-user-field { + color: #222933; + font-family: Heebo, open sans, "sans-serif", -apple-system, BlinkMacSystemFont; + border-radius: 4px; + display: flex; + position: relative; + border: 1px solid black; +} + +.e-btn-icon.e-icons.e-trash{ + font-size: 20px; + margin-top: 10px; +} + +.e-card{ + width: 900px; + padding: 10px; + max-height: 80vh; + overflow-y: auto; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + background-color: #fff; + border-radius: 8px; + display: flex; + flex-direction: column; + justify-content: flex-start; +} + +#addRecipientButton { + border: 1px solid #aaa; + border-radius: 6px; + padding: 12px 24px; + background: #fff; + font-weight: bold; + font-size: 16px; + cursor: pointer; + color: #222; +} + +#e-pv-e-sign-toolbar-user-viewer{ + height: 60px !important; +} + + diff --git a/How to/Create Signable PDF Forms with Dynamic User/src/index.tsx b/How to/Create Signable PDF Forms with Dynamic User/src/index.tsx new file mode 100644 index 0000000..15d2d92 --- /dev/null +++ b/How to/Create Signable PDF Forms with Dynamic User/src/index.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; +import RecipientSetup from './recipientSetup'; +import PdfViewer from './pdfViewer'; +import './index.css'; + +const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); + +root.render( + + + + } /> + } /> + + + +); diff --git a/How to/Create Signable PDF Forms with Dynamic User/src/pdfViewer.tsx b/How to/Create Signable PDF Forms with Dynamic User/src/pdfViewer.tsx new file mode 100644 index 0000000..5ef883b --- /dev/null +++ b/How to/Create Signable PDF Forms with Dynamic User/src/pdfViewer.tsx @@ -0,0 +1,605 @@ +import { useEffect, useRef, useState } from 'react'; +import { + PdfViewerComponent, Toolbar, Magnification, Navigation, LinkAnnotation, BookmarkView, + ThumbnailView, Print, TextSelection, Annotation, TextSearch, FormFields, FormDesigner, Inject +} from '@syncfusion/ej2-react-pdfviewer'; +import { DialogComponent } from '@syncfusion/ej2-react-popups'; +import { ButtonComponent, RadioButtonComponent } from '@syncfusion/ej2-react-buttons'; +import { ToolbarComponent, ItemDirective, ItemsDirective } from '@syncfusion/ej2-react-navigations'; +import { DropDownListComponent } from '@syncfusion/ej2-react-dropdowns'; +import { createSpinner, showSpinner, hideSpinner } from '@syncfusion/ej2-popups'; + +interface UserDetails { + Name: string; + Mail: string; + fieldIds: string[]; +} + +function PdfViewer() { + // Reference to access PDF viewer methods (e.g., retrieveFormFields, updateFormField) + const pdfViewer = useRef(null); + // Reference to access the selected radio button component + const radioBtnRef = useRef(null); + // State to control the visibility of the dialog + const [status, setStatus] = useState({ hideDialog: false }); + // State to hold user details, which can be loaded from localStorage or an API + const [userDetails, setUserDetails] = useState([]); + // Add a state to store user colors + const [userColors, setUserColors] = useState<{ [email: string]: string }>({}); + // Variable to store the currently selected user from the radio buttons + const [selectedUser, setSelectedUser] = useState(userDetails[0]?.Name || ''); + const [showDropdown, setShowDropdown] = useState(false); // Initially false: show user cards + const userMenu = useRef(null); + const fields = { + text: 'Name', // property to display in dropdown + value: 'Mail', // property to use as the value + }; + // Stores the current user's color for UI styling + let userColour = useRef(''); + // Tracks the email of the currently active user + let currentUser = useRef(''); + // Reference to the "Finish Signing" button component + let btnElement = useRef(null); + + const recipientCount = userDetails.length; + const rowHeight = 36; // Height allocated per recipient row in the dialog + const baseHeight = 100; // Minimum height of the dialog without any recipients + const dialogHeight = baseHeight + recipientCount * rowHeight; // Final calculated height of the dialog based on recipient count + + const hasLongName = userDetails.some(user => user.Name.length > 5); + const baseWidth = hasLongName ? 350 : 250; + const widthPerRecipient = recipientCount > 2 ? 60 : 30; // 60px increments for 3+ users, else 30px + const maxDialogWidth = 600; // Maximum allowed width for the dialog + const dialogWidth = Math.min(baseWidth + recipientCount * widthPerRecipient, maxDialogWidth); // Final calculated width of the dialog, capped at maxDialogWidth + + // Set the default selected user when userDetails are loaded + useEffect(() => { + if (userDetails.length > 0) { + setSelectedUser(userDetails[0].Name); + } + }, [userDetails]); + + // Load recipients from localStorage, convert them to userDetails format, and merge without duplicates + useEffect(() => { + const storedRecipients = localStorage.getItem('recipients'); + try { + let fieldId = 0; + const newUsers: UserDetails[] = []; + + if (storedRecipients) { + const recipients = JSON.parse(storedRecipients); + recipients.forEach((r: any) => { + newUsers.push({ + Name: r.userName, + Mail: r.userEmail, + fieldIds: [fieldId.toString()] + }); + fieldId++; + }); + } + + setUserDetails(prev => { + const existingEmails = new Set(prev.map(u => u.Mail)); + const filteredNewUsers = newUsers.filter(u => !existingEmails.has(u.Mail)); + return [...filteredNewUsers, ...prev]; + }); + } catch (error) { + console.error("Error parsing user info or recipients:", error); + } + }, []); + + // Initialize and attach a spinner to the container element on component mount + useEffect(() => { + const spinnerTarget = document.getElementById('container'); + if (spinnerTarget) { + createSpinner({ target: spinnerTarget }); + } + }, []); + + // Closes the dialog by updating the visibility state + function dialogClose() { + setStatus({ hideDialog: false }); + } + + // Triggered when a form field is added; opens the user selection dialog + const formFieldAdd = (args: any) => { + setStatus({ hideDialog: true }); + } + + const getRandomLightColor = (): string => { + // Generate RGB values between 180 and 255 for light colors + const r = Math.floor(180 + Math.random() * 75); + const g = Math.floor(180 + Math.random() * 75); + const b = Math.floor(180 + Math.random() * 75); + return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`; + }; + + const hexToRgba = (hex: string, alpha: number) => { + const r = parseInt(hex.slice(1, 3), 16); + const g = parseInt(hex.slice(3, 5), 16); + const b = parseInt(hex.slice(5, 7), 16); + return `rgba(${r},${g},${b},${alpha})`; + }; + + // Handles the Submit button click after user selection + const userSelectionSubmit = () => { + setSelectedUser(radioBtnRef.current?.getSelectedValue() || ''); + + setStatus({ hideDialog: false }); + dialogClose(); + + // Get the last added form field + const formFields = pdfViewer.current?.retrieveFormFields(); + const lastField = formFields?.[formFields.length - 1]; + + // Find the selected user's details + const user = userDetails.find(u => u.Name === selectedUser); + + if (lastField && user) { + let color = userColors[user.Mail]; + if (!color) { + color = getRandomLightColor(); + setUserColors(prev => ({ ...prev, [user.Mail]: color })); + } + + // Check if the field is a signature field (adjust property as needed) + const isSignatureField = lastField.type === 'SignatureField' || lastField.name?.toLowerCase().includes('sign'); + + // Use 40% opacity for signature fields, solid for others + userColour.current = isSignatureField ? hexToRgba(color, 0.4) : color; + + pdfViewer.current?.formDesigner.updateFormField( + lastField, + { + customData: { + name: user.Name, + email: user.Mail + }, + name: `${user.Name || ""} ${lastField.name || ""}`.trim(), + backgroundColor: userColour.current, + isRequired: true, + borderColor: '#303030' + } as any + ); + } + }; + + // Updates the selected user when a radio button is changed + const userChange = (args: any) => { + setSelectedUser(args.value); + }; + + // Renders the user dropdown component for selecting a signer + const dropdownComponent = () => { + return ( +
+
+ +
+
+ ); + } + + // Custom template for rendering each item in the dropdown list + const itemTemplate = (data: any) => { + return ( +
+ +
+
{data.Name}
+
{data.Mail}
+
+
+ ); + } + + // Custom template for displaying the selected value in the dropdown + const valueTemplate = (data: any) => { + return (
+ employee +
+
{data.Name}
+
{data.Mail}
+
+
); + }; + + // Renders the "Finish Signing" button + const buttonComponent = () => { + return (Finish Signing); + } + + // Disables the "Finish Signing" button when the component is first created + const disableFinishButtonOnInit = () => { + if (btnElement.current) + btnElement.current.disabled = true; + } + + // Renders user cards with image, name, and email in a horizontal layout + const userCardComponent = () => { + return ( +
+ {userDetails.map((user, idx) => ( +
+ user +
+
+ {user['Name'] as string} +
+
+ {user['Mail'] as string} +
+
+
+ ))} +
+ ); + }; + + // Renders the dialog content with user selection and submit button + const getRecipientDialogContent = () => { + return ( +
+

+ Select the User: +

+
+ {userDetails.map((user, index) => ( +
+ +
+ ))} +
+
+ Submit +
+
+ ); + }; + + // Initializes event listener for closing the form designer and shows the user dropdown + const documentLoaded = () => { + document.getElementById("pdfViewer_formdesigner_closeContainer")?.addEventListener("click", async function () { + setShowDropdown(true); + if (pdfViewer.current) + pdfViewer.current.designerMode = false; + showOnlyCurrentUserFields(); + }) + + } + + // Displays only the form fields assigned to the current user and hides others + const showOnlyCurrentUserFields = () => { + const formFields = pdfViewer.current?.retrieveFormFields(); + currentUser.current = userDetails[0].Mail; + if (!formFields || !currentUser.current) return; + + formFields.forEach(field => { + const fieldEmail = (field.customData as { email?: string })?.email; + // Show fields of current user, hide others + pdfViewer.current?.formDesigner.updateFormField(field, { + visibility: fieldEmail === currentUser.current ? 'visible' : 'hidden' + } as any); + }); + }; + + // Handles user selection change in the dropdown + const dropdownUserChange = (args: any) => { + const selectedMail = args.itemData.Mail; + const selectedIdx = userDetails.findIndex(u => u.Mail === selectedMail); + + // Validate all previous users, on first invalid - show error, block switch + for (let i = 0; i < selectedIdx; i++) { + const prevUser = userDetails[i]; + const prevUserFields = pdfViewer.current?.formFieldCollections?.filter( + (field: any) => (field.customData && field.customData.email === prevUser.Mail) + ); + // If any previous user's fields are invalid, cancel the dropdown change and show + if (!validateFormFields(prevUserFields || [], prevUser.Name)) { + args.cancel = true; + // Optionally, show which user needs to finish: + pdfViewer.current?.showNotificationPopup( + `Please complete all required fields for user: ${prevUser.Name}` + ); + return; + } + } + + // Proceed to update field visibility and editability based on selected user + const fieldCollection = pdfViewer.current?.formFieldCollections; + if (fieldCollection) { + for (let i = 0; i < fieldCollection.length; i++) { + const customData = fieldCollection[i]?.customData as any; + if (!customData) continue; + + const fieldUserIdx = userDetails.findIndex(u => u.Mail === customData.email); + + // For the selected user: show fields as editable + if (fieldUserIdx === selectedIdx) { + pdfViewer.current?.formDesigner.updateFormField(fieldCollection[i], { + visibility: 'visible', + isReadOnly: false, + } as any); + } + // For users before the selected one: show fields as read-only + else if (fieldUserIdx < selectedIdx) { + pdfViewer.current?.formDesigner.updateFormField(fieldCollection[i], { + visibility: 'visible', + isReadOnly: true, + } as any); + } + + // For users after the selected one: hide fields and make them read-only + else { + pdfViewer.current?.formDesigner.updateFormField(fieldCollection[i], { + visibility: 'hidden', + isReadOnly: true, + } as any); + } + + } + } + }; + + // Validates previous users' fields before allowing dropdown selection change and updates field visibility accordingly. + function validateFormFields(fieldsToCheck: any[], userLabel?: string, showPopup = true): boolean { + let missingFields: string[] = []; + let radioGroups: Record = {}; + + for (const field of fieldsToCheck) { + if (!field.isRequired) continue; + + if (field.type === "Checkbox" && !field.isChecked) { + missingFields.push(field.name); + } else if (field.type === "RadioButton") { + if (!radioGroups[field.name]) radioGroups[field.name] = false; + if (field.isSelected) radioGroups[field.name] = true; + } else if (field.type === "DropdownList" && (!field.value || field.value.length === 0)) { + missingFields.push(field.name); + } else if ( + field.type !== "Checkbox" && + field.type !== "RadioButton" && + field.type !== "DropdownList" && + (!field.value || (typeof field.value === "string" && field.value.trim() === "")) + ) { + missingFields.push(field.name); + } + } + + // After loop: find any radio groups with no selection + Object.entries(radioGroups).forEach(([group, selected]) => { + if (!selected) missingFields.push(group); + }); + + if (missingFields.length > 0) { + if (showPopup) { + const msgUser = userLabel || "User"; + const fieldsList = missingFields.join(', '); + const errorMessage = `${msgUser} hasn't signed the document.

Required fields: ${fieldsList}`; + pdfViewer.current?.showNotificationPopup(errorMessage); + } + return false; + } + return true; + } + + // Checks all required form fields on change and enables/disables the finish button based + const fieldChange = (args: any) => { + const forms = pdfViewer.current?.formFieldCollections || []; + const missingFields: Set = new Set(); + const radioGroups: Record = {}; + + forms.forEach(form => { + if (!form.isRequired) return; + const type = form.type; + const name = String(form.name); + + switch (type as any) { + case "Checkbox": + if (!form.isChecked) missingFields.add(name); + break; + case "RadioButton": + if (!(name in radioGroups)) radioGroups[name] = false; + if (form.isSelected) radioGroups[name] = true; + break; + case "DropdownList": + if (!form.value || (Array.isArray(form.value) && form.value.length === 0)) missingFields.add(name); + break; + default: + const valueIsEmpty = + !form.value || + (typeof form.value === "string" && form.value.trim() === "") || + (typeof args.newValue === "string" && args.newValue.trim() === ""); + if (valueIsEmpty) missingFields.add(name); + } + }); + + // After iteration, check for required radio groups not selected + Object.keys(radioGroups).forEach(group => { + if (!radioGroups[group]) missingFields.add(group); + }); + + //Enable the finish button once all required fields filled + if (missingFields.size > 0) { + if (btnElement.current) btnElement.current.disabled = true; + } else { + if (btnElement.current) btnElement.current.disabled = false; + if (pdfViewer.current) pdfViewer.current.enableDownload = true; + } + }; + + // Handles the final signing process: saves the PDF, sends it to the server, downloads the signed version, and disables UI controls + const finishSigning = () => { + // Show the spinner when signing starts + const spinnerTarget = document.getElementById('container'); + if (spinnerTarget) showSpinner(spinnerTarget); + const url = "https://ej2services.syncfusion.com/react/development/api/pdfviewer/FlattenDownload"; + pdfViewer.current?.saveAsBlob().then((blob) => { + return convertBlobToBase64(blob); + }).then((base64String) => { + const httpResponse = new XMLHttpRequest(); + httpResponse.open('POST', url, true); + httpResponse.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); + const requestData = JSON.stringify({ base64String }); + httpResponse.onload = () => { + // Always hide spinner at end (success or error) + if (spinnerTarget) hideSpinner(spinnerTarget); + if (httpResponse.status === 200) { + const responseBase64 = httpResponse.responseText.split('base64,')[1]; + if (responseBase64) { + const blob = createBlobFromBase64(responseBase64, 'application/pdf'); + const blobUrl = URL.createObjectURL(blob); + downloadDocument(blobUrl); + pdfViewer.current?.load(httpResponse.responseText, ""); + if (btnElement.current) + btnElement.current.disabled = true; + if (userMenu.current) + userMenu.current.enabled = false; + } else { + console.error('Invalid base64 response.'); + } + } else { + console.error('Download failed:', httpResponse.statusText); + } + }; + httpResponse.onerror = () => { + if (spinnerTarget) hideSpinner(spinnerTarget); + console.error('An error occurred during the download:', httpResponse.statusText); + }; + httpResponse.send(requestData); + }).catch((error) => { + if (spinnerTarget) hideSpinner(spinnerTarget); + console.error('Error saving Blob:', error); + }); + } + + // Converts a Blob object to a Base64-encoded string + const convertBlobToBase64 = (blob: any) => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(blob); + reader.onloadend = () => { + if (typeof reader.result === 'string') { + resolve(reader.result); + } else { + reject(new Error('Failed to convert Blob to Base64')); + } + }; + reader.onerror = (error) => reject(error); + }); + }; + + // Creates a Blob object from a Base64 string and specified content type + const createBlobFromBase64 = (base64String: any, contentType: any) => { + const sliceSize = 512; + const byteCharacters = atob(base64String); + const byteArrays = []; + + for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) { + const slice = byteCharacters.slice(offset, offset + sliceSize); + const byteNumbers = new Array(slice.length); + for (let i = 0; i < slice.length; i++) { + byteNumbers[i] = slice.charCodeAt(i); + } + const byteArray = new Uint8Array(byteNumbers); + byteArrays.push(byteArray); + } + + return new Blob(byteArrays, { type: contentType }); + }; + + // Triggers download of the signed PDF document using a blob URL + const downloadDocument = (blobUrl: any) => { + const anchorElement = document.createElement('a'); + anchorElement.href = blobUrl; + anchorElement.target = '_parent'; + + const downloadFileName = pdfViewer.current?.fileName || 'default.pdf'; + anchorElement.download = downloadFileName.endsWith('.pdf') + ? downloadFileName + : `${downloadFileName.split('.pdf')[0]}.pdf`; + + document.body.appendChild(anchorElement); + anchorElement.click(); + document.body.removeChild(anchorElement); + URL.revokeObjectURL(blobUrl); + }; + + return ( +
+ {/* Toolbar displaying user cards (e.g., name, email, image) */} +
+ + + + {showDropdown && ( + + )} + + +
+ + {/* PDF Viewer with form designer and other tools enabled */} + + {/* Inject required PDF viewer services and features */} + + + + {/* Dialog for selecting user type (Licensee or Licensor) */} +
+ + {getRecipientDialogContent()} + +
+ + {/* Container element used as the target for loading spinner and layout wrapper */} +
+
+ ); +} + +export default PdfViewer; + diff --git a/How to/Create Signable PDF Forms with Dynamic User/src/recipientSetup.tsx b/How to/Create Signable PDF Forms with Dynamic User/src/recipientSetup.tsx new file mode 100644 index 0000000..52f925c --- /dev/null +++ b/How to/Create Signable PDF Forms with Dynamic User/src/recipientSetup.tsx @@ -0,0 +1,179 @@ +import { useEffect, useRef, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { TextBoxComponent } from '@syncfusion/ej2-react-inputs'; +import { ButtonComponent } from '@syncfusion/ej2-react-buttons'; +import { FormValidator } from '@syncfusion/ej2-inputs'; + +function RecipientSetup() { + const navigate = useNavigate(); + // State to manage a list of recipients (e.g., for sharing or notifications) + const [recipients, setRecipients] = useState([{ userName: '', userEmail: '' }]); + // Ref to access the form DOM element for validation or reset + const formRef = useRef(null); + // Ref to hold the form validator instance (e.g., for triggering validation programmatically) + const validatorRef = useRef(null); + // Ref to access the button component for enabling/disabling it based on form state + const formBtnRef = useRef(null); + // State to manage the height of the card containing the recipient form + const [cardHeight, setCardHeight] = useState('400px'); + + useEffect(() => { + if(formRef.current) { + const dynamicRules: Record = {}; + + recipients.forEach((_, idx) => { + dynamicRules[`name_${idx}`] = { + required: [true, '* Name is required'], + minLength: [3, '* Name must be at least 3 characters'] + }; + dynamicRules[`email_${idx}`] = { + required: [true, '* Email is required'], + email: [true, '* Please enter a valid email'] + }; + }); + + if (validatorRef.current) { + // Update existing validator rules without destroying it + validatorRef.current.rules = dynamicRules; + } else { + // Create new validator only if it doesn't exist + validatorRef.current = new FormValidator(formRef.current, { + rules: dynamicRules + }); + } + } + }, [recipients]); + + // Adds a new empty recipient to the list + const addRecipient = () => { + const newRecipients = [...recipients, { userName: '', userEmail: '' }]; + setRecipients(newRecipients); + + if (formBtnRef.current) + formBtnRef.current.disabled = false; + + // Set card height to 500px if there is at least one recipient + if (newRecipients.length > 0) + setCardHeight('500px'); + else + setCardHeight('400px'); + }; + + // Updates a specific field (userName or userEmail) of a recipient at a given index + const recipientChange = (index: number, field: 'userName' | 'userEmail', value: string) => { + const updatedRecipients = [...recipients]; + updatedRecipients[index][field] = value; + setRecipients(updatedRecipients); + }; + + // Removes a recipient from the list by index + const removeRecipient = (index: number) => { + const updatedRecipients = recipients.filter((_, i) => i !== index); + setRecipients(updatedRecipients); + + if (updatedRecipients.length > 1) + setCardHeight('500px'); + else + setCardHeight('400px'); + + if (updatedRecipients.length === 0) { + if (formBtnRef.current) + formBtnRef.current.disabled = true; + } + }; + + // Handles form submission: validates input, stores data, and navigates to the PDF viewer + const formSubmit = (event: React.FormEvent) => { + event.preventDefault(); + if (validatorRef.current && validatorRef.current.validate()) { + const validRecipients = recipients.filter( + r => r.userName.trim() !== '' && r.userEmail.trim() !== '' + ); + localStorage.setItem('recipients', JSON.stringify(validRecipients)); + navigate('/pdf-viewer'); + } + }; + + // Renders the recipient input form UI with dynamic fields and a submit button + return ( +
+
+ {/* Header section with title and add button */} +
+
+ Add recipients +
+ +
+ + {/* Form for entering recipient details */} +
+ {recipients.map((recipient, idx) => ( +
+
+ {/* Recipient Name Field */} +
+ + recipientChange(idx, 'userName', e.value)} + /> +
+ + {/* Recipient Email Field */} +
+ + recipientChange(idx, 'userEmail', e.value)} + /> +
+ + {/* Remove recipient button */} + removeRecipient(idx)} + /> +
+
+ ))} + + {/* Submit button */} +
+ + Continue to Form Creation + +
+
+
+
+ ); +} +export default RecipientSetup; \ No newline at end of file diff --git a/How to/Create Signable PDF Forms with Dynamic User/tsconfig.json b/How to/Create Signable PDF Forms with Dynamic User/tsconfig.json new file mode 100644 index 0000000..a273b0c --- /dev/null +++ b/How to/Create Signable PDF Forms with Dynamic User/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": [ + "src" + ] +}