@servicetitan/dte-pdf-editor
v1.89.0
Published
A React component library for creating interactive PDF editors with drag-and-drop field placement, data model integration, e-signature support, fillable form fields, **job form (submission) fields**, and calculated fields with formulas.
Downloads
5,392
Readme
@servicetitan/dte-pdf-editor
A React component library for creating interactive PDF editors with drag-and-drop field placement, data model integration, e-signature support, fillable form fields, job form (submission) fields, and calculated fields with formulas.
Features
- 📄 PDF Rendering: Display and interact with PDF documents using
react-pdfandpdfjs-dist - 🎯 Drag & Drop Field Placement: Intuitively place fields on PDF pages by dragging from a sidebar
- 📊 Data Model Integration: Connect fields to structured data models using JSON Schema
- ✍️ E-Signature Support: Add signature, initials, date signed, and full name fields
- 📝 Fillable Fields: Support for text, number, date, checkbox, and radio button inputs
- 🧮 Calculated Fields: Formula-based fields with validation, autosuggest, and advanced formatting (number, currency, percent, date, rounding, separators)
- 🗂️ Forms Fields: Bind PDF placements to job form submission values (
__submission_fields.*paths), with async field definitions from the host and the same paths usable inside calculated-field formulas - 📋 Generic Fields: Custom table and text blocks with configurable rows/cells and inline editing in view mode
- 👥 Multi-Recipient Support: Assign fields to different recipients with color-coded visualization
- ⚙️ Field Configuration: Configure field properties including position, size, label, recipient assignment, radio group name (shared across options in the same group), formula/format for calculated fields, and table structure (header/body rows and cells) for generic table fields
- 🔀 Display Conditions: Show/hide rules with merge tags, fillable fields, and form submission fields; radio groups appear as a single data point with equal/not-equal operators and a value picker for group options
- 👁️ View Mode: Display PDFs with filled data in a read-only or interactive view mode (including generic fields via
PdfViewGeneric)
Recent changes
- fix/inconsistncy — PDF overlay layout, typography, and text clipping aligned with the backend PDF renderer; searchable lookup-backed display-condition values. See RELEASE_NOTES.md.
- fix/DTE-4360 (radio groups) — see RELEASE_NOTES.md.
Installation
npm install @servicetitan/dte-pdf-editorPeer Dependencies
This package requires the following peer dependencies:
npm install @servicetitan/anvil2@^3.7.1 react@~18.3.1 react-dom@~18.3.1Quick Start
import { PdfEditor, PdfField, PdfView, SchemaObject } from '@servicetitan/dte-pdf-editor';
import { useState } from 'react';
const pdfUrl = 'https://example.com/document.pdf';
function App() {
const [fields, setFields] = useState<PdfField[]>([]);
const [data, setData] = useState<Record<string, any>>({});
const forms = [
{ id: 1, name: 'Job checklist' },
];
const recipients = [
{ id: 1, name: 'technician', displayName: 'Technician' },
{ id: 2, name: 'customer', displayName: 'Customer' },
];
const dataModel: SchemaObject = {
type: 'object',
properties: {
CustomerName: {
type: 'string',
title: 'Customer Name',
options: { placeholder: 'Enter customer name' },
},
},
};
return (
<PdfEditor
pdfUrl={pdfUrl}
fields={fields}
onFieldsChange={setFields}
dataModel={dataModel}
recipients={recipients}
forms={forms}
onFormSelect={(formIds, sendFormFields) => {
// Load FormFieldInfo[] for each formId, then:
sendFormFields({
/* [formId]: [{ id, header, itemType: 'number' | 'text' | 'date' }, ...] */
});
}}
/>
);
}Components
PdfEditor
The main component for editing PDF documents and placing fields.
Props
| Prop | Type | Required | Description |
|------------------------|--------------------------------------------------------------------------------------------------|----------|-------------------------------------------------------------------------------------------------------------------------|
| pdfUrl | string | Yes | URL or path to the PDF file |
| fields | PdfField[] | No | Array of fields placed on the PDF |
| onFieldsChange | (fields: PdfField[]) => void | Yes | Callback when fields are added, modified, or deleted |
| onFormSelect | (formIds: number[], sendFormFields: (formFieldsByFormId: FormFieldsByFormIdI) => void) => void | No | With forms, host supplies FormFieldInfo[] per form id for sidebar, formulas, and preload; omit when not using forms |
| dataModel | SchemaObject | No | JSON Schema object defining available data model fields |
| recipients | RecipientInfo[] | No | Array of recipients that can be assigned to fields |
| forms | FormInfo[] | No | Job forms shown in the sidebar and formula builder; omit to hide the Forms field group |
| hideFields | FieldTypeEnum[] | No | Field menu groups to hide (e.g. omit forms when not supported) |
| hideConditionalLogic | boolean | No | When true, hides display-condition UI in the field config panel |
| loading | boolean | No | Whether the PDF is currently loading |
| loadingPlaceholder | ReactNode | No | Custom component to display while PDF is loading |
| errorPlaceholder | ReactNode | No | Custom component to display if PDF fails to load |
| onLoadSuccess | (numPages: number) => void | No | Called when the PDF document loads successfully |
| errors | Record<string, string> | No | Map of field id → validation message rendered on the canvas |
Example
<PdfEditor
pdfUrl="https://example.com/document.pdf"
fields={fields}
onFieldsChange={setFields}
dataModel={dataModel}
recipients={recipients}
loading={!pdfUrl}
loadingPlaceholder={<Spinner />}
errorPlaceholder={<Alert status="danger" title="Failed to load PDF" />}
/>PdfView
Component for displaying PDFs with filled data in view mode.
Props
| Prop | Type | Required | Description |
|------------------------------|----------------------------------------------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------|
| pdfUrl | string | Yes | URL or path to the PDF file |
| fields | PdfField[] | Yes | Array of fields to display on the PDF |
| data | DataModelValues | No | Data object containing values for data model fields |
| recipients | RecipientInfo[] | No | Array of recipients for field assignment |
| fillingBy | string[] | No | Array of recipient names that are allowed to fill fields |
| viewMode | PdfViewMode ('preview' \| 'fill') | No | Controls how the PDF is rendered — see View modes below. Defaults to the recipient-fill experience when omitted. |
| makeFillableFieldsReadonly | boolean | No | When true, all fillable fields are non-editable (default false). See Read-only fillable fields below. |
| onDataChange | (changedData: { [path: string]: string \| boolean }) => void | No | Callback when fillable field data changes |
| loading | boolean | No | Whether the PDF is currently loading |
| loadingPlaceholder | ReactNode | No | Custom component to display while PDF is loading |
| errorPlaceholder | ReactNode | No | Custom component to display if PDF fails to load |
View modes
The viewMode prop controls who is interacting with the PDF and how:
'preview'— Author/admin preview. All fields are visible and interactive so the admin can try out the form and see its behavior. Data changes update an internal preview buffer and are not persisted. Recipient scoping (fillingBy) is ignored — every field is fillable. Calculated fields with no resolvable value fall back to the field label (or'Calculated Field') so the field is still visible.'fill'— Read-only display. No fields are editable regardless offillingBy. Already-filled values are displayed; empty fields render empty.- Omitted (
undefined) — Recipient fill experience. Only fields whoserecipientmatches one of the names infillingByare editable for the current user; other recipients' fields are read-only.onDataChangeis invoked for every edit so the caller can persist the value.
Read-only fillable fields
Set makeFillableFieldsReadonly={true} on PdfView to block edits on every fillable field while still showing
current values — useful for a non-interactive preview of a partially or fully filled form.
- Text, number, date, textarea: uses the native
readOnlyattribute (value visible, not editable). - Checkbox and radio: use
disabledinstead — HTML does not supportreadOnlyon these input types. Checked state is still shown; controls cannot be toggled andonDataChangeis not called.
This prop applies on top of viewMode / fillingBy scoping (e.g. in 'preview' mode, fillable fields are normally
interactive unless makeFillableFieldsReadonly is set).
Example
<PdfView
pdfUrl="https://example.com/document.pdf"
fields={fields}
data={data}
recipients={recipients}
fillingBy={['technician']}
makeFillableFieldsReadonly={false}
onDataChange={(changedData) => {
setData({ ...data, ...changedData });
}}
/>Types
PdfField
Represents a field placed on a PDF document.
interface PdfField {
id: string;
type: FieldTypeEnum;
subType?: PdfFieldSubType;
x: number;
y: number;
page: number;
label: string;
groupName?: string; // fillable radio — shared name for all options with the same path
width: number;
height: number;
required?: boolean;
path?: string;
formSnapshot?: FormulaFieldFormSnapshot; // for `type: forms` — mirrors bound submission path metadata
recipient?: string;
description?: string;
data?: FieldDataType; // for generic fields (table/text)
formula?: StructuredFormula; // for calculated fields
formulaFormat?: CalculatedFieldFormat; // for calculated fields (includes dateFormat)
displayCondition?: DisplayConditionState | null;
styles?: PdfFieldStyles;
}PdfFieldStyles (optional on PdfField.styles):
fontSize— points; converted to editor pixels to match/conversions/templatetopdffontFamily— defaults to Arial/Helvetica (PdfSharp default)fontWeight—'normal'|'bold'; defaults to'normal'
DisplayConditionState and related types model optional show/hide rule groups for a field (exported from this package).
DisplayConditionSingle.valueLabel is optional and display-only: when a condition Value comes from a searchable
lookup dropdown, persist the selected option’s label so the condition editor can show it on reopen. Condition evaluation
still uses value only.
For radio groups, getDocumentFieldsDataPointOptions exposes one entry per shared path (isRadioGroup: true,
radioOptions for the value picker). Condition values use the chosen radio’s id.
For checkbox fillable fields, getDocumentFieldsDataPointOptions sets isCheckbox: true. Display conditions show
only Selected (is_equal_to) and Unselected (is_not_equal_to) — no value input. Both compare against the
literal "true" (fill/preview stores booleans that stringify to "true" / "false").
FieldTypeEnum
Enumeration of field types:
dataModel: Field connected to a data model schemaforms: Bound to a job form submission field (pathunder__submission_fields.*)eSign: E-signature fieldfillable: User-fillable form fieldcalculated: Formula-based calculated fieldgeneric: Custom table or text block (configurable rows/cells, inline editing)
ESignFieldType
Types of e-signature fields:
signature: Signature fieldinitials: Initials fielddateSigned: Date signed fieldfullName: Full name field
PdfFieldSubType
Union of fillable, e-sign, generic, and form field sub-types:
FillableFieldType | ESignFieldType | GenericFieldType | FormFieldType. Used for PdfField.subType and
FieldTypeOption.subType (form fields use FormFieldType: number | text | date).
FillableFieldType
Types of fillable fields:
text: Text inputnumber: Number inputdate: Date pickercheckbox: Checkboxradio: Radio button (use + on the canvas to add options; set Radio Group Name and per-option Label in the config panel)
Radio groups store the selected option’s id at the shared path in fill/preview data. Use
groupName to identify the group in the UI and in display-condition data points.
GenericFieldType
Types of generic fields:
table: Configurable table with header and body rows and editable cellstext: Text block with inline editing
FieldDataType
For generic fields, PdfField.data can be:
- GenericFieldTableDataType:
showHeader,header(height, cells),body(array of rows with height and cells) - GenericFieldTextDataType:
value(string)
Calculated Fields (formula & format)
Calculated fields use a structured formula and optional display format:
- StructuredFormula: Token list (numbers, operators
+ - * /, parentheses, field references) used for validation and safe editing. - CalculatedFieldFormat: Controls result display:
resultType(number|currency|percent|date), rounding mode, decimal places, thousands/decimal separators, prefix/postfix text,dateFormat(custom pattern usingYYYY,MM,DD, etc.). - FormulaToken (field): Field tokens carry
fieldType: 'date' | 'number'(schema / fillable / form operands) for date-aware evaluation. OptionalformSnapshotis filled when the token path is a submission field (__submission_fields.*) andparseExpressionis givenParseExpressionFormContext(forms+formFieldsByFormId).
Date fields in formulas use day-based arithmetic: date + number adds days, date - date yields a day count. Only +,
-, and parentheses are allowed when date fields are present.
Schema fields can opt in via options.useInCalculatedFields and options.useInConditionals for data model extraction
in formula builder and conditionals.
Job forms (FormInfo, FormFieldInfo, submission data)
FormInfo:{ id: number; name: string }— entries passed asPdfEditorforms.FormFieldInfo:{ id: string; header: string; itemType: 'number' | 'text' | 'date' }— returned from the host for each requested form id.FormFieldsByFormIdI:Record<number, FormFieldInfo[]>— argument tosendFormFieldsin the optional **onFormSelect** callback.- Preview /
PdfViewdata: Provide values under__submission_fieldsmirroring the path layout, e.g.{ __submission_fields: { [formId]: { [normalizedFieldId]: value } } }(field id keys without hyphens matchbuildFormFieldKey).
SchemaObject
JSON Schema object defining the data model structure. Supports nested objects, arrays, and various string subtypes (string, text, html, image).
interface SchemaObject {
type: 'object';
properties: Record<string, SchemaNode>;
title?: string;
options?: SchemaFieldBaseOptions;
}RecipientInfo
Information about a recipient that can be assigned to fields.
interface RecipientInfo {
id: number;
name: string;
displayName: string;
}Usage Examples
Basic Editor with Data Model
import { PdfEditor, PdfField, SchemaObject } from '@servicetitan/dte-pdf-editor';
import { useState } from 'react';
const dataModel: SchemaObject = {
type: 'object',
properties: {
JobInformation: {
type: 'object',
properties: {
JobNumber: {
type: 'string',
title: 'Job Number',
options: { placeholder: 'Enter job number' },
},
JobName: {
type: 'string',
title: 'Job Name',
options: { placeholder: 'Enter job name' },
},
},
},
},
};
function MyEditor() {
const [fields, setFields] = useState<PdfField[]>([]);
return (
<PdfEditor
pdfUrl="https://example.com/document.pdf"
fields={fields}
onFieldsChange={setFields}
dataModel={dataModel}
/>
);
}Editor with Recipients
const recipients = [
{ id: 1, name: 'technician', displayName: 'Technician' },
{ id: 2, name: 'customer', displayName: 'Customer' },
{ id: 3, name: 'accountant', displayName: 'Accountant' },
];
<PdfEditor
pdfUrl={pdfUrl}
fields={fields}
onFieldsChange={setFields}
recipients={recipients}
/>View Mode with Data
const [data, setData] = useState({
'JobInformation.JobNumber': 'JOB-12345',
'JobInformation.JobName': 'HVAC Installation',
});
<PdfView
pdfUrl={pdfUrl}
fields={fields}
data={data}
recipients={recipients}
fillingBy={['technician']}
onDataChange={(changedData) => {
setData({ ...data, ...changedData });
}}
/>Toggle Between Edit and View Modes
function PdfEditorPage() {
const [fields, setFields] = useState<PdfField[]>([]);
const [isView, setIsView] = useState(false);
const [data, setData] = useState<Record<string, any>>({});
return (
<>
<Button onClick={() => setIsView(!isView)}>
{isView ? 'Edit' : 'View'}
</Button>
{!isView ? (
<PdfEditor
pdfUrl={pdfUrl}
fields={fields}
onFieldsChange={setFields}
dataModel={dataModel}
recipients={recipients}
/>
) : (
<PdfView
pdfUrl={pdfUrl}
fields={fields}
data={data}
recipients={recipients}
fillingBy={['technician']}
onDataChange={(changedData) => {
setData({ ...data, ...changedData });
}}
/>
)}
</>
);
}Field Operations
Adding Fields
Fields are added by dragging from the sidebar onto the PDF canvas. The sidebar displays:
- Data Model Fields (Merge Tags): Grouped by schema structure
- E-Sign Fields: Signature, initials, date signed, full name
- Fillable Fields: Text, number, date, checkbox, radio
- Forms (when
forms+onFormSelectare provided): Choose a job form, then drag a field onto the canvas; the newPdfFieldstorespathandformSnapshotfor the bound submission key - Calculated Fields: Formula-based fields (configure formula and format in the config panel)
- Generic Fields: Text and Table (configure table rows/cells in the config panel; inline editing in view mode)
Configuring Fields
Click on a field to open the configuration panel where you can:
- Change the label
- For radio fillable fields: set Radio Group Name (applies to all options with the same path) and Label per option
- Adjust position and size
- Assign a recipient
- Set the required status (fillable fields, except checkbox)
- Update the data model path
- For calculated fields: edit formula (with validation and autosuggest from data model, fillable fields, and number/date job form fields via the Forms slide-out), set result type (number/currency/percent/date), rounding, decimals, separators, and date format
- For forms fields: edit label like other display labels; binding is defined by
path/formSnapshotfrom placement - For generic table fields: configure header visibility, header/body row heights, and cell values via Table configs; generic text fields support inline editing in the overlay
- Display conditions (when not hidden): rules can reference merge tags, fillable fields, and form fields; radio
groups appear once by
groupNamewith equal/not-equal operators and a Select value for the selected option’sid
Radio groups (fillable)
- Drag Radio onto the PDF.
- Open the config panel: set Radio Group Name and the Label for the first option.
- Click + on the radio overlay to add more options in the same group (shared
pathandgroupName, uniqueidandlabelper option). - In display conditions, pick the group under Fillable field → Data point; choose Is equal to or Is not equal to and select an option in Value.
Moving and Resizing
- Move: Click and drag a field to reposition it
- Resize: Use the resize handles on field corners
Deleting Fields
Select a field and use the delete button in the configuration panel, or press the delete key.
Styling
The package includes default styles. To customize, you can override CSS classes:
.dte-pdf-editor: Main editor container.dte-pdf-view-container: View mode container.dte-pdf-wrapper: PDF document wrapper.dte-pdf-field-overlay: Field overlay container.dte-pdf-field-editor: Editor field chrome (dashed outline; layout metrics fromgetFieldBoundsStyle).dte-pdf-field-view: View-mode field container.dte-pdf-field-text--single-line/--multiline/--wrapped: Text clip modes (hard clip, no ellipsis).dte-pdf-editor-sidebar-container: Sidebar container
Field overlays use border-box sizing with no padding so width/height match the backend PDF clip rectangle.
Editor selection chrome uses outline (not border) so it does not affect layout.
Lookup-backed display condition values
Schema fields tagged with options.fieldLookupSource can drive the Value control in display conditions. The host
resolves options via the lookup event bus (host-events.ts):
- Static source — host returns the full option list; the editor filters locally (
SelectFieldSync). - Searchable source — host sets
searchable: trueon the first response; the editor requests options per typed query and can resolve saved keys viakeysfor label rehydration. PersistvalueLabelonDisplayConditionSinglewhen the user picks a searchable option.
Browser Support
This package requires modern browsers with support for:
- ES6+
- Canvas API
- Drag and Drop API
License
Copyright © ServiceTitan
