# Authentication Source: https://docs.joyfill.io/api/authentication How to authenticate with the Joyfill Platform You can authenticate requests to Joyfill using an API key or a User Access Token. Joyfill responds with an error if you don’t include a valid authentication method or if the authentication method is expired. * [API Keys](/api/authentication#api-keys) - Used for authenticating secure backend requests. * [User Access Tokens](/api/authentication#user-access-tokens) - Used for authenticating client-side (web & mobile) requests. # API Keys *** API Keys are used for authenticating secure backend requests. API Keys can be created inside the Joyfill Manager API Keys page. ## Creation You can use the Joyfill Manager to view, revoke, and create API keys. Follow the steps below to create a new API Key inside Joyfill Manager: * **Step 1:** In the top navigation bar select the "Settings and Users" tab. When the menu appears select "API Keys". This will navigate you to the API Keys page. * **Step 2:** Select the "Add Key" button (*securely copy and store your public and secret key. This is the only time you will have access to the secret key.*) * **Step 3:** You will need to base64 encode your Public and Secret keys before they can used with the API. See instructions below. ### Base64 Encoding You can encode your API keys using the command: `echo -n : | base64`. An example of the command is shown below: ```bash bash theme={null} echo -n pk_pnUBoEpGNtCq31LbY3Oxgvsw2UO9:sk_SVcxWggo642Wq3sfVW8AuxIFw2cT | base64 ``` ## Usage You must authenticate using HTTP basic authentication. Use your API Public Key as the username, and the API Secret as the password. You must send an Authorization header with the value Basic followed by base 64 encoded `public_key:secret_key` . **Header example:** ```bash bash theme={null} Authorization: Basic cHVibGljX2tleV9leGFtcGxlOnNlY3JldF9rZXlfZXhhbXBsZQo ``` # User Access Tokens *** User Access Tokens are used for authenticating client-side (web & mobile) application requests . User access tokens can be created, retrieved and revoked via the [User Access Token API endpoints](/api/users/overview-users) and inside the [Joyfill Manager users page](https://app-joy.joyfill.io/users). ## Creation There are two ways to create a User Access Token. The first is from the user page within the Joyfill Manager and the other is via our API. See below for more details. ### Option 1: Joyfill Manager Follow the steps below to generate a User Access Token for your own user account in the Joyfill Manager: * **Step 1:** Login to the [Joyfill Manager](https://app-joy.joyfill.io) * **Step 2:** In the top navigation bar select the "Settings and Users". When the menu appears select "Manager Users". This will navigate you to the Users page. * **Step 3:** Select the "Access Tokens" button next to your user account. * **Step 4:** Click the "Add Access Token" button (*securely copy and store your user access token.*) ### Option 2: API Request Learn more: [Creating User Access Tokens](/api/users/create-a-user-access-token) ## Usage You must authenticate using HTTP bearer authentication. You must send an Authorization header with the value Bearer followed by the user access token. Header example: ``` Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbiI6IjYzYTBjOTZhZTJhZTg2N2Q1ZDE3ZjExNCIsImV4cGlyZXNPbiI6MTY3OTI4ODM5OTk5OX0.QZxoPkb2HAl1C0n4C9FTacEmbPb2DHVMa_iPn4cC08o ``` # Bulk create documents Source: https://docs.joyfill.io/api/documents/bulk-create-documents post /documents/bulk # Create a document Source: https://docs.joyfill.io/api/documents/create-a-document post /documents # Create a document pdf export Source: https://docs.joyfill.io/api/documents/create-a-document-pdf-export post /documents/{identifier}/exports/pdf # Create a document pdf export using Raw JSON Payload Source: https://docs.joyfill.io/api/documents/create-a-document-pdf-export-using-raw-json-payload post /documents/exports/pdf # Create a image upload (data uri) Source: https://docs.joyfill.io/api/documents/create-a-image-upload-data-uri post /documents/{identifier}/files/datauri Create a document image upload to be used for image fields, page backgrounds, etc. # Delete a document Source: https://docs.joyfill.io/api/documents/delete-a-document delete /documents/{identifier} # List all documents Source: https://docs.joyfill.io/api/documents/list-all-documents get /documents # Overview Source: https://docs.joyfill.io/api/documents/overview A document represents the fields, styles, layouts, and populated data that the Joyfill Platform uses to render forms, power fillable pdfs, generate downloadable reports, capture user input and much more. # Retrieve a document Source: https://docs.joyfill.io/api/documents/retrieve-a-document get /documents/{identifier} # Search all documents Source: https://docs.joyfill.io/api/documents/search-all-documents post /documents/search Search, filer and sort documents # Update a document Source: https://docs.joyfill.io/api/documents/update-a-document post /documents/{identifier} # Update a document via changelogs Source: https://docs.joyfill.io/api/documents/update-a-document-via-changelogs post /documents/{identifier}/changelogs # Create a group Source: https://docs.joyfill.io/api/groups/create-a-group post /groups # Delete a group Source: https://docs.joyfill.io/api/groups/delete-a-group delete /groups/{identifier} # List all groups Source: https://docs.joyfill.io/api/groups/list-all-groups get /groups # Overview Source: https://docs.joyfill.io/api/groups/overview-groups A group represents a customer of your business/product. It lets you create users, templates, documents, etc. that belong to the same customer. # Retrieve a group Source: https://docs.joyfill.io/api/groups/retrieve-a-group get /groups/{identifier} # Update a group Source: https://docs.joyfill.io/api/groups/update-a-group post /groups/{identifier} # Identifiers Source: https://docs.joyfill.io/api/identifiers Overview of identifiers inside of Joyfill Identifiers represent readable IDs for resources inside of Joyfill. Identifiers are the primary way you will query, update, and remove resources via the Joyfill API. Identifiers are also how you can identify individual fields on the form for pre-populating and extracting field data. Joyfill will auto generate a unique identifier for each new resource that you create. Each identifier will be unique only to your organization. ## Resource Examples | Resource | Example | | -------- | -------------------------- | | Group | `group_*************` | | User | `user_**************` | | Document | `doc_***************` | | Template | `template_***************` | | Fields | `field_*************` | ## Custom Identifiers Joyfill identifiers can be set to custom values at the moment of resource creation. The custom identifier can be an ID inside your own database or some other value that helps you connect and uniquely identify the associated resource between Joyfill and your own system. **Requirements** * Custom identifiers must be unique. * Type: String, Minimum Length: 3, Max Length: 50. * Alphanumeric and `_` characters only. *** # Overview Source: https://docs.joyfill.io/api/introduction Getting started with the Joyfill API The Joyfill API is organized around REST. Our API has predictable resource-oriented URLs, accepts JSON request bodies, returns JSON-encoded responses, and uses standard HTTP response codes, authentication, and verbs. # Hosts The primary host is `api-joy.joyfill.io` for both read and write operations. All API access must use HTTPS and will require [Authentication](/api/authentication). # Format The entire API uses JSON encoded as UTF-8. The body of POST and PUT requests must be either a JSON object or a JSON array (depending on the particular endpoint) and their Content-Type header should be set to application/json; charset=UTF-8. The body of responses is always a JSON object, and their content type is always application/json; charset=UTF-8. # Authentication Joyfill uses API keys and User Access Tokens to authenticate API requests. Learn more: [Authenication](/api/authentication) # Identifiers Identifiers represent readable IDs for resources inside of Joyfill. Identifiers are the primary way you will query, update, and remove resources via the Joyfill API. Learn more: [Identifiers In Joyfill](/api/identifiers) # Search Source: https://docs.joyfill.io/api/platform-api-search Query, filter, and sort data in Joyfill Some top level API resources support retrieval with search API methods. You can use the search APIs to retrieve your Joyfill data in a flexible manner. Using search is a faster alternative to paginating through all resources. To create a search query, review the Search query language and reference the query fields of the resource. ### Important Note: Only documents created after May 30th 2023 are available for query and sort. # Examples Here are some examples of what you can do with the Search Documents API. **IMPORTANT: Query Template Documents** - If you're trying to query documents for a specific template can still follow the same guide below. Instead of retrieving a document in the Getting Started step, you simply need to use the Templates API route to retrieve the template. Retrieving the `File ID` and `FieldID` for internal field queries work the same for both templates and documents because they both follow the same object structure. ### Getting Started Before we get started with querying documents data we are going to retrieve a document and grab our target `FileID` and `FieldID`. These IDs will be used to query documents based on their internal field data. ```bash bash theme={null} //Step 1: Retrieve A Document const response = await fetch("https://api-joy.joyfill.io/v1/documents/doc_identifier", { method: 'GET', mode:'cors', headers: { Authorization: `Bearer ${userAccessToken}`, 'Content-Type': 'application/json' } }); //Step 2: There are two methods to retrieving the FileID and FieldID const joyDoc = await response.json(); //Method 1: Using files object joyDoc.files[0]._id //File ID. joyDoc.files[0].pages[0].fields[0].field //FieldID. IMPORTANT: Ensure you use `.field` property. //Method 2: Using top level fields joyDoc.fields[0].fileId; joyDoc.fields[0].fieldId; ``` ### Return exact match and sort results Lookup documents that match the exact internal field value of `"joy"` and sort by `createdOn`. ```bash bash theme={null} const fileId = '638ca7c8880dfc1bca968be0'; const fieldId = '638ca7c8674374a0508b232a'; const response = await fetch("https://api-joy.joyfill.io/v1/documents/search", { method: 'POST', mode:'cors', headers: { Authorization: `Bearer ${userAccessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ query: { "fields.638ca7c8880dfc1bca968be0.638ca7c8674374a0508b232a.value": "joy" }, sort: { createdOn: -1 } }) }); ``` ### Return substring match and sort results Lookup documents that contain the internal field value of `"joy"` (case insensitive) and sort by internal field value. ```bash bash theme={null} const fileId = '638ca7c8880dfc1bca968be0'; const fieldId = '638ca7c8674374a0508b232a'; const response = await fetch("https://api-joy.joyfill.io/v1/documents/search", { method: 'POST', mode:'cors', headers: { Authorization: `Bearer ${userAccessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ query: { "fields.638ca7c8880dfc1bca968be0.638ca7c8674374a0508b232a.value": {$regex: "joy", $options: "i"} }, sort: { "fields.638ca7c8880dfc1bca968be0.638ca7c8674374a0508b232a.value": -1 } }) }); ``` ### Combine Multiple Filters Lookup documents matching a combination of internal field data and date range. The documents below will contain the value `joy` and have a `createdOn` timestamp within the specified 6 day period. ```bash bash theme={null} const fileId = '638ca7c8880dfc1bca968be0'; const fieldId = '638ca7c8674374a0508b232a'; const response = await fetch("https://api-joy.joyfill.io/v1/documents/search", { method: 'POST', mode:'cors', headers: { Authorization: `Bearer ${userAccessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ query: { "$and": [ {"fields.638ca7c8880dfc1bca968be0.638ca7c8674374a0508b232a.value": {$regex: "joy", $options: "i"}}, {"createdOn": {$gt: 1672981200000, $lt: 1685133261338}} //Created over a 6 day period ] } }) }); ``` # Search query language ### Search Syntax | Type | Usage | Description | Examples | | ------------------------------ | -------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Exact Match | `field:"value"` | Exact match operator. | `name:"joy"` returns records where the name is exactly “joy” in a case-sensitive comparison. | | Regular Expression Match | `field:{$regex:"value", $options: "i"}` | Substring and Case Insensitive match operator | `name:{$regex: "joy", $options: "i"}` returns records where the name contains "joy" in a case-insensitive comparison. Example matches are: "JOY", "joY", "[joy@email.com](mailto:joy@email.com)", etc. | | Not Equal Match | `field:{$ne:"value"}` | Returns records that don’t match the clause | `name:{$ne: "joy"}` returns records that aren’t equal to "joy" | | Greater Than / Less Than Match | `field:{$gt:"value"}`, `field:{$gte:"value"}`, `field:{$lt:"value"}`, `field:{$lte:"value"}` | Greater than/less than operators | `createdOn:{$gt: 10}` returns records that have a createdOn greater than 10. `$gt` specifies greater than. `$gte` specifies greater than or equal to. `$lt` specifies less than. `$lte` specifies less than or equal to. | | AND | `$and:[{field:"value1"}, {field:"value2"}]` | The query returns only records that match both clauses. | `$and:[{name: "joy"}, {template: "template_000"}]` | | OR | `$or:[{field:"value1"}, {field:"value2"}]` | The query returns records that match either of the clauses | `$or:[{name: "joy"}, {template: "template_000"}]` | # Sort query language ### Sort Syntax | Type | Usage | Description | | --------------- | ---------- | ------------------------------------------- | | Ascending Sort | `field:1` | Returns records sorted in ascending order. | | Descending Sort | `field:-1` | Returns records sorted in descending order. | # Supported Query and Sort Fields ### Query fields for documents | Field | Description | Value Type | | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | | `template` | Returns records that match a specified template identifier. | String | | `group` | Returns records that match a specified group identifier. | String | | `stage` | Returns records that match a specified stage. | String('draft', 'published') | | `name` | Returns records that match a specified name | String | | `createdOn` | Returns records that match a specified createdOn date. | Number (millisecond timestamp) | | `fields.FileID.FieldID.value` | Returns records that match a specified field value. You will need to replace the `FileID` and `FieldID` with intended targets from your Joyfill Document or Template. | Mixed (String, Int, Float, or Boolean) | *** # Create a template Source: https://docs.joyfill.io/api/templates/create-a-template post /templates # Delete a template Source: https://docs.joyfill.io/api/templates/delete-a-template delete /templates/{identifier} # List all templates Source: https://docs.joyfill.io/api/templates/list-all-templates get /templates # Overview Source: https://docs.joyfill.io/api/templates/overview-templates A template is a saved set of fields, layouts, styles, etc. that will be populated via api request or filled out by a user repeatedly. The template type is used to power template style workflows. # Retrieve a template Source: https://docs.joyfill.io/api/templates/retrieve-a-template get /templates/{identifier} # Sync template documents Source: https://docs.joyfill.io/api/templates/sync-template-documents post /templates/{identifier}/documents Sync the latest template styles and layout changes to pre-existing template documents # Update a template Source: https://docs.joyfill.io/api/templates/update-a-template post /templates/{identifier} # Create a user Source: https://docs.joyfill.io/api/users/create-a-user post /users # Create a user access token Source: https://docs.joyfill.io/api/users/create-a-user-access-token post /users/{identifier}/access_tokens # Delete a user Source: https://docs.joyfill.io/api/users/delete-a-user delete /users/{identifier} # Delete a user access token Source: https://docs.joyfill.io/api/users/delete-a-user-access-token delete /users/{identifier}/access_tokens/{token} # List all user access tokens Source: https://docs.joyfill.io/api/users/list-all-user-access-tokens get /users/{identifier}/access_tokens # List all users Source: https://docs.joyfill.io/api/users/list-all-users get /users # Overview Source: https://docs.joyfill.io/api/users/overview-users A user represents an individual interacting with any part of the Joyfill APIs, UI Components, etc. There are two different types of users within Joyfill: Organization Users and Group Users. # Organization Users *** An organization represents your own product or company. A organization user represents someone in your own company. These could be your developers, customer support team, account executives, etc. An organization user can perform actions against any objects owned by your organization. **Capabilities** * **Groups** - create, update, remove, and retrieve any group own by your organization. * **Users** - create, update, remove, and retrieve any user within your organization and any user owned by a group within your organization. * **Documents** - create, update, remove, and retrieve any document owned by your organization and any document owned by a group within your organization. # Group Users *** A group represents a customer of your business/product. Therefore, a group user represents users and employees of your customers. A group user can only perform actions against objects owned by the group they belong to. For instance, if you have two customers called Company A and Company B. A user from within Company A cannot access any users, documents, etc. within Company B. **Capabilities** * **Groups** - retrieve only the group they belong to. * **Users** - create, update, remove, and retrieve only other users within the same group. * **Documents** - create, update, remove, and retrieve only other documents within the group they belong to. # Retrieve a user Source: https://docs.joyfill.io/api/users/retrieve-a-user get /users/{identifier} # Convert PDF to PNGs (data uri) Source: https://docs.joyfill.io/api/utilities/convert-pdf-to-pngs-data-uri post /utilities/pdf/to/png/datauri Convert a PDF into page background images. # Security Source: https://docs.joyfill.io/ios/about/security # Overview Joyfill follows industry best-practices to keep your data safe: You can only access the Joyfill service via TLS (https). When you submit data to for storage or to generate a PDF, this data is encrypted at rest using AES-256. All stored files are encrypted at rest, using the AWS Key Management Service. This includes template PDFs, generated PDFs, and any other files that are stored in Amazon S3. Passwords are salted and hashed with high level expansion rounds. We do not store plaintext passwords in our database. We subscribe to security mailing lists and patch any vulnerabilities as soon as possible. # Compliance (HITRUST, HIPAA, SOC 2, PCI DSS, etc.) To maintain compliance we offer a self-hosting option. Our self-hosting option allows you to retain and manage all template, submission, file and any other data directly in your own system. [See Self-Hosting](/joyfill/self-hosted) or reach out to our team directly via live chat at [https://joyfill.io](https://joyfill.io) # Vulnerability Disclosures Joyfill welcomes vulnerability disclosures. Please send an email to [contact@joyfill.io](mailto:contact@joyfill.io) to report any security vulnerabilties. # Questions Reach out directly to our team using our live chat at [https://joyfill.io](https://joyfill.io) *** # JoyfillAPIService Source: https://docs.joyfill.io/ios/api-reference/api-service-module API service layer for communicating with Joyfill backend # JoyfillFormulas Source: https://docs.joyfill.io/ios/api-reference/formulas-module Formula engine and calculation utilities # Joyfill Module Source: https://docs.joyfill.io/ios/api-reference/joyfill-module Main Joyfill SDK with UI components and core functionality # JoyfillModel Source: https://docs.joyfill.io/ios/api-reference/model-module Data models, schemas, and form structures # API Overview Source: https://docs.joyfill.io/ios/api-reference/overview Complete API reference for the Joyfill iOS SDK modules # iOS API Reference Comprehensive documentation for Joyfill iOS SDK with cross-module references. ## iOS SDK Modules Main Joyfill SDK with UI components, views, and core functionality for building form-based applications. API service layer for communicating with Joyfill backend services and managing network requests. Formula engine and calculation utilities for dynamic form field computations and expressions. Data models, schemas, and structures representing forms, fields, and document definitions. ## Quick Links * [**Full iOS API Reference**](https://joyfill.github.io/api-references/ios/) - Complete SwiftDoc documentation with all modules * [**Getting Started Guide**](/ios/getting-started) - Setup and installation instructions * [**iOS Guides**](/ios/guides/modes) - Integration guides and tutorials ## Module Overview ### 📱 Joyfill Module The main SDK module containing: * **JoyfillFormView** - Primary form rendering component * **JoyfillFormConfig** - Configuration objects for form initialization * **Form Delegates** - Event handling protocols * **UI Components** - Built-in form field components [View Joyfill Module Documentation →](https://joyfill.github.io/api-references/ios/Joyfill/documentation/joyfill) ### 🌐 JoyfillAPIService Module The API service module providing: * **Network Layer** - HTTP client for Joyfill API * **Authentication** - Token management and authentication flows * **API Endpoints** - Methods for documents, templates, and users * **Request/Response Models** - Type-safe API models [View JoyfillAPIService Documentation →](https://joyfill.github.io/api-references/ios/JoyfillAPIService/documentation/joyfillapiservice) ### 🧮 JoyfillFormulas Module The formula engine module featuring: * **Formula Parser** - Parse and evaluate form formulas * **Calculation Engine** - Dynamic field calculations * **Expression Evaluator** - Support for complex expressions * **Built-in Functions** - Math, string, and date functions [View JoyfillFormulas Documentation →](https://joyfill.github.io/api-references/ios/JoyfillFormulas/documentation/joyfillformulas) ### 📦 JoyfillModel Module The data models module including: * **JoyDoc Schema** - Document and template structures * **Field Models** - All form field type definitions * **Validation Models** - Validation rules and constraints * **Type Definitions** - Shared types and enums [View JoyfillModel Documentation →](https://joyfill.github.io/api-references/ios/JoyfillModel/documentation/joyfillmodel) ## Documentation Format The iOS SDK documentation is generated using **Swift-DocC** (Swift Documentation Compiler), providing: * ✅ Type-safe API documentation * ✅ Interactive code examples * ✅ Cross-module references * ✅ Search functionality * ✅ Dark/light mode support ## Additional Resources Installation and setup guide Handle form events and callbacks Latest updates and changes Security best practices # Release Notes Source: https://docs.joyfill.io/ios/changelogs/RELEASE_NOTES Complete changelog of Joyfill iOS SDK releases > Source: [https://github.com/joyfill/components-swift/releases](https://github.com/joyfill/components-swift/releases) *** ## 3.0.0-rc26 **Release Date:** September 3, 2026 CHANGED * Keyboard Focus — the keyboard no longer stays focused on the previous field after tapping another field. * JSON Schema — added required and cell-level conditional logic properties (`requiredLogic`, `cellVisibilityLogic`, `cellRequiredLogic`, `cellsHidden`) to the validation schema. FIXED * Page Selection Sheet — scroll indicator no longer misaligned on iOS 27 after rotating an iPad with the keyboard open. ## 3.0.0-rc25 **Release Date:** August 20, 2026 ADDED * **Table & Collection Editability** — added an `editability` flag (`inline`, `form`) to restrict a row to grid-only editing, row-form-only editing, or both. CHANGED * **API Rename** — `DocumentEditor.updateField(event:fieldIdentifier:)` renamed to `updateField(event:)`. FIXED * **Table/Collection Bulk-Edit Crash** — fixed a crash when navigating into nested schemas via goto row navigation. ## 3.0.0-rc24 **Release Date:** July 16, 2026 ADDED * **Field lookup by identifier and title** — `DocumentEditor` now supports `field(identifier:)` and `field(title:)` in addition to `field(fieldID:)`, so you no longer have to filter `allFields` by hand. Title lookups are case-insensitive to stay consistent across platforms. CHANGED * **MultiSelect columns are now single-select by default** — set `multi: true` on a table or collection column to allow picking more than one option. FIXED * **Image field decorators** — decorators applied to a standalone image field now render in the UI instead of only being saved to JSON. * **Row-form scrolling on large tables & collections** — fixed janky, sticky scrolling in the single-row / bulk-edit form on fields with large datasets. * **Landscape date + time picker** — opening the combined date + time picker in landscape no longer freezes the app. ## 3.0.0-rc23 **Release Date:** July 2, 2026 ADDED * DocumentEditor — new config-based initializer accepts a reusable `DocumentEditorConfig` object. * Formula Dates — date functions now parse date strings, not just timestamps.
 * JSON Schema — added field-based conditional logic conditions (file/page/field) to the validation schema. CHANGED * Formula Arrays — array results on text fields now display as bracketed strings. FIXED * Duplicate Page Without Values — copying a page without values no longer fails schema validation for table and collection fields. * Formula Dates — out-of-range date amounts no longer crash and now fail safely. ## 3.0.0-rc22 **Release Date:** June 19, 2026 CHANGED * Goto Navigation — `goto` now scrolls the row form to the target column when the target cell is in the same row. FIXED * Collection Field Crash — fixed a multithreading crash that could occur while a Collection field was loading. * Cross-Modal Goto — `goto` now opens the row form correctly when moving between different field modals. ## 3.0.0-rc21 **Release Date:** June 18, 2026 CHANGED * Date Cell Layout — table date cell uses a unified font, the full date pill is tappable. * Row Form — navigation buttons are pinned to the top and stay fixed while scrolling. FIXED * Row Form External Updates — An open row form now reflects changes pushed through the Change API while it's open (table and collection). * Date Field Empty Value — Date fields no longer show the current time when the value is an empty or invalid string. * Readonly Textarea Scroll — Read-only textarea fields with long content are now scrollable. * Table Filter Horizontal Scroll — table columns remain horizontally scrollable when a filter returns zero rows. * Xcode 27 Build — build error caused by invalid `@State` init assignments is resolved. ## 3.0.0-rc20 **Release Date:** June 5, 2026 ADDED * JSON Schema — added decorator definitions to the validation schema. * Table Row Edit Form — added required-field indicator support in the row edit form. CHANGED * Table/Collection Performance — faster bulk edits, row inserts, and deletes in large tables. * Table Required Indicator — moved indicator from column header to cell for smoother horizontal scrolling. FIXED * Change API Sync — date, chart, signature, and dropdown fields now stay in sync with external value updates. * Field Decorators — fixed padding for decorators when field title is missing or empty. ## 3.0.0-rc19 **Release Date:** May 20, 2026 CHANGED * Row Forms — open in readonly mode with disabled cells while decorator actions stay tappable. * Row Delete Payload — rowDelete change logs now include the deleted row data alongside existing identifiers. **How to use it** ```swift theme={null} // Inside your onChange handler if change.target == "field.value.rowDelete", let deletedRow = change.change?["row"] as? [String: Any] { let rowId = deletedRow["_id"] as? String // deletedRow contains the full row payload (cells, children, etc.) } ``` FIXED * Row Form Text Column — no longer emits multiple changelogs on a single tap. * Row Decorator Column — now appears whenever decorator data exists, even when the `decorate` flag is missing. * Mobile Page Duplicate — duplicated pages reset to `deletable` `true` and `copyable` `[.withValues, .withoutValues]`. ## 3.0.0-rc18 **Release Date:** May 7, 2026 ADDED * **Decorator Icons** — added support for two new decorator icons: `pencil` and `pen-to-square`. CHANGED * **Row Decorator Column** — added horizontal scroll for inline decorators. * **Chart Blank Coordinates** — blank `x/y` values on chart points are now saved as `null` and displayed as empty in the UI, instead of being coerced to `0`; schema bumped to `1.0.1` to allow nullable coordinates. FIXED * **Decorators Visibility & Interaction** — decorators are now visible and interactive on readonly forms and empty title fields. * **Duplicate Page Without Values** — duplicating a page without values now keeps read-only and display-text field values intact. ## 3.0.0-rc17 **Release Date:** April 28, 2026 ADDED * **Decorator API** — Added support for row- and cell-specific decorators in table and collection fields, accessible via scoped path targeting. FIXED * **Crash Fix** — Resolved a crash (non-production) that could occur when dismissing bulk edit in collection field. * **Threading** — Removed background thread access to published properties in table and collection view models across multiple call chains. ## 3.0.0-rc16 **Release Date:** April 14, 2026 ADDED * **Decorators** — Dynamic decorators API with live UI updates. ```swift theme={null} import Joyfill import JoyfillModel var decorator = Decorator() decorator.icon = "comment" decorator.label = "Review" decorator.color = "#3B82F6" decorator.action = "open_review" // path formats: // field -> "pageId/fieldPositionId" // row -> "pageId/fieldPositionId/rowId" // column -> "pageId/fieldPositionId/rowId/columnId" let path = "\(pageId)/\(fieldPositionId)/\(rowId)/\(columnId)" // Add documentEditor.addDecorators(path: path, decorators: [decorator]) // Read let decorators = documentEditor.getDecorators(path: path) // Update (matched by action) decorator.label = "Open Review" documentEditor.updateDecorator(path: path, action: "open_review", decorator: decorator) // Remove documentEditor.removeDecorator(path: path, action: "open_review") ``` * **Validation** — Path-scoped validation via `validate(path:)`, including support for row- and cell-level checks. **Breaking:** `ComponentValidity` includes `.notFound` when the path is invalid or cannot be resolved (for example unknown field position, or missing row/column for table/collection paths). ```swift theme={null} // path formats: // row -> "pageId/fieldPositionId/rowId" // cell -> "pageId/fieldPositionId/rowId/columnId" let result = documentEditor.validate(path: path) switch result { case .notFound: // invalid or unresolvable path break case .page(_), .field(_), .row(_), .cell(_): break } ``` FIXED * **Page list view** — Fixed UI hangs with 300+ pages. * **Navigation** — Fixed row edit sheet dismiss/reset behavior during programmatic navigation. * **Logging** — Wrapped `Log()` with `#if DEBUG` to eliminate string construction overhead in release builds. ## 3.0.0-rc15 **Release Date:** April 1, 2026 FIXED * **Collection cross-schema row navigation** — Fixed a crash that occurred when navigating to a row belonging to a different schema within the same collection. ## 3.0.0-rc14 **Release Date:** March 30, 2026 ADDED * **Scoped validation** — The `validate` API now supports validating a specific path via `validate(path:)` (for example a page or a single field), not only the full form. * **Form footer** — Added a `formFooter` modifier on the SDK so host apps can inject a footer across form screens. ```swift theme={null} Form(documentEditor: documentEditor) .formFooter { MyFooterView() } ``` ## 3.0.0-rc13 **Release Date:** March 25, 2026 ADDED * **Per-page action controls** — Page objects now support configuration properties that allow disabling deletion and/or duplication on a per-page basis; pages with disabled actions hide the corresponding buttons in the paginator UI. * **Cell-level focus and blur** — Table and Collection fields now emit focus and blur events at the cell level. * **Date/time column filtering** — Table and Collection support filtering on date/time columns. FIXED * **Form loading** — Improved load time for large templates by optimizing schema validation. ## 3.0.0-rc12 **Release Date:** March 13, 2026 ADDED * **Decorators** — Integrators can attach icons or text to fields or rows; tapping them fires a custom event to the host application, enabling custom UI actions. * **Example project** showing how to keep a custom header visible while using the Joyfill form, including when the SDK presents modals — [SimpleUIkitProject](https://github.com/joyfill/SimpleUIkitProject). FIXED * UI now stays in sync with the document during bulk edits and row form edits, ensuring filled fields are correctly recognized during validation. *** ## 3.0.0-rc11 **Release Date:** February 26, 2026 ADDED * **Column conditional logic** — Table and collection columns can now be shown or hidden based on conditional logic rules, with support across page duplication. * **View-based field and column hiding** — Fields and columns with hiddenViews set for the current view type are now force-hidden, taking top priority over conditional logic. * **Navigation: column targeting and focus** — goto() now supports full path pageId/fieldPositionId/rowId/columnId for cell-level navigation. GotoConfig(focus: true) triggers the onFocus callback for programmatic field focus. CHANGED * **Validation with row and cell output** — The validate() method now returns row-level and cell-level validity for table and collection fields, with goto() support to navigate directly to invalid fields. *** ## 3.0.0-rc10 **Release Date:** February 12, 2026 ADDED * **Page focus and blur events** — When the current page changes (e.g. on load or after `goto`), `onFocus` and `onBlur` receive page events via `event.pageEvent` (e.g. `"page.focus"`, `"page.blur"`). **Breaking:** `onFocus` and `onBlur` now take `Event` instead of `FieldIdentifier`. Use `event.fieldEvent` for field focus/blur and `event.pageEvent` for page focus/blur. ```swift theme={null} func onFocus(event: Joyfill.Event) { if let pageEvent = event.pageEvent { print("Page focused: \(pageEvent.type)") } } func onBlur(event: Joyfill.Event) { if let pageEvent = event.pageEvent { print("Page blurred: \(pageEvent.type)") } } ``` * **Metadata** — Support for reading and updating field-level metadata and row-level metadata for table and collection rows. You can react to changes in `onChange` and update metadata via the editor's Change API. * **Row-level navigation** — The `goto` method now accepts a `GotoConfig` parameter. Use path `pageId/fieldPositionId/rowId` to navigate to a table or collection row, and `GotoConfig(open: true)` to open the row form modal. CHANGED * Duplicate page fixes for mobile and desktop; formulas handled correctly when duplicating pages. *** ## 3.0.0-rc9 **Release Date:** January 30, 2026 ADDED * External navigation API — added `goto(pageId/fieldPositionId)` function to programmatically control page and field navigation with auto scroll. CHANGED * Enhanced page deletion logic to properly handle shared fields and field positions across web and mobile views. FIXED * Page duplication breaking conditional logic when duplicating mobile view pages. * Crash when opening row form caused by data inconsistency between row order and cell values. * Remove extra spacing between date field and title. *** ## 3.0.0-rc8 **Release Date:** December 19, 2025 ADDED * Single-click option to open a row form for Collection and Table fields. * “Select All” rows functionality for filtered collections and tables. * Ability to delete rows while filters are active in Collection fields. * Page deletion support. * Enhanced Page List UI. CHANGED * Color handling updated to support RGBA color format. FIXED * Issue where the Date field did not update on the first change due to double parsing. * Ensured the last page always remains visible, regardless of hidden state or conditional logic. *** ## 3.0.0-rc7 **Release Date:** November 28, 2025 ADDED * Support for inserting a row while filters or sorting are active — both Insert Below and Add Row now behave intuitively. CHANGED * Updated the default format for date fields and corrected 24-hour format handling. * Signature field layout — Clear and Save buttons are now aligned to the same height for a more consistent UI. * Performance optimizations for Table/Collection. * Added space between the Add button and the required field indicator in Collection fields for improved clarity. FIXED * Crash caused by on-change handler when side-by-side form changes were misaligned. * SDK crash when selecting a date due to device-level 24-hour format settings. * Bulk edit creating unwanted empty changelog entries. * Field parsing issue where “deleted” property was interpreted as a double instead of a boolean. * Various issues found in the example project. *** ## 3.0.0-rc6 **Release Date:** November 13, 2025 ADDED * Row navigation now keeps the target row in view when closing the row form in Collection and Table fields. FIXED * Fixed UI empty space in collection quick view after expanding a single row and navigating back. * Fixed the date field clear (X) button by increasing its tap area for more reliable interaction. * Fixed the validation helper bug for required collection fields. *** ## 3.0.0-rc5 **Release Date:** October 30, 2024 Learn more in the [release notes](https://github.com/joyfill/components-swift/releases/tag/3.0.0-rc5). ADDED * Support for custom date and time formats. * Highlighting for the selected row in Table and Collection fields to improve user visibility. CHANGED * Required field validation — added page ID to validation status object for more accurate field tracking. FIXED * Multi-selection text-to-box image alignment issue. * Crash in Table Quick View in change handler. * Handling of decimal numbers without a leading zero in formulas. *** ## 3.0.0-rc4 **Release Date:** October 10, 2024 Learn more in the [release notes](https://github.com/joyfill/components-swift/releases/tag/3.0.0-rc4). ADDED * Scrolling support in table column titles to ensure visibility when titles are very large. CHANGED * Disabled editing of text and barcode column cells in Table/Collection when in read-only mode. * Disabled duplicate page functionality in read-only mode. * Disabled all event handlers while in read-only mode to prevent unintended interactions. * Adjusted Table/Collection colors for proper appearance in dark mode. FIXED * Table/Collection image cell triggering `onChange` twice. * Date field calling `onFocus` before the `onChange` event. * Memory leak and retain cycle between `DocumentEditor` and `JoyfillDocContext`. * Block field appearance when color value is empty. * Filtering number columns with `0` returned empty rows. * Include timezone (`tz`) property in changelog when updating date columns in tables to ensure accurate time handling. *** ## 3.0.0-rc3 **Release Date:** September 25, 2024 Learn more in the [release notes](https://github.com/joyfill/components-swift/releases/tag/3.0.0-rc3). CHANGED * Updated README documentation. FIXED * Multithreading crash related to collection field initialization. * `onBlur` events not being called. * Collection field quick view image column count not updating. *** ## 3.0.0-rc2 **Release Date:** September 19, 2024 Learn more in the [release notes](https://github.com/joyfill/components-swift/releases/tag/3.0.0-rc2). CHANGED * Updated README documentation. FIXED * Changes made by change handler being discarded on page switch. *** ## 3.0.0-rc1 **Release Date:** September 17, 2024 Learn more in the [release notes](https://github.com/joyfill/components-swift/releases/tag/3.0.0-rc1). ADDED * Collection field. * Formula support. * License-based collection field enforcement. * Support for new table column types in Table and Collection field. * Collection sort and filter. * Collection conditional logic. * Collection field validation. * Support for Change handler to modify the form externally/programmatically. * Date time zone support. * Image field URL replacement. * Block field additional styling. * Insensitive conditional logic for texts. CHANGED * Updated README and related docs. FIXED * Crash issues by removing force unwraps. * Field ordering. *** ## 2.0.6 **Release Date:** July 6, 2024 Learn more in the [release notes](https://github.com/joyfill/components-swift/releases/tag/2.0.6). ADDED * Support for block field styles. * Support for updating image URLs from outside via document editor with live UI refreshing. * Support for single and multiple images for table image column. FIXED * Showing image picker in already presented sheets. * Random crashes by removing force unwraps across the project. * Issues related to image field (image reset bug on re-upload of same image in single-selection). * Case sensitivity in conditional logic (removed case sensitivity for text). * Title visibility when converting web view to mobile view. * Checkbox issue related to multi property. * Add empty cells object when inserting new rows in table. * Memory leaks by using `weak self` in table closures. *** ## 2.0.5 **Release Date:** April 25, 2024 Learn more in the [release notes](https://github.com/joyfill/components-swift/releases/tag/2.0.5). FIXED * Image getting replaced in image field if upload handler is not called when upload button tapped. * `int64` parsing issue in model. *** ## 2.0.4 **Release Date:** April 17, 2024 Learn more in the [release notes](https://github.com/joyfill/components-swift/releases/tag/2.0.4). ADDED * Page duplication. * User access token handling in example project. FIXED * Table image view UI on iPad. * Field ordering in validations. * Crash on invalid JSON. * Missing `int` case in `valueUnion` in model (crash fix). *** ## 2.0.3 **Release Date:** February 26, 2024 Learn more in the [release notes](https://github.com/joyfill/components-swift/releases/tag/2.0.3). CHANGED * Made dictionaries public in the `JoyDoc` Model. *** ## 2.0.2 **Release Date:** February 19, 2024 Learn more in the [release notes](https://github.com/joyfill/components-swift/releases/tag/2.0.2). FIXED * Form content being obscured by the keyboard for text fields. * Extra space in the Table field on iOS 15. * Keyboard auto-dismiss on table text cell for the first row on iOS 15. *** ## 2.0.1 **Release Date:** February 14, 2024 Learn more in the [release notes](https://github.com/joyfill/components-swift/releases/tag/2.0.1). CHANGED * Applied conditional logic to the current `pageId` passed in `Form` init. * Improved web → mobile view conversion when multiple fields share the same Y position. FIXED * Logical `AND` / `OR` corner cases in conditional logic. * iPad rotation issue in UIKit Sample Project. *** ## 2.0.0 **Release Date:** December 24, 2023 Learn more in the [release notes](https://github.com/joyfill/components-swift/releases/tag/2.0.0). ADDED * Support hidden property for the fields header. CHANGED * Improved table field performance and overall form performance for large forms. * Updated conditional logic APIs. FIXED * Unsupported column type. * Table navigation on iPad. *** ## 1.2.2 **Release Date:** November 12, 2023 ADDED * Insert below & move row in table field. *** ## 1.2.1 **Release Date:** November 9, 2023 FIXED * Model version issues. *** ## 1.2.0 **Release Date:** October 15, 2023 ADDED * Only validate current visible view for field validator. * Cocoapods support. *** ## 1.1.0 **Release Date:** September 25, 2023 ADDED * Table field search, filter, sort, bulk update, and more. * Test cases for selection field. * Unit test cases for validation fields. CHANGED * Improved performance for large tables (1,000+ rows) to prevent crashes. * Handled unsupported field types gracefully. FIXED * Table title display issue. * Padding for fields in form view. *** ## 1.0.6 **Release Date:** July 26, 2023 ADDED * UI test case for selection fields. CHANGED * Updated README documentation for validation. FIXED * Validation not working properly with conditional logic. * Drop-down field showing deleted options. *** ## 1.0.5 **Release Date:** July 24, 2023 ADDED * New validation APIs for improved form handling. * Support for duplicating rows in table fields. FIXED * Conditional logic not working for single-select and multi-select fields. * Crash when adding a new row in an empty table view. *** ## 1.0.4 **Release Date:** July 9, 2023 ADDED * Conditional logic functionality. FIXED * Multiple choice issue where deleted options were still showing. *** ## 1.0.3 **Release Date:** June 6, 2023 FIXED * Page navigation now hidden when page count is 1. * Tooltip visibility issue when explicitly set to hidden. *** ## 1.0.2 **Release Date:** June 5, 2023 ADDED * Page navigation functionality. * Field tooltip for better usability. * Documentation for public APIs. * UI test cases in the example project. * Unit test cases in the example project. CHANGED * Improved overall performance and minor stability enhancements. FIXED * Issues related to the signature field. *** ## 1.0.1 **Release Date:** May 13, 2023 FIXED * An issue in the `JoyDoc` model where the app was crashing when values in some of the fields were nil. *** ## 1.0.0 **Release Date:** May 7, 2023 ADDED * JoyDoc model to be JSON/dictionary-backed for future-proofing. * Data can now be accessed via `document.dictionary`, and initialization can be done using a dictionary input. CHANGED * Improved overall performance and resolved minor issues. * No changes to public APIs (getters/setters). FIXED * Empty form appearing for newly created documents. * Table row addition not triggering updates correctly. * Chart coordinate values not updating. * Signature filled on mobile not displaying on web. REMOVED * Deprecated API service dependency. *** ## 0.2.17 **Release Date:** April 16, 2023 ADDED * Support for the `disabled` field across the entire form. CHANGED * Updated model with newly added fields to fix parsing issues and missing data when saving documents. * Improved code structure through cleanup and general performance optimizations. FIXED * UI alignment issues in the `Date` field. *** ## 0.2.16 **Release Date:** April 4, 2023 CHANGED * Updated public API — `JoyFillView` renamed to `Form`, and `currentPageID` renamed to `pageID` for consistency with Android SDK naming conventions. FIXED * Image alignment issue in image view. * Console warnings. * Table editing changes not reflecting in the Table Quick View. *** ## 0.2.15 **Release Date:** April 1, 2023 CHANGED * Improved overall performance and stability. FIXED * Various minor bugs and issues. *** ## 0.2.14 **Release Date:** March 27, 2023 REMOVED * Unused dependency `SwiftUICharts`. *** ## 0.2.13 **Release Date:** March 26, 2023 ADDED * Chart field. * Rich Text field. CHANGED * Improved overall performance and stability. FIXED * Minor bugs and issues. *** ## 0.2.12 **Release Date:** March 18, 2023 FIXED * Minor bugs and issues. *** ## 0.2.11 **Release Date:** March 15, 2023 ADDED * Implementation of all field types with full support. * Full light mode support (dark mode coming soon). CHANGED * Improved change handlers for each field to provide proper variable support (see docs/readme). * This is a beta release; some bugs may occur — please report them to the Joyfill team. * Refer to [official documentation](https://docs.joyfill.io/docs/swift) for more details. REMOVED * Previous SDK — now fully replaced by the new implementation. *** ## 0.2.10 **Release Date:** March 15, 2023 🎉 **First public version of the Joyfill Swift SDK.** ADDED * Implementation of all field types with full support. * Full light mode support (dark mode coming soon). CHANGED * Improved change handlers for each field — now provide proper variable support (see docs/readme). * This is a beta release — occasional bugs may occur; please report them to the Joyfill team. * Refer to the [official documentation](https://docs.joyfill.io/docs/swift) for more details. REMOVED * Previous SDK — fully replaced by the new Swift SDK. *** ## 0.2.9 **Release Date:** March 15, 2023 FIXED * Minor bugs and issues. *** ## 0.2.8 **Release Date:** March 14, 2023 ADDED * Pre-release version for testing as the main release. *** ## 0.2.7 **Release Date:** March 14, 2023 ADDED * Handle TableView onFocus. *** ## 0.2.3 **Release Date:** March 14, 2023 ADDED * Handle TableView onFocus. *** ## 0.2.3-beta **Release Date:** March 14, 2023 ADDED * Handle TableView onFocus. # Getting started Source: https://docs.joyfill.io/ios/getting-started ## Add Form on iOS This guide explains how to integrate Joyfill iOS SDK into a new or existing iOS project using [SwiftUI](https://developer.apple.com/documentation/SwiftUI) By the end, you’ll be able to display a Form. ## Requirements * A Mac running macOS * The [latest stable version of Xcode](https://developer.apple.com/xcode/) ## Creating your project Skip this step if you’re using an existing project. If you’re starting a new iOS app, follow the steps below. **Steps:** 1. Open Xcode and select **File** > **New** > **Project**. 2. Select the **iOS** tab and choose the **App** template. 3. Enter your product name (for example, Joyfill-Demo) and organization identifier (for example, com.example). 4. Click **Next**, choose a location to save the project, and click **Create**. ## Adding Joyfill to your project **Steps:** 1. In Xcode, select your project. 2. Go to the **Package Dependencies** tab and click **+**. 3. Enter the Joyfill Swift Package URL into the search field: ```swift theme={null} https://github.com/joyfill/components-swift.git ``` 4. In the **Dependency Rule** fields, select: * **Version** > **Up to Next Minor** — for controlled updates 5. Click **Add Package**. Confirm the addition. > In this confirmation step, ensure that the Add to Target column displays the correct target (usually the app target), and that the checkbox on the left is selected. For many projects — especially newly created ones — there will only be one target, so you can click the Add Package button again. However, for more complex projects with multiple targets, it’s important to double-check this setting. Joyfill will now appear under **Swift Package Dependencies**. ## Displaying a Form **Steps:** 1. Add your JSON file to the project by dragging it into the Xcode project navigator. Click **Finish** when prompted. 1.1 Here is an example json you can use [first-form.json](https://github.com/joyfill/components-swift/blob/update-readme-file/JoyfillSwiftUIExample/JoyfillExample/Simple%20Form%20Example/first-form.json) 2. In your SwiftUI view, import the Joyfill SDK: ```swift theme={null} import Joyfill import JoyfillModel ``` 3. Display the Form using the following code: ```swift theme={null} import Joyfill import JoyfillModel struct SimpleFormExampleView: View { let documentEditor: DocumentEditor let document = loadDoc(named: "first-form") init() { self.documentEditor = DocumentEditor(document: document) } var body: some View { Form(documentEditor: documentEditor) } } private extension SimpleFormExampleView { static func loadDoc(named name: String) -> JoyDoc { let url = Bundle.main.url(forResource: name, withExtension: "json")! let data = try! Data(contentsOf: url) let dict = try! JSONSerialization.jsonObject(with: data) as! [String: Any] return JoyDoc(dictionary: dict) } } ``` 4. Build and run your application. You’ll now see your First Form document displayed using Joyfill built-in UI. ## Listen for form changes The Joyfill SDK provides comprehensive event handling through the **FormChangeEvent** protocol, allowing you to respond to user interactions, data changes, file uploads, and errors in real-time. **Create an Event Handler** Implement the FormChangeEvent protocol: ```swift theme={null} import Joyfill import JoyfillModel final class ChangeHandler: FormChangeEvent { func onChange(changes: [Joyfill.Change], document: JoyfillModel.JoyDoc) { // Persist, sync, or react to changes if let firstChange = changes.first { print(">>>>>>>>onChange", firstChange.change ?? "") } } func onFocus(event: Joyfill.Event) { if let field = event.fieldEvent { // Field focused (e.g. field.fieldID) } else if event.pageEvent != nil { // Page focused (event.pageEvent?.type == "page.focus") } } func onBlur(event: Joyfill.Event) { if let field = event.fieldEvent { // Field blurred } else if event.pageEvent != nil { // Page blurred } } func onUpload(event: Joyfill.UploadEvent) { // File(s) uploaded/removed } func onCapture(event: Joyfill.CaptureEvent) { // Media captured (e.g., camera) } func onError(error: Joyfill.JoyfillError) { // Schema validation/version or runtime errors } } ``` Pass your event handler to the DocumentEditor: ```swift theme={null} let handler = ChangeHandler() let editor = DocumentEditor( document: myDocument, config: DocumentEditorConfig(events: handler) ) ``` # Decorators Source: https://docs.joyfill.io/ios/guides/decorators Decorators are tappable indicators (icon + label) attached to a **field**, **column**, **row**, or **cell**. Taps are delivered through **`onFocus`** so your app can run custom logic — navigation, uploads, etc. ## Decorator model ```swift theme={null} import JoyfillModel var d = Decorator() d.action = "openHelp" // required, unique within scope d.icon = "circle-info" // optional, see Supported icons d.label = "Help" // optional d.color = "#3B82F6" // optional, must be #RRGGBB ``` | Property | Type | Notes | | -------- | --------- | -------------------------------------------------------- | | `action` | `String` | **Required**. Non-empty. Unique within its scope (path). | | `icon` | `String?` | See [Supported icons](#supported-icons). | | `label` | `String?` | Text. | | `color` | `String?` | 6-digit hex (`#RRGGBB`). | A decorator renders only when it has a non-empty `icon` **or** `label`. Action-only entries are stored but not displayed. ## Constructing a path Every path starts with `pageId/fieldPositionId`. What you append after that determines what gets decorated. > **Reserved keywords.** The path grammar uses three reserved tokens — **`schemas`**, **`rows`**, and **`columns`**. Anything else in a path slot is treated as an id (page id, field-position id, row id, column id, or schema key). Don't use these keywords as ids. ### Field decorators Just the two ids. Applies to the field's header. ```swift theme={null} let fieldPath = "\(pageId)/\(fpId)" ``` ### Table — `/rows`, `/columns/colId`, or specific `rowId` / `rowId/colId` A table has four decorator scopes, two **common** (defaults applied everywhere) and two **specific** (overrides for one row or cell): | What you want | Append | Example | | ----------------------------------------------- | ---------------- | -------------------------------- | | Common decorators on **every row** | `/rows` | `pageId/fpId/rows` | | Decorators on **one specific row** | `/rowId` | `pageId/fpId/row_42` | | Common decorators on **every cell in a column** | `/columns/colId` | `pageId/fpId/columns/col_status` | | Decorators on **one specific cell** | `/rowId/colId` | `pageId/fpId/row_42/col_status` | Specific paths inherit from the matching common path on the first write — anything you set on `/rows` shows on `row_42` until you write to `row_42` directly. ### Collection — same as table, plus `/schemas/schemaKey/…` for nested rows A collection's **root** rows behave like a table — the four scopes above use the exact same path shapes. Take a "People" collection where each person row holds a nested "Addresses" schema: ``` schema "people" (root, children: [addresses]) schema "addresses" (nested under people) Rows: p_alice ← row in "people" addresses → [ addr_home, addr_work ] ← rows in "addresses", under p_alice p_bob addresses → [ addr_apt ] ``` **Common rows / columns** of any schema — root or nested — are schema-level defaults. Address them directly with `schemas/schemaKey/…`, no parent walk needed: | What you want | Path shape | Example | | ---------------------------- | -------------------------------------- | ----------------------------------------------- | | Common rows in any schema | `pageId/fpId/schemas/sk/rows` | `pageId/fpId/schemas/addresses/rows` | | Common columns in any schema | `pageId/fpId/schemas/sk/columns/colId` | `pageId/fpId/schemas/addresses/columns/col_zip` | **A specific nested row or cell** lives under a particular parent. Walk through that parent's row id, then `schemas/sk/`, then the nested row id: | What you want | Path shape | Example | | -------------------- | -------------------------------------- | --------------------------------------------------------- | | Specific nested row | `…/rowId/schemas/sk/nestedRowId` | `pageId/fpId/p_alice/schemas/addresses/addr_home` | | Specific nested cell | `…/rowId/schemas/sk/nestedRowId/colId` | `pageId/fpId/p_alice/schemas/addresses/addr_home/col_zip` | If `addresses` itself had children, you'd chain another `schemas/.../rowId/…` after `addr_home` — the same pattern repeats for every level. > **Schema keys** come from the field's `schema` map. The schema marked `root: true` holds top-level rows; its `children` array names the nested schemas reachable from a row in this schema. ## API Four methods, all on `DocumentEditor`. Errors are reported via `onError`. ```swift theme={null} let fieldPath = "\(pageId)/\(fieldPositionId)" editor.getDecorators(path: fieldPath) // -> [Decorator] editor.addDecorators(path: fieldPath, decorators: [d]) editor.updateDecorator(path: fieldPath, action: "openHelp", decorator: updated) editor.removeDecorator(path: fieldPath, action: "openHelp") ``` Same shape for every path scope. A few examples: ```swift theme={null} // Common row decorators on a table — applied to every row editor.addDecorators(path: "\(pageId)/\(fpId)/rows", decorators: [duplicate]) // Override on a specific row editor.addDecorators(path: "\(pageId)/\(fpId)/\(rowId)", decorators: [archive]) // Cell-specific decorator editor.addDecorators(path: "\(pageId)/\(fpId)/\(rowId)/\(colId)", decorators: [upload]) // Nested collection row let nestedPath = "\(pageId)/\(fpId)/\(parentRowId)/schemas/\(nestedSK)/\(nestedRowId)" editor.addDecorators(path: nestedPath, decorators: [comment]) ``` ## Behavior to know * **Copy-on-write seed.** First write to a row-self / cell scope seeds from the matching common scope, so existing common decorators stay visible on that row alongside your override. Subsequent writes diverge freely. * **Collection license gating.** Writes against a collection field require a license that enables collection features. Without it, the call emits `decoratorError` and is rejected. ## Handling taps Decorator taps come through **`onFocus`** with the decorator's `action` exposed on the field event's `type` / `target`. `rowIds` / `columnId` / `parentPath` on `FieldIdentifier` tell you where the user tapped. ```swift theme={null} func onFocus(event: Joyfill.Event) { guard let field = event.fieldEvent else { return } if let action = field.type, !action.isEmpty { // Decorator tap print("Decorator:", action, "field:", field.fieldID, "rows:", field.rowIds ?? [], "column:", field.columnId ?? "-") } else { // Ordinary field focus } } ``` See [Event handling](/ios/guides/event-handling) for the full focus/blur flow. ## Errors All four APIs report through `onError` as `JoyfillError.decoratorError(DecoratorError)`: * Path didn't resolve (bad ids, deleted row, malformed grammar) * Validation (`action` empty, `color` not `#RRGGBB`) * Duplicate `action` in batch or against an existing entry * `removeDecorator` / `updateDecorator` with an unknown `action` * Collection write without a valid license Reads (`getDecorators`) on an unresolvable path also emit `onError` and return `[]`. ## Display limits `DecoratorConfig`, passed to `DocumentEditor` at init, controls how many decorators render inline before the rest collapse into a kebab menu. ```swift theme={null} let config = DecoratorConfig( visibleLimitInFields: 2, // field + column scopes; default 2 visibleLimitInRows: 1 // row scopes; default 1 ) let editor = DocumentEditor(document: doc, decoratorConfig: config) ``` ## Supported icons The SDK maps common names to bundled artwork or SF Symbols, including: `camera`, `import`, `paperclip`, `image`, `file`, `comment`, `comments`, `upload`, `download`, `rotate`, `cloud`, `filter`, `share`, `paper-plane`, `folder`, `folder-open`, `magnet`, `eye`, `circle-info`, `add`, `plus`, `print`, `flag`, `pencil`, `pen-to-square`. Unknown names fall back to a default symbol. # Event Handling Source: https://docs.joyfill.io/ios/guides/event-handling This guide covers the event callbacks available in Joyfill forms and when they're triggered. ### **Available Events** | Event | When Triggered | Purpose | | --------- | -------------------------------------- | -------------------------- | | onChange | Field value changes | Track form changes | | onFocus | Field gains focus, or page gains focus | Handle field or page focus | | onBlur | Field loses focus, or page loses focus | Handle field or page blur | | onUpload | File upload requested | Handle file uploads | | onCapture | Barcode scan requested | Handle barcode scanning | ## **onChange Event** Triggered when any field value changes in the form. **Parameters:** * changes: \[Change] - Array of change objects describing what was modified * document: JoyDoc - The fully updated document with all changes applied ```swift theme={null} func onChange(changes: [Change], document: JoyDoc) { if let firstChange = changes.first { print(">>>>>>>>onChange", firstChange.change ?? "") } // The document parameter contains the fully updated form } ``` ## **onFocus Event** Triggered when a field receives focus, or when a page becomes the current page (page focus). **Parameters:** * event: **Event** — Either a field focus or a page focus. Use `event.fieldEvent` for field focus, or `event.pageEvent` for page focus. **Event properties:** * `event.fieldEvent` — Set for field focus; contains `FieldIdentifier` (e.g. `fieldID`, `pageID`, `fieldPositionId`). * `event.pageEvent` — Set for page focus; contains `PageEvent` with `type: "page.focus"` and `page: Page`. ```swift theme={null} func onFocus(event: Joyfill.Event) { if let field = event.fieldEvent { print("Field focused: \(field.fieldID)") } else if let pageEvent = event.pageEvent { print("Page focused: \(pageEvent.page.id ?? "")") } } ``` ## **onBlur Event** Triggered when a field loses focus, or when the user leaves a page (page blur). **Parameters:** * event: **Event** — Either a field blur or a page blur. Use `event.fieldEvent` for field blur, or `event.pageEvent` for page blur. **Event properties:** * `event.fieldEvent` — Set for field blur. * `event.pageEvent` — Set for page blur; contains `PageEvent` with `type: "page.blur"` and `page: Page`. ```swift theme={null} func onBlur(event: Joyfill.Event) { if let field = event.fieldEvent { print("Field blurred: \(field.fieldID)") } else if let pageEvent = event.pageEvent { print("Page blurred: \(pageEvent.page.id ?? "")") } } ``` ## **onUpload Event** Triggered when a file upload is requested for image or file fields. **Parameters:** * event: UploadEvent - Upload event details **Properties Available:** * fieldEvent: FieldIdentifier - The field requesting upload * multi: Bool - Whether multiple files are allowed * uploadHandler: (\[String]) -> Void - Callback to provide file URLs ```swift theme={null} func onUpload(event: UploadEvent) { print("📤 Upload requested for field: \(event.fieldEvent.fieldID)") // Option 1: Present a photo picker for the user to choose an image. // Option 2: Directly send pre-uploaded image identifiers or URLs to the upload handler. let exampleImageURL = "https://example.com/uploads/sample-image.jpg" // Example: sending a direct image URL or an identifier for the upload event.uploadHandler([exampleImageURL]) } ``` ## **onCapture Event** Triggered when camera capture is requested for Barcode cell. **Parameters:** * event: CaptureEvent - Capture event details **Properties Available:** * fieldEvent: FieldIdentifier - The field requesting capture * captureHandler: (ValueUnion) -> Void - Callback to provide captured content ```swift theme={null} func onCapture(event: Joyfill.CaptureEvent) { print("📷 Capture requested for field: \(event.fieldEvent.fieldID)") print("User can open a barcode scanner to capture the code.") event.captureHandler(.string("Scan Button Clicked")) } ``` ## **onError Event** * Used to listen to errors during document processing. * error: JoyfillError — details about the failure. * Error types include: * schemaValidationError — Document schema validation failures * schemaVersionError — SDK and document version compatibility issues ```swift theme={null} func onError(error: Joyfill.JoyfillError) { switch error { case .schemaValidationError(let schemaError): print("❌ Schema Error: \(schemaError)") case .schemaVersionError(let versionError): print("❌ Schema Error: \(versionError)") } print("Error occurred: \(error)") } ``` # Navigation Source: https://docs.joyfill.io/ios/guides/external-navigation This document describes how to programmatically navigate to pages, fields, table/collection rows, and individual cells within a form using the `goto` API on `DocumentEditor`. ## Overview The `goto` method enables programmatic navigation to specific locations within a form. This is useful for: * Guiding users to required fields after validation * Implementing custom navigation flows * Deep linking to specific form sections * Auto-scrolling to specific fields * Opening a specific table or collection row in its row form (modal) * Focusing a specific cell within a table or collection row ## Path-Based Navigation Navigate using a slash-separated path string. Up to four segments are supported. ```swift theme={null} // Navigate to a page let status = editor.goto("page_123") // Navigate to a field on a page (with auto-scroll) let status = editor.goto("page_123/fieldPosition_456") // Navigate to a table or collection row (optionally open row form) let status = editor.goto("page_123/fieldPosition_456/row_789", gotoConfig: GotoConfig(open: true)) // Navigate to a specific cell in a table or collection row let status = editor.goto("page_123/fieldPosition_456/row_789/column_012", gotoConfig: GotoConfig(open: true, focus: true)) ``` | Path Format | Description | | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `"pageId"` | Navigate to the top of the specified page | | `"pageId/fieldPositionId"` | Navigate to the page and automatically scroll to the field | | `"pageId/fieldPositionId/rowId"` | Navigate to the page, scroll to the table/collection field, and select the row. Use `GotoConfig(open: true)` to also open the row form modal. | | `"pageId/fieldPositionId/rowId/columnId"` | Navigate to the page, scroll to the table/collection field, select the row, and target a specific column/cell. Use `GotoConfig(focus: true)` to auto-focus the cell. | **Important:** Use `fieldPositionId` from `page.fieldPositions[]._id`, not `fieldId` from `document.fields[]._id`. Using the wrong ID will cause navigation to fail. For row-level paths, the field must be a **table** or **collection** type. The `rowId` must match an existing row's ID in that field's value; otherwise `goto` returns `.failure`. For column-level paths, the `columnId` must match an existing visible column in the field; otherwise `goto` returns `.failure` (but still navigates to the row). ## GotoConfig Navigation behavior is configured via `GotoConfig`. Pass it as the second argument to `goto`. ```swift theme={null} public struct GotoConfig { /// When true, automatically opens the row form modal for table/collection rows. public let open: Bool /// When true, triggers the onFocus callback for the target field or cell. public let focus: Bool public init(open: Bool = false, focus: Bool = false) } ``` | Property | Default | Description | | -------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `open` | `false` | If the path targets a table/collection row, set to `true` to open that row's form in a modal after navigation. | | `focus` | `false` | When `true`, triggers the SDK's `onFocus` event for the target field or cell. For text, number, and barcode cells this also opens the keyboard. | Examples: ```swift theme={null} // Navigate to a row and open its form for editing let status = editor.goto("page_123/fieldPosition_456/row_789", gotoConfig: GotoConfig(open: true)) // Navigate to a field and trigger onFocus let status = editor.goto("page_123/fieldPosition_456", gotoConfig: GotoConfig(focus: true)) // Navigate to a specific cell, open the row form, and focus the cell let status = editor.goto("page_123/fieldPosition_456/row_789/column_012", gotoConfig: GotoConfig(open: true, focus: true)) ``` If you omit `gotoConfig`, the default `GotoConfig()` is used (`open: false`, `focus: false`). ## NavigationStatus The `goto` method returns a `NavigationStatus` indicating success or failure. ```swift theme={null} let status = editor.goto("page_123/fieldPosition_456", gotoConfig: GotoConfig()) switch status { case .success: // Navigation succeeded - target exists and is visible print("Successfully navigated to the field") case .failure: // Navigation failed - target doesn't exist, is hidden, or unsupported print("Navigation failed") } ``` | Response | Description | | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `.success` | The target exists, is supported, and is visible (and for row paths, the row exists; for column paths, the column exists and is visible). | | `.failure` | The target does not exist, is hidden, or has an unsupported field type. | **Failure reasons include:** * Page does not exist * Page is hidden (due to conditional logic) * Field position does not exist * Field is hidden (due to conditional logic) * For row-level paths: field is not a table/collection, or the row ID is not found in the field's value * For column-level paths: the column ID does not exist or is hidden When the page changes, the SDK emits page focus and page blur events. See [Event Handling](/ios/guides/event-handling#onfocus-event) for details. # Form Configuration Source: https://docs.joyfill.io/ios/guides/form-configuration This document describes the configuration options available when initializing a `DocumentEditor` for customizing form behavior, including page management, field interactions. ## Initialization with DocumentEditorConfig The recommended way to initialize a `DocumentEditor` is with a single `DocumentEditorConfig` object. This groups every editor option — mode, event handler, license, schema validation, page behavior, and display — into one configuration surface. ```swift theme={null} import Joyfill import JoyfillModel // 1. Page behavior let pageConfig = PageConfig( navigation: true, enableDuplicates: false, enableDeletes: false, currentPageID: nil ) // 2. Display & decorators let decoratorConfig = DecoratorConfig( visibleLimitInFields: 2, visibleLimitInRows: 1 ) let displayConfig = DisplayConfig( singleClickRowEdit: false, decorators: decoratorConfig ) // 3. Assemble the editor config let config = DocumentEditorConfig( mode: .fill, events: handler, // your FormChangeEvent handler (optional) license: "your-license", // optional validateSchema: true, page: pageConfig, display: displayConfig ) // 4. Create the editor let editor = DocumentEditor( document: myDocument, config: config ) ``` Every parameter has a default, so you only set what you need: ```swift theme={null} // Minimal — all defaults let editor = DocumentEditor(document: myDocument) // Just an event handler let editor = DocumentEditor( document: myDocument, config: DocumentEditorConfig(events: handler) ) ``` ### DocumentEditorConfig | Parameter | Type | Default | Description | | ---------------- | ------------------ | ----------------- | ------------------------------------------------------------------------------------------------------------------------- | | `mode` | `Mode` | `.fill` | Editing mode. `.fill` allows editing; `.readonly` disables edits (and page duplication/deletion). | | `events` | `FormChangeEvent?` | `nil` | Handler that receives change, focus, upload, capture, and error events. See [Event Handling](/ios/guides/event-handling). | | `license` | `String?` | `nil` | License token. A valid license enables licensed features such as Collection fields. | | `validateSchema` | `Bool` | `true` | When `true`, validates the document schema on init. See [Schema Validation](#schema-validation). | | `page` | `PageConfig` | `PageConfig()` | Page-navigation and page-operation behavior. | | `display` | `DisplayConfig` | `DisplayConfig()` | Field-interaction and decorator display behavior. | ### PageConfig | Parameter | Type | Default | Description | | ------------------ | --------- | ------- | ----------------------------------------------------------------- | | `navigation` | `Bool` | `true` | Show the page navigation UI. | | `enableDuplicates` | `Bool` | `false` | Allow users to duplicate pages (`.fill` mode only). | | `enableDeletes` | `Bool` | `false` | Allow users to delete pages (`.fill` mode only). | | `currentPageID` | `String?` | `nil` | Page to open initially. `nil` falls back to the first valid page. | ### DisplayConfig | Parameter | Type | Default | Description | | -------------------- | ----------------- | ------------------- | ------------------------------------------------------------------------------ | | `singleClickRowEdit` | `Bool` | `false` | Open table/collection rows for editing with a single tap. | | `decorators` | `DecoratorConfig` | `DecoratorConfig()` | Controls how many decorators show inline before overflowing into a kebab menu. | The per-parameter initializer shown in the sections below (for example `DocumentEditor(document:mode:events:...)`) is **deprecated**. It still works and forwards its arguments into a `DocumentEditorConfig`, but new code should use the config-based initializer above. ## Page Management ### Page Navigation Control the visibility of the page navigation UI. ```swift theme={null} let editor = DocumentEditor( document: myDocument, navigation: true // Show page navigation UI ) ``` | Parameter | Type | Default | Description | | ------------ | ------ | ------- | ---------------------------------------------------------------------------------------------------- | | `navigation` | `Bool` | `true` | When `true`, displays the page navigation dropdown button that allows users to switch between pages. | **Behavior:** * Clicking the button opens a modal sheet showing all available pages * The page selector respects conditional logic - hidden pages are not shown in the list ### Page Duplication Control whether users can duplicate existing pages. ```swift theme={null} import Joyfill import JoyfillModel let editor = DocumentEditor( document: myDocument, mode: .fill, // Page duplication only works in .fill mode isPageDuplicateEnabled: true ) ``` | Parameter | Type | Default | Description | | ------------------------ | ------ | ------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `isPageDuplicateEnabled` | `Bool` | `false` | When `true`, users can duplicate pages via the page navigation UI. The duplicated page includes all field values from the original. | **Behavior:** * Duplicated pages retain all field values from the source page, including conditional logic * The new page is inserted immediately after the source page in the page order * Field IDs are regenerated for all fields on the duplicated page to ensure uniqueness ### Page Deletion Control whether users can delete pages from multi-page forms. ```swift theme={null} let editor = DocumentEditor( document: myDocument, mode: .fill, // Page deletion only works in .fill mode isPageDeleteEnabled: true ) ``` | Parameter | Type | Default | Description | | --------------------- | ------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `isPageDeleteEnabled` | `Bool` | `false` | When `true`, users can delete pages via the page navigation UI. A confirmation dialog appears before deletion to prevent accidental data loss. | **Behavior:** * Delete is only available when there is more than one page (you cannot delete the last remaining page) * A confirmation dialog appears to user before deletion * Page deletion is permanent and cannot be undone * If the current page is deleted, the form automatically navigates to the next available page ## Field Interactions ### Single Click Row Edit Simplify the process for opening table and collection rows for editing. ```swift theme={null} let editor = DocumentEditor( document: myDocument, singleClickRowEdit: true ) ``` | Parameter | Type | Default | Description | | -------------------- | ------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `singleClickRowEdit` | `Bool` | `false` | When `true`, users can open a row for editing with a single tap. When `false`, users must go through multiple steps to edit a row. | **Behavior:** * **Default (`false`)**: Users must follow multiple steps to open a row form * **Enabled (`true`)**: Users can open the row form for editing with a single tap, providing a faster and easier editing experience * This setting applies to both Table fields and Collection fields * The edit behavior respects the form's mode - in `.readonly` mode, rows cannot be edited regardless of this setting ## Schema Validation Control whether the document schema is validated during initialization. ```swift theme={null} let editor = DocumentEditor( document: myDocument, validateSchema: true // Validate schema on init ) ``` | Parameter | Type | Default | Description | | ---------------- | ------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `validateSchema` | `Bool` | `true` | When `true`, validates the document schema version and structure during initialization. Validation errors are reported via the `onError` event handler. | **Behavior:** * If validation fails, `editor.schemaError` will contain error details * The form will display an error screen instead of the document * Validation errors are also sent to the `events.onError()` handler if configured * Set to `false` to skip validation (useful for testing or when you're certain the document is valid) ## Programmatic Page Operations You can also perform page operations programmatically using the `DocumentEditor` API. ### Duplicate a Page ```swift theme={null} // Duplicate a page programmatically editor.duplicatePage(pageID: "page_123") ``` ### Navigate to a Page ```swift theme={null} // Switch to a different page editor.currentPageID = "page_456" ``` # Form footer Source: https://docs.joyfill.io/ios/guides/form-footer ## What it is `formFooter` is a SwiftUI modifier on the Joyfill `Form` that adds **your** bottom bar—buttons, labels, validation hints, or any other views—**above the home indicator and safe area**. The SDK keeps that bar on screen as users move through the main form and many nested flows (for example chart detail, signature canvas, or table and collection row forms), so you do not have to rebuild the same toolbar on every screen yourself. It is **not** configured on `DocumentEditor` or in JSON; it is plain SwiftUI you compose next to `Form`. ## How to use 1. Chain **`.formFooter { … }`** on `Form` or on **any parent** that wraps `Form` (the modifier only needs to sit **above** `Form` in the view hierarchy). 2. Build the footer inside the closure like any other SwiftUI view. You can use `@State`, `ObservableObject`, and animations there; when that state changes, the footer updates. ```swift theme={null} import SwiftUI import Joyfill import JoyfillModel struct MyFormScreen: View { let documentEditor: DocumentEditor var body: some View { Form(documentEditor: documentEditor) .formFooter { HStack { Button("Save") { // e.g. persist or call your API } Button("Submit") { } } .padding() .frame(maxWidth: .infinity) .background(Color(.secondarySystemBackground)) } } } ``` * If you **do not** add `formFooter`, the SDK does not add a footer; layout matches earlier versions. * Keep the footer **compact** when possible so it leaves enough room for form content; long footers still respect the safe area but reduce visible scroll area. ## Show or hide the footer by page You can turn the footer **on or off for specific pages** using the same **page focus and blur** callbacks described in [Event handling](/ios/guides/event-handling). When the user moves to another page, the SDK calls `onFocus` with `event.pageEvent` (`type` is `"page.focus"`) and calls `onBlur` on the previous page with `"page.blur"`. Each `PageEvent` includes `page` (use `page.id` for that page’s `_id` in the doc). 1. Keep a `Bool` (or similar) in SwiftUI state—often `@Published` on an `ObservableObject` you pass to `DocumentEditor` as `events:`. 2. In `onFocus`, when `event.pageEvent` is set, update that flag from `page.id` (for example hide the footer when the id is in a `Set` of pages that should not show actions). 3. Optionally use `onBlur` with `event.pageEvent` for the same `type`/page shape when you need logic tied to **leaving** a page. 4. In `.formFooter { }`, only build the footer when your flag is `true` (use `if showFooter { … }`). The `Form` does not need extra APIs; your existing `FormChangeEvent` implementation drives visibility. ```swift theme={null} // Properties on the same type (e.g. @Published var showFooter; let pagesWithoutFooter: Set). // In your FormChangeEvent type (same object you pass as DocumentEditor’s `events:`). func onFocus(event: Joyfill.Event) { if let pageEvent = event.pageEvent, pageEvent.type == "page.focus", let id = pageEvent.page.id { showFooter = !pagesWithoutFooter.contains(id) } // Still handle field focus via event.fieldEvent when needed. } func onBlur(event: Joyfill.Event) { if let pageEvent = event.pageEvent, pageEvent.type == "page.blur" { // Optional: page being left is pageEvent.page } } // On your Form: .formFooter { if showFooter { HStack { Button("Save") { }; Button("Submit") { } } .padding() .frame(maxWidth: .infinity) .background(Color(.secondarySystemBackground)) } } ``` ## Example project The [Example app](https://github.com/joyfill/SimpleUIkitProject) includes a sample footer (show/hide) and matching form JSON. # Image Upload Handling Source: https://docs.joyfill.io/ios/guides/image-upload-handling The Joyfill SDK provides comprehensive image upload functionality through image fields, supporting both single and multiple image uploads with flexible handling options. **Architecture Overview** When users interact with image fields, the SDK triggers onUpload events that your application must handle by implementing the FormChangeEvent protocol. **Parameters** | Parameter | Type | Description | | ------------- | ------------------- | --------------------------------------------------- | | fieldEvent | FieldIdentifier | Contains metadata about the field requesting upload | | target | String? | The target action for the upload | | multi | Bool | Whether multiple files are allowed | | schemaId | String? | Schema identifier for table/collection fields | | parentPath | String? | Path to parent field for nested structures | | rowIds | \[String]? | Array of row IDs for table fields | | columnId | String? | Column identifier for table fields | | uploadHandler | (\[String]) -> Void | Callback function to provide uploaded URLs | **Detailed Example** For a complete working example with image picker implementation, see: [**ImageReplacementTest.swift**](https://github.com/joyfill/components-swift/blob/main/JoyfillSwiftUIExample/JoyfillExample/ImageReplacementTest.swift) - Demonstrates image upload with URL replacement **Common Usage Patterns** **Pattern 1: Immediate Server** ```swift theme={null} func onUpload(event: UploadEvent) { // 'urls' can come from your gallery, camera, or any other source let urls: [String] = [] event.uploadHandler(urls) } ``` **Pattern 2: Programmatic Image Replacement** You can replace images after upload using the replaceImageURL method: ```swift theme={null} func onUpload(event: UploadEvent) { // 'localURLs' can come from your gallery, camera, or any other source let localURLs: [String] = [] // Show images immediately event.uploadHandler(localURLs) // Upload and replace in background for localURL in localURLs { uploadToServer(localURL) { serverURL in documentEditor?.replaceImageURL( newURL: serverURL, url: localURL, fieldIdentifier: event.fieldEvent ) } } } ``` **Flow:** 1. User taps image field → SDK creates UploadEvent 2. Your onUpload handler receives the event 3. You present image picker/camera 4. Process selected images (resize, upload to server, etc.) 5. Call event.uploadHandler(\[urls]) with final URLs 6. SDK updates the form with the provided URLs # Form Modes Source: https://docs.joyfill.io/ios/guides/modes The Joyfill SDK supports two distinct modes that control how users can interact with your forms: **Fill Mode** and **Read-Only Mode**. **Fill Mode (Default)** **Fill Mode** (.fill) is the default mode that allows users to interact with and edit form fields. ```swift theme={null} // These are equivalent - Fill Mode is default let documentEditor1 = DocumentEditor(document: document) let documentEditor2 = DocumentEditor(document: document, mode: .fill) ``` **Features:** * Users can input data into all field types * File uploads and camera capture are enabled * Form validation is active * Page duplication is available (if enabled) * All interactive elements are functional **Read-Only Mode** **Read-Only Mode** (.readonly) displays forms for viewing only, preventing any user modifications. ```swift theme={null} let documentEditor = DocumentEditor( document: document, mode: .readonly // Must be explicitly set ) ``` **Features:** * All fields are disabled for input * File uploads and camera capture are disabled * Form data is displayed but cannot be modified * Page duplication is automatically disabled * Navigation remains functional for multi-page forms # Populating and Extracting Data Source: https://docs.joyfill.io/ios/guides/populating-and-extracting-data ## DocumentEditor Field Access The DocumentEditor provides several convenient properties and methods to access and work with form fields programmatically. ## Get Updated Document at Any Time You can access the current document state in two ways: **1. Direct Access from DocumentEditor** ```swift theme={null} // Get the current document with all updates let currentDocument = documentEditor.document ``` **2. From onChange Event** ```swift theme={null} func onChange(changes: [Change], document: JoyDoc) { // The document parameter contains the fully updated document let updatedDocument = document // This is the same as documentEditor.document at this moment print("Document updated with \(changes.count) changes") } ``` **allFields - Get All Fields** ```swift theme={null} let documentEditor = DocumentEditor(document: document) // Get all fields in the document let fields = documentEditor.allFields print("Total fields: \(fields.count)") // Iterate through all fields for field in documentEditor.allFields { print("Field: \(field.title ?? "No title") - Type: \(field.fieldType)") } ``` **field(fieldID:) - Get Specific Field** ```swift theme={null} // Get a specific field by ID if let field = documentEditor.field(fieldID: "textField1") { print("Field title: \(field.title ?? "No title")") print("Field type: \(field.fieldType)") } ``` **field(identifier:) - Get Field By Identifier** ```swift theme={null} // Get a specific field by identifier if let field = documentEditor.field(identifier: "textField1_identifier") { print("Field title: \(field.title ?? "No title")") print("Field type: \(field.fieldType)") } ``` **field(title:) - Get Field By Title** ```swift theme={null} // Get a specific field by title (case-insensitive) if let field = documentEditor.field(title: "Text Field 1") { print("Field title: \(field.title ?? "No title")") print("Field type: \(field.fieldType)") } ``` **getFieldIdentifier - Field Context Access** The getFieldIdentifier(for:) method provides complete context information about a field, which is essential for the Change API and field operations. ```swift theme={null} // Get field identifier by field ID let fieldIdentifier = documentEditor.getFieldIdentifier(for: "textField1") print("Field ID: \(fieldIdentifier.fieldID)") print("Page ID: \(fieldIdentifier.pageID ?? "No page")") print("File ID: \(fieldIdentifier.fileID ?? "No file")") print("Identifier: \(fieldIdentifier.identifier ?? "No identifier")") print("Position ID: \(fieldIdentifier.fieldPositionId ?? "No position")") ``` # Change API Programmatic updates to a JoyDoc via the editor. Apply multiple updates in one call. The Change API uses the change(changes:) method on DocumentEditor to apply programmatic updates to form fields. All changes are applied as an array of Change objects. ```swift theme={null} // Apply changes to the form documentEditor.change(changes: [change1, change2, change3]) ``` **Supported Change Types** | Target | Description | Usage | | ------------------------ | ------------------ | ------------------------------------ | | `field.update` | Update field value | Set text, numbers, dates, selections | | `field.value.rowCreate`  | Create table row | Add new rows to tables | | `field.value.rowUpdate` | Update table row | Modify existing table rows | | `field.value.rowDelete` | Delete table row | Remove rows from tables | | `field.value.rowMove` | Reorder table rows | Change row positions | **Basic Field Update** ```swift theme={null} let fieldId = "textField123" //Get field identifier let id = editor.getFieldIdentifier(for: fieldId) //Create change object let change = Change( v: 1, sdk: "swift", target: "field.update", _id: editor.documentID ?? "", identifier: editor.documentIdentifier, fileId: id.fileID ?? "", pageId: id.pageID ?? "", fieldId: id.fieldID, fieldIdentifier: nil, fieldPositionId: id.fieldPositionId ?? "", change: ["value": "Updated value"], createdOn: Date().timeIntervalSince1970 ) // Apply the change editor.change(changes: [change]) ``` **Create table row example** ```swift theme={null} let tableFieldId = "tableField1" let id = editor.getFieldIdentifier(for: tableFieldId) let newRow = Change( v: 1, sdk: "swift", target: "field.value.rowCreate", _id: editor.documentID ?? "", identifier: editor.documentIdentifier, fileId: id.fileID ?? "", pageId: id.pageID ?? "", fieldId: id.fieldID, fieldIdentifier: nil, fieldPositionId: id.fieldPositionId ?? "", change: [ "row": [ "_id": UUID().uuidString, "cells": [:] ], "targetRowIndex": 0 ], createdOn: Date().timeIntervalSince1970 ) editor.change(changes: [newRow]) ``` # Required Field Validation Source: https://docs.joyfill.io/ios/guides/required-field-validation The validation system checks required fields, validates data formats, and provides detailed feedback about validation status. ### **How Required Field Validation Works** Joyfill automatically validates required fields based on: * **Field requirement**: Fields marked as required = true * **Field visibility**: Hidden fields are always filtered out of the validation output * **Field values**: Required fields must have non-empty values to be valid **Basic Usage** ```swift theme={null} let documentEditor = DocumentEditor(document: document) // Validate all fields let validationResult = documentEditor.validate() if validationResult.status == .valid { print("Form is complete and valid") // Proceed with submission } else { print("Form has validation errors") // Show errors to user } ``` **Path-scoped validation (`validate(path:)`)** Use a path string when you only need validation for the whole document, one page, one field, or—on **table** and **collection** fields—a **single row** or **single cell**. Rules match `validate()`, scoped to the path depth. The result is a `ComponentValidity`: `.page`, `.field`, `.row`, `.cell`, or `.notFound`. * **`""`** (or whitespace-only) — same as `validate()`; `.page(Validation)`. * **`pageId`** — fields on that page only; `.page(Validation)`. * **`pageId/fieldPositionId`** — that field when the first segment is the page that owns the position; `.field(FieldValidity)`. If the page does not match, falls back to page-scoped validation for `pageId`. * **`pageId/fieldPositionId/rowId`** — **row-level** validation for table/collection fields; `.row(RowValidity)`. Use the row’s id from the field value. If the row is missing or not applicable, `.notFound`. * **`pageId/fieldPositionId/rowId/columnId`** — **cell-level** validation for a column in that row; `.cell(CellValidity)`. If the column or row is missing, `.notFound`. Surrounding whitespace around the path and next to `/` is ignored. For `.field`, you can use the `fieldValidity` helper on `ComponentValidity` when convenient. ```swift theme={null} // Field (any type) let fieldResult = documentEditor.validate(path: "\(pageId)/\(fieldPositionId)") if case .field(let fieldValidity) = fieldResult { // fieldValidity.status, fieldValidity.rowValidities (tables/collections), etc. } // Row (table or collection) let rowPath = "\(pageId)/\(fieldPositionId)/\(rowId)" let rowResult = documentEditor.validate(path: rowPath) if case .row(let rowValidity) = rowResult { // Row-level status and rowValidity.cellValidities } // Cell (table or collection column in a row) let cellPath = "\(pageId)/\(fieldPositionId)/\(rowId)/\(columnId)" let cellResult = documentEditor.validate(path: cellPath) if case .cell(let cellValidity) = cellResult { // Cell-level validation for that column } ``` **Key Points** * **Call validate()** before form submission * **Check status** for overall validation result * **Use fieldValidities** to get specific field errors * **Required fields** must have non-empty values * **Hidden fields** are always filtered out of the validation output (conditional logic and `hiddenViews`) * **Page hidden**: all of its fields are valid * **Table/Collection**: validate their required columns per row. Each `FieldValidity` includes `rowValidities` with row-level and cell-level results (`RowValidity` and `CellValidity`) * **Non-required table/collection fields** still validate rows against required columns * **Navigate to invalid fields**: use `fieldValidity.pageId` and `fieldValidity.fieldPositionId` with `goto()` to navigate directly to invalid fields * **Path-scoped checks**: use `validate(path:)` after a single edit, for one page submit, or with the same path shapes as `goto()` — including **`pageId/fieldPositionId/rowId`** and **`pageId/fieldPositionId/rowId/columnId`** for table/collection row and cell validation # Schema Validation Source: https://docs.joyfill.io/ios/guides/schema-validation This guide shows you how to validate document structure using Joyfill's schema validation system. ### **What is Schema Validation** Schema validation checks if your JSON document follows the correct Joyfill document structure before creating a form. ### **Enabling/Disabling Schema Validation** ```swift theme={null} let documentEditor = DocumentEditor( document: document, validateSchema: true // Default is true ) // Check for schema errors if let error = documentEditor.schemaError { print("Schema validation failed: \(error.message)") // Handle error - show message, prevent form display, etc. } ``` ### **Manual Validation** ```swift theme={null} let schemaManager = JoyfillSchemaManager() if let error = schemaManager.validateSchema(document: document) { print("Schema validation failed: \(error.message)") print("Error code: \(error.code)") print("Errors: \(String(describing: error.error))") } else { print("Document schema is valid") } ``` ### **How Schema Validation Works** When validateSchema = true: 1. **Document is validated** against the Joyfill schema during editor creation 2. **If validation fails**, an error is set and onError callback is triggered 3. **If validation passes**, the editor is created normally **Common Error Codes** | Error Code | Description | Solution | | ------------------------- | ----------------------------- | ---------------------- | | ERROR\_SCHEMA\_VALIDATION | Document structure is invalid | Fix document structure | | ERROR\_SCHEMA\_VERSION | Version incompatibility | Update SDK or document | **FormChangeEvent Integration** Schema errors automatically trigger the onError event: ```swift theme={null} func onError(error: Joyfill.JoyfillError) { switch error { case .schemaValidationError(let schemaError): print("❌ Schema Error: \(schemaError)") case .schemaVersionError(let versionError): print("❌ Schema Error: \(versionError)") } print("Error occurred: \(error)") } ``` ## **Important Notes** > ⚠️ **Default Behavior**: Schema validation is **enabled by default** (validateSchema = true) >  🚫 **Not Recommended**: Disabling schema validation can lead to crashes - only use for testing >  🔍 **Error Details**: Schema errors include both user-friendly messages and technical details >  ⚡ **Validation Timing**: Schema validation happens during editor creation, not during form interaction # How To Create Templates Source: https://docs.joyfill.io/joyfill-dev-sandbox/how-to-create-templates How to build your first JoyDoc Template in the Developer Portal # Intro In this guide we are going to show you how to quickly build your first Traditional Form Template. Inside of Joyfill Forms and PDFs are referred to as Documents and Templates so that is how we will refer to them both going forward. ### Before you continue with this guide please ensure that you've gone through the [getting started](/docs/quick-start) steps. # Navigate to your Template Library Once you've created a Joyfill Developer account you will now be able to start building out your Document Templates library. Document Templates are your saved forms and pdfs structures that you or your customers will use to populate with data, export to PDF, etc. Click "Add Template" to get started. This will navigate you to the template builder page. # Build your first template Now that you're on the template builder page we are going to start off by building a simple Work Order template. Follow the steps below and feel free to add any styles or extra fields you want. Just make sure add the fields mentioned below along with the identifiers so that the rest of the guide makes sense. **Step 1:** Drag and Drop an Image Field onto your template. Ensure that the new image field is selected with a blue border around it. Now you can upload a test logo in the field settings located in the right panel. **Step 2:** Drag and Drop a Display Text Field onto your template. Change the Display Text inside the field settings located in the right panel to say "Work Order". I also changed the following styles. **Step 3:** Drag and Drop a Short Text and a Long Text field onto your template. We are going to change the titles and identifiers of both these fields in the field settings located in the right panel. **Step 4:** Change the identifier of the Job # field to be "job\_number" and the Description field to be "job\_description". **Step 5:** Save your changes! # Summary Congratulations! You have officially built your first Joyfill template. Now that you have a template we can move on to using Joyfill inside your own project. *** # Key Terminology Source: https://docs.joyfill.io/joyfill-dev-sandbox/key-terminology Descriptions of Joyfill's main data types and concepts A note about terminology used in this documentation. There are a few terms we'll use that have alternate meanings, and you may be more familiar with the other definitions. We feel it's important for us to clarify the terminology below. * **Organization** - Your company or product that is integrating with Joyfill. * **Group** - One of your customers or an entity you want to group a set of templates, documents and users underneath. * **User** - An individual interacting with the Joyfill Platform. * **Organization User** - An individual/employee within your organization. For instance, one of your developers. * **Group User** - An individual/employee that belongs to one of your customers. * **Template** - A saved Form or PDF that will be filled out via a user or populated via API request repeatedly. * **Document** - A filled out template or a single use document. * **Template Instance** - A document that was generated from a Template. * **Single Instance** - A document that is generated and meant to be used only once. * **Library** - A saved list of Templates. ## Template vs Document The Joyfill platform supports documents and templates. Documents and templates both share a similar data structure (the [JoyDoc](/joyfill/joydoc-usage)). Documents and templates provide a way to organize and utilize the data based on its intended use case within your product or service. By sharing a similar data structure it makes it really easy to work with documents and templates together/interchangeably. ### Template The template type is used to power template based workflows within Joyfill. A template is a saved set of fields, layouts, styles, etc. that will be populated via api request or filled out by a user repeatedly. When you fill out a template it generates a document from the original template structure and schema. ### Document The document type is the default and general purpose type for managing the Joyfill document data. The document type can represent a single instance document, an instance of a filled out template and much more. *** # Joyfill Managed Source: https://docs.joyfill.io/joyfill-dev-sandbox/pdf-generator-joyfill Generate PDF files with the Joyfill API # Overview In this guide I'm going to show you how to utilize the Joyfill Managed PDF Export API to generate downloadable PDFs. # Guide ## Setup Requirements * Complete [Setup](/joyfill-dev-sandbox/setup) ## Implementation ### Step 1: Retrieve identifier You can retrieve the document or template identifier that you want to export via the Joyfill Manager (see screenshot below) or by using the Joyfill API. Identifiers will be unique to your organization. Ensure you use the identifiers from your account. Do not use the ID from the screenshot above because it will not work for you. ### Step 2: Generate PDF Once you have your identifier you can generate a PDF by using the [Export PDF API route](/api/documents/create-a-document-pdf-export). ## Examples ### Postman To send our HTTP request we are going to be using postman. Feel free to use whatever tool you want to make an http request to our Export API. **a. Add URL** **b. Configure Authorization** * **Type:** API KEY * **Key:** Authorization * **Value:** Bearer \ * **Add to:** Header **c. Add Body** **Important Note:** You must add an empty object in the body. *** # Joyfill Manager Source: https://docs.joyfill.io/joyfill-dev-sandbox/platform-joyfill-manager # What is the Joyfill Manager? The [Joyfill Manager](https://app-joy.joyfill.io/login) empowers anyone on your team (customer success, technical support, developers, etc.) to manage, create, and remove Templates, Documents, PDFs, Customers, Access Tokens, etc. all through an easy to use admin portal. No need to wait on the developers anymore. Everything can be managed, prepared, and published directly from the Joyfill Manager. ## Template Libraries All templates in your organization library or in a group library have a `DRAFT` and a `PUBLISH` state that can be used to toggle which form templates are available to your users. This allows your team to work on forms and digital PDF templates without your users having access to them until they are ready. ## Groups Groups are a way to manage templates, documents, and users for one of your customers. You can think of Groups as a way to group things under an individual customer. # Retrieve Template and Document Identifiers In the Joyfill Manager you can retrieve template and document identifiers under the ID column. *** # Setup Source: https://docs.joyfill.io/joyfill-dev-sandbox/setup Welcome to Joyfill! It's easy to get started with a Joyfill Developer account and add powerful form and digital PDF capabilities to your product or service. Just follow the steps below and you'll be up and running shortly. *** # Setup Steps The steps below are the prerequisites you will need to accomplish before moving on to the rest of our getting started guides. A couple of the steps below will be done in our [Joyfill Manager.](/joyfill-dev-sandbox/platform-joyfill-manager) ## Step 1: Create Joyfill Account To begin working with Joyfill, go to [Joyfill's Platform](http://app-joy.joyfill.io/) and create an account (*jump to step 2 if you already have an account*). By creating an account you will add yourself as the first user to your newly created Joyfill Organization and be placed inside the Joyfill Manager. ## Step 2: Generate Your `userAccessToken` Once you're inside the Joyfill Manager you will want to select from the top navigation bar Settings & Users -> Manager Users -> and click "Access Tokens" button next to your user. Once the modal appears select "Add Access Token". Copy and securely store your access token for later use. ## Step 3: Create Your First Template Create your first template within the Joyfill Manager by going to the [Template Library](https://app-joy.joyfill.io/library) tab in the top navigation and click the "Add Template". We recommended following this guide [Create Your First Template](/joyfill-dev-sandbox/how-to-create-templates) when creating your first template. This makes it easy to experiment with the different parts of the Joyfill Platform easily. ## Step 4: Get your template `identifier` Inside the Joyfill Manager you will want to select from the top navigation bar [Template Library](https://app-joy.joyfill.io/library) and under your Templates table list within the column `ID` copy that for pasting into our example React/JS guides. *** # Webhooks Source: https://docs.joyfill.io/joyfill-dev-sandbox/webhooks Webhooks allow you to subscribe to server-side notifications of events, like document update calls and newly available data. Webhooks are helpful to optimize your Joyfill integration. ## Subscribing to Webhook Events You can register a webhook by going to the [Joyfill Manager](https://app-joy.joyfill.io/login) -> Manage Settings & Users -> Webhooks. Here you can click the "**+ Enable Webhooks**" to get started. Here you will see our robust webhook manager interface. It lets you add endpoints, register for only specific events per endpoint, see event logs, and more! ### Available Events We are adding more all the time but below are the currently registerable webhook events from Joyfill's API. | Event Type | Payload | | --------------- | ----------------------------------------- | | document.create | Object[``](/joyfill/joydoc-usage) | | document.update | Object[``](/joyfill/joydoc-usage) | | document.delete | Object[``](/joyfill/joydoc-usage) | **Template Events** - The events listed above are used for both template and document data types. To determine if the event is associated with a template or a document simply check the `data.type` property of the webhook payload. ### Payloads All webhooks return the associated event-type data. For instance, below you can see an example of a `document.create` webhook event. ```bash bash theme={null} { "timestamp": 111414606 "eventType": "document.create" "data": { "_id": "64e541457eb07af9b070312e" "createdOn": 111414605 "identifier": "doc_64e5z14t7eb07af9b070312e" "metadata": {} "name": "Testing Doc" "source": "doc_64125cc452a50bdf0f84afbc" "stage": "published" "type": "document|template" } } ``` ## Setup For quickly testing and post-enabling webhooks in the Joyfill Manager we recommend pressing on the Add Endpoint action and from there using [Svix Playground](https://www.svix.com/play/) (see below). This will familiarize you with the payloads of each registered event on your end. ### Example Once you are familiar with the payloads coming in. You can now register your own endpoints. Repeat the steps above but put your own endpoint urls in where you plan to receive the webhooks. **Make sure to return a code 200 or a successful response**. This ensures you see proper webhook event response logs in the Joyfill Manager webhooks page. In order to retrieve the document that was associated with an event please use the `event.data.source`or `event.data.identifier`(document ID) to make a request to our API for that document. ```bash bash theme={null} // This example uses Express to receive webhooks const express = require('express'); const app = express(); const express = require("express") const app = express() const port = 3000 var bodyParser = require("body-parser") app.use(bodyParser.json({ type: "application/json" })) const getDocument = async (docIdentifier, userAccessToken) => { const response = await fetch(`https://api-joy.joyfill.io/v1/documents/${docIdentifier}`, { 'method': 'GET', 'mode': 'cors', 'headers': { 'Authorization': `Bearer ${userAccessToken}`, 'Content-Type': 'application/json' }, }); const data = await response.json(); return data; } app.post("/webhook", async (req, res) => { const event = req.body const eventDocument = await getDocument(event.data.identifier); switch (event.eventType) { case "document.update": // Then define and call a method to update a related record in your DB. // updateDocument(event.data.identifier, eventDocument); break case "document.created": // Then define and call a method to create add record in your DB. // createDocument(event.data.identifier, eventDocument); break default: console.log(`Unhandled event type ${event.eventType}`) } // Return a response to acknowledge receipt of the event res.send(200) }) app.listen(port, () => { console.log(`Example app listening on port ${port}`) }) ``` *** # Field Identifiers Source: https://docs.joyfill.io/joyfill/field-identifiers How to properly utilize field identifiers within the JoyDoc. # Overview Field identifiers inside of Joyfill allow you to associate your own internal ID or naming convention with a field. Your custom identifiers will help you clearly identify the data that is associated with each field on the form. Identifiers can be set programmatically or via the Joyfill SDK User Interfaces. See example below: ## Populate Use Cases * Identify user data entry for each field. * Pre-populate form field values. * Set custom selection options for dropdown fields, multi-select fields, and more. *** # Welcome to Joyfill Source: https://docs.joyfill.io/joyfill/index Embeddable form builder SDKs for every platform Joyfill Hero Light Joyfill Hero Dark # What is Joyfill? Joyfill provides you with ready-to-use embeddable UI SDKs, APIs, and Services that empower you to add powerful Form and PDF capabilities directly inside your own application on web and mobile. **To your end users**, Joyfill is an easy-to-use interface that allows them build any form, digitize any legacy PDF and then fill those documents out on web or mobile directly in your own applications. **To customer success teams**, Joyfill is an easy-to-use drag-and-drop form builder that allows them to build any kind of form or digitize any PDF provided by your customers. No need to wait on the developers anymore. Everything can be managed, prepared, and published directly inside the Joyfill Manager. **And to developers**, Joyfill is a powerful set of ready-to-use Embeddable UI Components and APIs that can used to add powerful form and digital PDF solutions directly inside your own product on web and mobile without the 1000s of hours spent on development and support. # Developer Documentation Build powerful, customizable form experiences with Joyfill. Integrate forms into your applications across iOS, Android, React Native, Web, or use our REST API for custom integrations. ## Get Started in Minutes Select the SDK that matches your technology stack - iOS, Kotlin, React Native, or Web. Quick installation via your preferred package manager (SPM, Gradle, npm). Add your access token and configure the SDK to match your needs. Display a form in your app with just a few lines of code. ## Choose Your Platform Build native iOS form experiences with Swift Create Android apps with our Kotlin SDK Cross-platform mobile forms with React Native Embed forms in any web application Build custom integrations with our REST API # JoyDoc Source: https://docs.joyfill.io/joyfill/joydoc-usage Getting started with the JoyDoc JSON standard. The JoyDoc is what we call the standardized JSON structure that gets used by all Joyfill Platform UI Components, APIs, and Exports. ## The Why The JoyDoc is the culmination of the best practices from web layout, design and interaction into the simplistic JSON data structure. We have designed the JoyDoc from our first hand experience in what it actually takes to build any kind of form or pdf solution for today's applications. This standardized JSON structure is capable of handling any data visualization, layout and collection requirement needed for digital forms or PDFs on web, desktop and mobile devices in your application or service. *** What’s Next Checkout the full JSON object and how to utilize it within Joyfill. * [Full Schema (JSON)](/joyfill/schema-json) # Metadata Source: https://docs.joyfill.io/joyfill/metadata The JoyDoc Document, File, Page, and Fields have a metadata parameter. You can use this parameter to attach arbitrary key-value data. This metadata can be used to add context, support your internal functionality, and much more. You can specify up to 50 keys, with key names up to 40 characters long and values up to 200 characters long. Metadata is useful for storing additional, structured information on an object. For example, you could store your scoring, failing values, etc. from your system on a Joyfill Field object. Your users won't see metadata unless you show it to them. Don't store any sensitive information (bank account numbers, card details, personally identifiable info(PII) about a user and so on) in metadata. *** # Reference Source: https://docs.joyfill.io/joyfill/schema-json JoyDoc property reference and descriptions # Contents * [Full JSON](/joyfill/schema-json#full-json) * [Document Properties](/joyfill/schema-json#document-properties) * [File Properties](/joyfill/schema-json#file-properties) * [Page Properties](/joyfill/schema-json#page-properties) * [Field Position Properties](/joyfill/schema-json#field-position-properties) * [Field Properties](/joyfill/schema-json#field-properties) ## Full JSON ```bash bash theme={null} Object{ //Document identifier: String, group: String, type: String, stage: String, name: String, files: Array[ Object{ //File identifier: String, name: String, styles: Object pages: Array[ Object{ //Page identifier: String, name: String, width: Number, height: Number, cols: Number, rowHeight: Number, layout: String, presentation: String, backgroundImage: String, margin: Number, padding: Number, borderWidth: Number, borderColor: String, borderStyle: String, fieldPositions: Array[ Object{ //Field Position _id: String, field: String, type: String, displayType: String, width: Number, height: Number, x: Number, y: Number, condition: String, targetValue: String, targetValueDisplayType: String, customTrueTargetDisplayValue: String, customFalseTargetDisplayValue: String, invertBooleanCondition: Boolean, rowIndex: Number, column: String, primaryDisplayOnly: Boolean, primaryMaxWidth: Number, primaryMaxHeight: Number, maxImageWidth: Number, maxImageHeight: Number, titleFontSize: Number, titleFontColor: String, titleFontStyle: String, titleFontWeight: String, titleTextAlign: String, titleTextTransform: String, titleTextDecoration: String, lineHeight: Number, fontSize: Number, fontColor: String, fontStyle: String, fontWeight: String, textAlign: String, textTransform: String, textDecoration: String, textOverflow: String, padding: Number, borderColor: String, borderRadius: Number, borderWidth: Number, backgroundColor: String, zIndex: Number, columnTitleFontSize: Number, columnTitleFontColor: String, columnTitleFontStyle: String, columnTitleFontWeight: String, columnTitleTextAlign: String, columnTitleTextTransform: String, columnTitleTextDecoration: String, columnTitleBackgroundColor: String, columnTitlePadding: Number, }, //End of Field Position ... ] //End of fields array }, //End of Page ... ], //End of pages array views: [ Object{ //View type: String, _id: String, pages: PageObject }, ... ] }, //End of File ... ], //End of files array fields: [ Object{ //Field file: String, _id: String, identifier: String, type: String, title: String, value: Mixed, matadata: Object, rowOrder: [String, String, ...], yTitle: String, yMin: Number, yMax: Number, xTitle: String, xMin: Number, xMax: Number, options: Array[ Object{ _id: String, value: String, }, ... ], //end of options tableColumnOrder: [String, ...], tableColumns: Array[ Object{ title: String, type: String, identifier: String, value: String, options: Array[ Object{ _id: String, value: String, }, ... ], //end of options array maxImageWidth: Number, maxImageHeight: Number, }, //end of column object ... ], //end of tableColumns array },//End of Field ... ] //End of fields array }//End of Document ``` ## Document Properties | Name | Type | Description | | ---------- | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | \_id | String | Must be a Joyfill internally generated ID. DO NOT manually set this value. Cannot be used with a custom value. Should be used for reference only. Example: `642529511e4ec1adeb444111` | | identifier | String | The identifier specifies an ID or some other value that helps you connect and uniquely identify the associated resource between Joyfill and your own system. Can be a custom value or if left blank the identifier will be auto generated by Joyfill. Example: `doc_642529511e4ec1adeb444111` | | group | String | Specifies the identifier of the Joyfill Group that this document is associated with. | | type | String ('document', 'template') | Specifies document category based on the functional usage. [Learn more](/docs/quick-start#key-terminology) about the usage differences between the template and document type. | | stage | String ('draft', 'published') | Specifies the current stage of completion/visibility. | | name | String | Name to visually display for the document | | files | array\_objects | Array of JoyDoc File objects | ## File Properties | Name | Type | Description | | ---------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | \_id | String | Must be a Joyfill internally generated ID. DO NOT manually set this value. Cannot be used with a custom value. Should be used for reference only. Example: `642529511e4ec1adeb444111` | | identifier | String | The identifier specifies an ID or some other value that helps you connect and uniquely identify the associated resource between Joyfill and your own system. Can be a custom value or if left blank the identifier will be auto generated by Joyfill. Example: `file_642529511e4ec1adeb444111` | | name | String | Name to visually display for the file | | styles | Object | Object of default styles to apply to all JoyDoc Fields within the JoyDoc File. See Field style properties to see what style options are available. | | pages | array\_objects | Array of JoyDoc Page objects | | views | array\_objects | Array of JoyDoc View objects | ## View Properties | Name | Type | Description | | ----- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | \_id | String | Must be a Joyfill internally generated ID. DO NOT manually set this value. Cannot be used with a custom value. Should be used for reference only. Example: `642529511e4ec1adeb444111` | | type | String ('mobile') | Specifies the intended device or experience the view is targeted at. | | pages | array\_objects | Array of JoyDoc Page objects | ## Page Properties | Name | Type | Description | | ------------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | \_id | String | Must be a Joyfill internally generated ID. DO NOT manually set this value. Cannot be used with a custom value. Should be used for reference only. Example: `642529511e4ec1adeb444111` | | identifier | String | The identifier specifies an ID or some other value that helps you connect and uniquely identify the associated resource between Joyfill and your own system. Can be a custom value or if left blank the identifier will be auto generated by Joyfill. Example: `page_642529511e4ec1adeb444111` | | name | String | Name to visually display in page list. | | width | Number | Page width in pixels. | | height | Number | Page height in pixels. | | layout | String ('grid', 'float') | Layout mode for page fields | | rowHeight | Number | Row heights for the page grid. | | cols | Number | Column count for the page grid | | presentation | String ('normal', 'transparent') | Visual display style of fields | | margin | Number | Page margin in pixels | | padding | Number | Page padding in pixels | | borderWidth | Number | Page border width in pixels | | borderColor | String | Page border color in HEX | | borderStyle | String ('solid', 'double', 'dashed') | Page border style | | fieldPosition | array\_objects | Array of JoyDoc Page Field Position objects | ## Field Position Properties | Name | Type | Description | | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | \_id | String | Must be a Joyfill internally generated ID. DO NOT manually set this value. Cannot be used with a custom value. Should be used for reference only. Example: `642529511e4ec1adeb444111` | | field | String | Specifies ID of linked Field. Must be a Joyfill internally generated ID. DO NOT manually set this value. Cannot be used with a custom value. Should be used for reference only. Example: `642529511e4ec1adeb444111` | | type | String ('text', 'textarea', 'number', 'date', 'multiSelect', 'dropdown', 'block', 'image', 'signature', 'table', 'inputGroup', 'chart') | Primary type of a field. The primary type determines core functionality for the field. | | displayType | String ('text', 'circle', 'square', 'check', 'radio', 'original', 'horizontal') | Display type of a field. Determines visual functionality for a field. | | width | Number | Number of Page columns to utilize for the field. | | height | Number | Number of Page rows to utilize for the field. | | x | Number | Horizontally position the top left corner of a field at Page column number. | | y | Number | Vertically position the top left corner of a field at Page row number. | | condition | String ('equals') | Condition to determine visual selection status of a selector displayType Field. | | targetValue | String | JoyDoc Field Option ID | | targetValueDisplayType | String ('original', 'custom') | Specifies whether or not to overwrite the default JoyDoc Field Option display text with a true/false custom text value. | | customTrueTargetDisplayValue | String | Specifies the custom display text overwrite for a true selection state. | | customFalseTargetDisplayValue | String | Specifies the custom display text overwrite for a false selection state. | | invertBooleanCondition | Boolean | Invert the conditional selection state of a selector displayType Field. | | rowIndex | Number | Specifies the linked rowIndex of an individually placed JoyDoc Field Table Cell. | | column | String | Specifies the linked JoyDoc Field Table Column ID of an individually placed JoyDoc Field Table Cell. | | primaryDisplayOnly | Boolean | Visually display only the primary part of the JoyDoc Field. | | primaryMaxWidth | Number | Max width in pixels of the primary part of the JoyDoc Field. | | primaryMaxHeight | Number | Max width in pixels of the primary part of the JoyDoc Field. | | maxImageWidth | Number | Max width in pixels of the internal image. | | maxImageHeight | Number | Max height in pixels of the internal image. | | titleFontSize | Number | Font size in pixels of JoyDoc Field title. | | titleFontColor | String | Font color in Hex of JoyDoc Field title. | | titleFontStyle | String ('normal', 'italic') | Font style of JoyDoc Field title. | | titleFontWeight | String ('normal', 'bold') | Font weight of JoyDoc Field title. | | titleTextAlign | String ('left', 'center', 'right') | Text align of JoyDoc Field title. | | titleTextTransform | String ('none', 'uppercase') | Text transform of JoyDoc Field title. | | titleTextDecoration | String ('none', 'underline') | Text decoration of JoyDoc Field title. | | lineHeight | String | Percentage to utilize for each line out of a total 100% | | fontSize | Number | Font size in pixels of JoyDoc Field element. | | fontColor | Number | Font color in Hex of JoyDoc Field element. | | fontStyle | String ('normal', 'italic') | Font style of JoyDoc Field element. | | fontWeight | String ('normal', 'bold') | Font weight of JoyDoc Field element. | | textAlign | String ('left', 'center', 'right') | Text align of JoyDoc Field element. | | textTransform | String ('none', 'uppercase') | Text transform of JoyDoc Field element. | | textDecoration | String ('none', 'underline') | Text decoration of JoyDoc Field element. | | textOverflow | String (null, 'ellipsis') | Text overflow of JoyDoc Field element. | | padding | Number | Padding in pixels of JoyDoc Field element. | | borderColor | String | Border color in Hex of JoyDoc Field element. | | borderRadius | Number | Border radius in pixels of JoyDoc Field element. | | borderWidth | Number | Border width in pixels of the JoyDoc Field element. | | backgroundColor | String | Background color in HEX of the JoyDoc Field element. | | zIndex | Number | z index position relative to other JoyDoc Fields. | | columnTitleFontSize | String | Font size of the table columns in the JoyDoc Field. | | columnTitleFontColor | String | Font color of the table columns in the JoyDoc Field. | | columnTitleFontStyle | String ('normal', 'italic') | Font style of the table columns in the JoyDoc Field. | | columnTitleFontWeight | String ('normal', 'bold') | Font weight of the table columns in the JoyDoc Field. | | columnTitleTextAlign | String ('left', 'center', 'right') | Text align of the table columns in the JoyDoc Field. | | columnTitleTextTransform | String ('none', 'uppercase') | Text transform of the table columns in the JoyDoc Field. | | columnTitleTextDecoration | String ('none', 'underline') | Text decoration of the table columns in the JoyDoc Field. | | columnTitleBackgroundColor | String | Background color in HEX of the table columns in the JoyDoc Field. | | columnTitlePadding | Number | Padding in pixels of the table columns in the JoyDoc field. | ## Field Properties | Name | Type | Description | | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | \_id | String | Must be a Joyfill internally generated ID. DO NOT manually set this value. Cannot be used with a custom value. Should be used for reference only. Example: `642529511e4ec1adeb444111` | | file | String | Must be a Joyfill internally generated ID. DO NOT manually set this value. Cannot be used with a custom value. Should be used for reference only. Example: `642529511e4ec1adeb444111` | | identifier | String | The identifier specifies an ID or some other value that helps you connect and uniquely identify the associated resource between Joyfill and your own system. Can be a custom value or if left blank the identifier will be auto generated by Joyfill. Primarily used for data mapping in the API. Example: `field_642529511e4ec1adeb444111` | | type | String ('text', 'textarea', 'number', 'date', 'multiSelect', 'dropdown', 'block', 'image', 'signature', 'table', 'inputGroup', 'chart') | Primary type of a field. The primary type determines core functionality for the field. | | title | String | Field title. | | value | Mixed | Field value | | rowOrder | array\_strings | Array of JoyDoc of Row IDs inside Table Field value property. | | options | array\_objects | Array of JoyDoc Field Option objects. | | tableColumns | array\_objects | Array of JoyDoc Field Table Column objects. | | tableColumnOrder | array\_strings | Array of JoyDoc Field Table Column IDs. | | metadata | Object | Object | | yTitle | String | Vertical title for JoyDoc Fields with type 'chart'. | | yMax | Number | Vertical max value for JoyDoc Fields with type 'chart'. | | yMin | Number | Vertical min value for JoyDoc Fields with type 'chart'. | | xTitle | String | Horizontal title for JoyDoc Fields with type 'chart'. | | xMax | Number | Horizontal max value for JoyDoc Fields with type 'chart'. | | xMin | Number | Horizontal min value for JoyDoc Fields with type 'chart'. | ## Summary ### Important Note: You're not limited to or required to follow this data structure internally. You can store your data any way you like in your internal system. *** # Self-hosting Source: https://docs.joyfill.io/joyfill/self-hosted How to utilize Joyfill with your own internal systems and data. # Overview There are two primary integration paths when working with the Joyfill Platform: Joyfill Managed Data and Self Hosted Data. If you are wondering which type of integration path is right for your use case please feel free to reach out on the [site](https://joyfill.io/developers/) via our chat and a customer success team member can setup a call with our engineering and platform specialists. # Self Hosting Data Joyfill enables you to utilize your own data with most of the Joyfill Platform solutions without us retaining any of that information in our own system. ## Available Platform Solutions | Solution | Description | | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Web and Mobile Components** | Joyfill Web and Mobile Components can be used within your own applications without the need to interact with any Joyfill APIs or Systems. You can utilize all the power of the Joyfill Builder, Filler, and Previewer with your own internal data. | | **Exports** | Joyfill's Export solution can generate exports, populated PDFs, and much more using only a properly formatted JSON payload. Joyfill doesn't need to store or retain this information to generate these reports. Simply send us a JSON payload and we can send back the PDF export. | ## Reasons to self-host * **Compliance Requirements** - This integration path can help accomplish compliance requirements you may have within your organization and application. * **Pre-existing/Legacy System** - This integration path can be utilized to enhance a pre-existing solution or product. With a pre-existing solution it's likely that you already have users, stored data, and much more within your system and have no interested in moving that to a third party integration partner. The self-hosted route allows you to retain all your pre-existing data internally but still utilize core Joyfill Components. *** # Security Source: https://docs.joyfill.io/kotlin/about/security # Overview Joyfill follows industry best-practices to keep your data safe: You can only access the Joyfill service via TLS (https). When you submit data to for storage or to generate a PDF, this data is encrypted at rest using AES-256. All stored files are encrypted at rest, using the AWS Key Management Service. This includes template PDFs, generated PDFs, and any other files that are stored in Amazon S3. Passwords are salted and hashed with high level expansion rounds. We do not store plaintext passwords in our database. We subscribe to security mailing lists and patch any vulnerabilities as soon as possible. # Compliance (HITRUST, HIPAA, SOC 2, PCI DSS, etc.) To maintain compliance we offer a self-hosting option. Our self-hosting option allows you to retain and manage all template, submission, file and any other data directly in your own system. [See Self-Hosting](/joyfill/self-hosted) or reach out to our team directly via live chat at [https://joyfill.io](https://joyfill.io) # Vulnerability Disclosures Joyfill welcomes vulnerability disclosures. Please send an email to [contact@joyfill.io](mailto:contact@joyfill.io) to report any security vulnerabilties. # Questions Reach out directly to our team using our live chat at [https://joyfill.io](https://joyfill.io) *** # Kotlin API Reference Source: https://docs.joyfill.io/kotlin/api-reference/kotlin-docs Complete API documentation for the Joyfill Kotlin SDK # Migration guide Source: https://docs.joyfill.io/kotlin/changelogs/migration-guide # Migration guide for v1.x.x to v2.x.x This guide explains how to migrate your apps after the v2 release’s package and artifact restructure. It covers three paths: staying on v1 via legacy modules, migrating from v1 to v2, and moving from v2-beta to the final v2. For feature-level differences and what’s new, see: [What’s new in 2.0.0](./v2-changes.md) *** ## What changed at a glance * Dependencies: use standard `io.joyfill:*` artifacts for v2, or `io.joyfill:legacy-*` to remain on v1 ([details](#1-migration-v1--v2), [stay on v1](#0-option-stay-on-v1-legacy-modules)) * Package names: v2-beta code under `joyfill2` moved to `joyfill` in v2 — rename imports from `joyfill2` → `joyfill` ([details](#2-migration-v2-beta--v2)) * Event API: legacy `FieldEvent` has been replaced by `ComponentEvent.*` — use `ComponentEvent.FieldEvent` (standalone fields) and `ComponentEvent.CellEvent` (table cells) ([details](#3-migration-fieldevent--componentevent-events)) * Behavior notes: most imports remain identical for v1→v2 because the package is still `joyfill`; some v1 UI components may not exist in v2 — check v2 docs for alternatives ([details](#1-migration-v1--v2)) *** ### Dependencies changes * Replace legacy artifacts with the standard ones: * io.joyfill:legacy-compose → io.joyfill:compose * io.joyfill:legacy-models → io.joyfill:models * io.joyfill:legacy-builder → io.joyfill:builder * io.joyfill:legacy-api → io.joyfill:api Gradle example (Kotlin DSL) ```kotlin theme={null} // BEFORE (v1) dependencies { implementation("io.joyfill:legacy-compose:") implementation("io.joyfill:legacy-models:") implementation("io.joyfill:legacy-builder:") implementation("io.joyfill:legacy-api:") } // AFTER (v2) dependencies { implementation("io.joyfill:compose:") implementation("io.joyfill:models:") implementation("io.joyfill:builder:") implementation("io.joyfill:api:") } ``` Imports and usage * Keep using `joyfill.*` imports. * Example usage: ```kotlin theme={null} Form( editor = rememberEditor(document = doc), mode = Mode.fill, ) ``` *** ### FieldEvent → ComponentEvent.\* (events) Summary * v1 used a single `FieldEvent` for all field callbacks. * v2 unifies events under `ComponentEvent.*` with two concrete types: * `ComponentEvent.FieldEvent` for standalone fields (text, number, image, signature, etc.) * `ComponentEvent.CellEvent` for table cells (row-aware events) * Handler parameter types changed accordingly in `Form`/components. Why this changed * The v2 architecture standardizes how components emit and consume events and makes table events first-class by including row/column context. API mapping * v1 (legacy): `joyfill.FieldEvent` * v2: `joyfill.ComponentEvent.FieldEvent` and `joyfill.ComponentEvent.CellEvent` Key properties mapping * Common in both versions: * `fieldId`, `fieldIdentifier`, `pageId`, `id` (document id), `identifier` (document identifier), `fileId`, `fieldPositionId` * v2 additions/notes: * `source`: the typed editor instance (e.g., `ImageEditor`, `TextEditor`) * `multi`: available when the underlying component is a file component (e.g., image with multi-upload) * Cell-only: `rowIds`, `columnId`, `schemaId`, `parentPath` Before → After examples 1. General field handlers (change/focus/blur) * v1 (legacy Form): ```kotlin theme={null} Form( // ... onFieldChange = { e: FieldEvent -> /* use e.fieldId, e.pageId, ... */ }, onFocus = { e: FieldEvent -> /* ... */ }, onBlur = { e: FieldEvent -> /* ... */ }, ) ``` * v2 (Form/components): ```kotlin theme={null} Form( // ... onFieldChange = { e: ComponentEvent<*> -> when (e) { is ComponentEvent.FieldEvent<*> -> { /* standalone field */ } is ComponentEvent.CellEvent<*> -> { /* table cell */ } } }, onFocus = { e: ComponentEvent<*> -> /* same pattern as above */ }, onBlur = { e: ComponentEvent<*> -> /* same pattern as above */ }, ) ``` 2. File upload/capture handlers (image/signature/barcode) * v1: ```kotlin theme={null} Form( onUpload = { e: FieldEvent -> listOf("https://.../file1") }, onCapture = { e: FieldEvent -> "barcode-or-text" }, ) ``` * v2: ```kotlin theme={null} Form( onUpload = { e: ComponentEvent -> // Optional: narrow by editor type when (val src = e.source) { is joyfill.editors.image.ImageEditor -> listOf("https://.../img1") is joyfill.editors.signature.SignatureEditor -> listOf("https://.../sig1") else -> emptyList() } }, onCapture = { e: ComponentEvent -> // Example for barcode/text-capable components when (e.source) { is joyfill.editors.barcode.BarcodeEditor -> "scanned-barcode" else -> null } }, ) ``` 3. Table cell events * v2 introduces `CellEvent` for row-aware callbacks used by table components and their editors: ```kotlin theme={null} val handler: (ComponentEvent.CellEvent<*>) -> Unit = { e -> val rowIds = e.rowIds // one or more row ids affected val columnId = e.columnId val fieldId = e.fieldId // handle per-row updates, analytics, etc. } ``` Minimal refactor recipe * Replace callback parameter types: * `FieldEvent` → `ComponentEvent<*>` when used at the Form level * For table-specific handlers: prefer `ComponentEvent.CellEvent<*>` * For single-field components: prefer `ComponentEvent.FieldEvent<*>` * Update `when` branches to handle both `FieldEvent` and `CellEvent` where applicable. * If you previously relied on `attachments` in events: v2 surfaces file uploads through file editors and returns should come from your handler; `attachments` on events is not required in typical flows. Runtime checks and generics * `ComponentEvent` is generic on the editor type (`E : ComponentEditor`). When you need editor-specific behavior, check `e.source` with `is ImageEditor` etc. References * v2 event system overview: [architecture/v2/event-system.md](./architecture/v2/event-system.md) *** ## FAQ Q: Why do both v1 and v2 have `joyfill.Form`? A: v1 code was moved into `legacy-*` modules but retained the exact same package names for easier migration. Which `Form` you get depends on whether your dependency is from `legacy-*` or from the standard modules. Q: How do I verify I’m on v2? * Ensure you’re using the standard artifacts (no `legacy-` prefix). * Confirm your imports are `joyfill.*` and that you do NOT have `joyfill2.*` anywhere. Q: Any behavioral differences? * Some components may be missing in v2 compared to v1, but v2 offers many new features and improved architecture. See the v2 docs for details. ## Related Documentation * Architecture Overview: [architecture/README.md](./architecture/README.md) * V2 Overview: [architecture/v2/README.md](./architecture/v2/README.md) * V2 UI Components: [architecture/v2/ui-components.md](./architecture/v2/ui-components.md) * V2 Editor Pattern: [architecture/v2/editor-pattern.md](./architecture/v2/editor-pattern.md) * V2 API Integration: [architecture/v2/api-integration.md](./architecture/v2/api-integration.md) *** ## Important context * Prior to v2 release, we had: * v1 under the `joyfill` package * v2-beta under the `joyfill2` package * With the v2 release: * v2 now lives under the `joyfill` package * Legacy v1 classes were moved into separate legacy modules, but kept the same `joyfill` package names for source compatibility * Practically, both legacy and v2 expose symbols like `joyfill.Form`. Which one you use depends on the dependency you add. What this means for you * V1 → V2: swap your dependencies from legacy modules to v2 modules. Most imports remain identical because the package is still `joyfill`. * V2-beta → V2: rename your imports from `joyfill2` to `joyfill`. No other code changes should be required. *** ## Stay on v1 (Legacy Modules) > \[!CAUTION] > This is not RECOMMENDED. This is a temporary path for those who want to continue using v1 for a short period of time. Summary * If you want to continue using the legacy v1 implementation for a period, switch your dependencies to the `legacy-*` artifacts. * Imports remain `joyfill.*` because legacy modules preserve the same package names. Dependencies * Replace standard artifacts with legacy ones if you were previously on the standard artifacts but wish to remain on v1: * `io.joyfill:compose` → `io.joyfill:legacy-compose` * `io.joyfill:models` → `io.joyfill:legacy-models` * `io.joyfill:builder` → `io.joyfill:legacy-builder` * `io.joyfill:api` → `io.joyfill:legacy-api` Gradle example (Kotlin DSL) ```kotlin theme={null} // BEFORE (standard artifacts) dependencies { implementation("io.joyfill:compose:") implementation("io.joyfill:models:") implementation("io.joyfill:builder:") implementation("io.joyfill:api:") } // AFTER (stay on v1 legacy) dependencies { implementation("io.joyfill:legacy-compose:") implementation("io.joyfill:legacy-models:") implementation("io.joyfill:legacy-builder:") implementation("io.joyfill:legacy-api:") } ``` Verification * Ensure your imports remain `joyfill.*` and not `joyfill2.*`. * Confirm you’re pulling `legacy-*` artifacts in your dependency tree. Notes * This path is intended for temporary continuity. Plan to migrate to v2 soon. # Release Notes Source: https://docs.joyfill.io/kotlin/changelogs/releases Complete changelog of Joyfill Kotlin Multiplatform SDK releases ## 2.0.0-RC27 CHANGED * JSON Schema — synced bundled `joyfill-schema` with upstream: adds `columnTitleTextOverflow`, `RequiredLogic`, and per-cell (`schema`/`column`) condition shape ## 2.0.0-RC26 CHANGED * Field Labels — decorators that cannot fit beside the title now move to their own line beneath it, so a crowded label can be two lines tall FIXED * Field Decorators — a long field title no longer pushes decorator chips off screen * Field Titles — a label crowded with decorators no longer collapses its title to one character per line * Decorator Live Updates — a decorator added while a row form is open now appears immediately, instead of only after scrolling the row out of view and back ## 2.0.0-RC25 ADDED * Cell Visibility Logic — table and collection cells can now be hidden or shown per row via conditional logic, including conditions that mix same-row and document-level references * Field Editability — new schema-level `editability` property on table and collection fields, controlling whether they can be edited through the row form only or through both the row form and the tabular view * Finder — new bulk accessor that returns every cell in a row, including hidden ones CHANGED * Web Targets — removed the JS/Wasm targets from `component-joyfill` (**breaking**), and enabled Gradle dependency scanning * Required Logic — renamed the `requiredLogic` action wire value `"unforce"` to `"unenforce"` (**breaking**) FIXED * Mobile Flow Layout — conditionally-hidden fields now collapse instead of leaving blank space * Grid Columns — `cellsHidden` columns with no `cellVisibilityLogic` are no longer rendered * Cell Decorators — decorators added to cells with no existing entry now persist after save ## 2.0.0-RC24 CHANGED * Performance — stabilization and optimization work across validation, formulas, conditional logic, and row deletion FIXED * Date & Signature Fields — layout issues in landscape orientation * Conditional Logic — regression when duplicating pages * Formulas — additional correctness fixes uncovered during stabilization ## 2.0.0-RC23 CHANGED * Public API — grouped related `Form` / `DocumentEditor` parameters into focused config objects (following the `DecoratorConfig` pattern), aligned parameter names with the iOS SDK, and consolidated everything under a single swappable config instance * Formula Engine — stabilized formula behavior across Android, iOS, and Web SDKs to ensure consistent results on all platforms ## 2.0.0-RC22 CHANGED * Delete Page Dialog — simplified the confirmation message to "Are you sure you want to delete this page?" to match iOS * Date Columns — removed automatic local time zone assignment to row data when date columns are present, matching iOS behavior * Row Creation Change Logs — rows added while a filter is active now report the injected filter values instead of column defaults * Filter Dialogs — improved IME handling (eliminated UI jitter while typing) and rendering performance for large filter lists * Multi-Select — refined option spacing and reduced the selected-state corner radius * Table Preview — improved layout, cell/icon alignment, and divider styling FIXED * Collection Field Validation — nested child table invalidity is now surfaced in the validation output (`RowValidity` / `ComponentValidity`), so consumers can navigate to the failing nested field via `goto(rowId, columnId)` * Page Duplication — fixed an out-of-memory crash when duplicating a page with \~11k rows multiple times * Date Picker — dismissing or clearing the date picker now correctly fires an `onBlur` event * Read-Only Image Field — focus state no longer updates on read-only image fields * Table Close — focus is now cleared before the table closes, preventing the cell from being blurred after the table * Image Dialog — fixed mismatched delete/upload button heights by aligning their padding ## 2.0.0-RC21 FIXED * Page hidden state regression — pages were incorrectly losing their hidden state ## 2.0.0-RC20 ADDED * JSON Schema — added decorator definitions to the validation schema CHANGED * Collection Field — performance optimization FIXED * `editor.goto` with `focus=true` not focusing cells on table and collection fields ## 2.0.0-RC19 CHANGED * Row Delete Payload — `rowDelete` change logs now include the deleted row data alongside the existing params * Bulk Edit Change Logs — bulk edits now emit a single change event containing a list of change logs instead of one event per individual change * Performance — general performance improvements, including a fix for an out-of-memory crash. FIXED * Signature Field Readonly — empty signature fields can no longer be opened or edited when the form/field is in readonly mode * Row Decorator Column — now appears whenever decorator data exists, even when the `decorate` flag is missing ## 2.0.0-RC18 ADDED * Decorator Icons — added support for two new decorator icons: `pencil` and `pen-to-square` CHANGED * Row Decorator Column — improved the UI of inline decorators with horizontal scroll, fixed curvature bugs, and optimized performance * Chart Blank Coordinates — blank x/y values on chart points are now saved as `null` and displayed as empty in the UI, instead of being coerced to `0`; schema bumped to 1.0.1 to allow nullable coordinates FIXED * Decorators Visibility & Interaction — decorators are now visible and interactive on readonly forms * Duplicate Page Without Values — duplicating a page without values now keeps read-only and display-text field values intact ## 2.0.0-RC17 ADDED — Added support for row and cell-specific decorators in table and collection fields, accessible via scoped path targeting. ## 2.0.0-RC16 ADDED * `editor.validate(path)` — validates a specific page, field, row, or cell by path * phases: page → field → row → cell, each level narrows the scope * Dynamic decorator manipulation — `editor.decorators` now supports adding, updating, and removing decorators on fields, rows, and cells via slash-separated paths: ```kotlin theme={null} // path formats: "pageId/fieldPositionId" | "pageId/fieldPositionId/rowId" | "pageId/fieldPositionId/rowId/columnId" val path = "$pageId/$fieldPositionId/$rowId/$columnId" val decorator = Decorator().apply { icon = "comment" label = "Review" color = "#3B82F6" action = "open_review" } // Add documentEditor.decorator.add(path, decorator) // Read val decorators = documentEditor.decorator.list(path) // Update (matched by action) decorator.label = "Open Review" documentEditor.decorator.update(path, action = "open_review", decorator = decorator) // Remove documentEditor.decorator.remove(path, action = "open_review") ``` FIXED * Date column filter UI now respects the column's configured format (e.g., time-only columns show only time in the filter UI instead of the full date+time) * `onChange` and `onFieldChange` now fire correctly for: * clearing date cells * page focus/blur events * row actions (delete/move up/move down/insert below) * `onFocus` now fires correctly for: * dropdown field and cell * date cell (on open/clear) * signature cell * `onBlur` now fires correctly for: * dropdown field and cell * multi-select cell * signature cell * Table row form is now visible when a column has a long title ## 2.0.0-RC15 ADDED * Date column filtering support in table and collection fields * `reason` property on the validation result returned by `field.validate()` — a list of strings describing why the field is invalid (empty list when valid) * Per-page action controls — page objects now support configuration properties that allow disabling deletion and/or duplication on a per-page basis; pages with disabled actions hide the corresponding buttons in the paginator UI FIXED * `onFieldChange` not being emitted when `inlineFields` mode was enabled for signature and image fields * `onFieldChange` not being emitted when adding a new row to a table or collection field * Row decorator `onChange` event now includes the `parentPath` property as expected * Form flickering when the keyboard is open and the user is typing inside a cell of a table or collection field ## 2.0.0-RC14 ADDED * Decorators: integrators can attach icons or text to fields or rows; tapping them fires a custom event to the host application, enabling custom UI actions * `inlineFields` parameter on the `Form` composable: when `true`, openable fields (table, collection, image, signature, chart) expand inline within the existing view instead of opening a modal or new screen, giving the host application full control of the UI layer * Single field validation via `field.validate()` CHANGED * Changelog entries now include a `type` property (mirrors `target`, e.g. `"field.update"`) for consistency with the web SDK FIXED * `goto()` now correctly returns a failure result when a valid row ID is provided but the specified `columnId` does not exist * Multi-select field options with long text are now displayed in full instead of being truncated * `DateField` clearing now correctly fires `onFieldChange` from the form in addition to `onChange` from the `DocumentEditor` * Column order in field validation results for table and collection fields now matches the defined column order ## 2.0.0-RC13 ADDED * Required field validation output now includes rows and cells for validation issues * Table/collection validation now follows proper hierarchy — validation depends on row validity, not only whether the field is empty or has no rows * External Navigation Pt.3 — cell navigation and focus highlighting: `goto` supports `columnId` for row-form cell targeting and `focus: true` for auto-focus with highlighted border styles * Conditional logic on columns — show and hide table/collection columns based on other field values (same pattern as existing conditional logic) ## 2.0.0-RC12 ADDED * Row metadata support to link rows to pages, fields, or other rows * Page navigation events (`page.focus`, `page.blur`) * Row-level navigation via `goto(pageId/fieldPositionId/rowId, { open: true })` FIXED * Formula references are now correctly updated when duplicating pages * Improved stability when navigating between rows ## 2.0.0-RC11 ADDED * External navigation API — added `goto("pageId/fieldPositionId")` function to externally control which page, field is displayed * Page deletion confirmation dialog — when page deletion is enabled, a confirmation dialog is shown before deletion * Page deletion and duplication configuration options — both features can now be enabled/disabled via configuration CHANGED * UI improvements for better user experience FIXED * Fields without associated fieldPosition are now filtered out from validation output * Crash scenario with conditional logic when duplicating a page ## 2.0.0-RC10 ADDED * (JF-243) Support for Page Deletion * Enhanced Page Duplication UI * Single Click Option to Open Row Form — when enabled, users can open the row form with a single click instead of multiple clicks * Loaders while filtering for better UX feedback * Bulk row actions while filtering — users can now select all rows, bulk edit, or delete all filtered rows * Additional tests for formulas and various components to ensure stability CHANGED * Exposed `ErrorFormState` composable as public API (was internal) * Refactored hidden state — hidden state will no longer be updated in the JSON when a field changes visibility or a page is modified * Filter optimization for improved performance FIXED * (JF-230) Duplicated pages not appearing in the correct position and not being saved — duplicated page now inserts directly below the source page * Formulas not working after duplicating a page * Conditional logic not working after duplicating a page * Duplicating Table and Collection fields caused row IDs to be shared across pages, leading to unintended row deletions * Page duplication for Chart field not working due to duplicated point IDs * Chart data not being saved after modifications * Crash when duplicating a page * Multi-select validation incorrectly showing as valid when all visible options are unselected but a deleted option remains selected * Multi-select column colors not being acknowledged in Table and Collection fields ## 2.0.0-RC9 ADDED * Row insertion when filter and sort is active in Table and Collection fields * "Add after" option in row actions dropdown when filter is active CHANGED * Performance improvement on Collection field row actions FIXED * Multi select options being clickable in readonly mode * Internal functions incorrectly exposed as public API * Keyboard not appearing after bulk edit * Parent path calculation corrupted by row action when editing child rows * Row form submit button showing "Submitted" state after reopening following bulk edit * "Add Row" button not disabled in readonly mode for Table fields * Filtered table allowing navigation to hidden rows in row form * Duplicate page creating image field attachments with same ID, causing unintended deletion across pages ## 2.0.0-RC8 ADDED * Row navigation now keeps the target row in view when closing the row form in Collection and Table fields. ## 2.0.0-RC7 ADDED * Row highlighting for selected rows in Table and Collection fields to improve visibility for users CHANGED * Support for additional custom time formats * Required field validation — added page ID to the validation status object for more accurate field tracking FIXED * Crash when adding rows in Table fields caused by invalid default values in columns such as Text, Signature, Date, or Block * Edge case involving Collection fields and formula calculations * Multiselect fields not working when a deleted option shared the same value as a non-deleted option * Formula fields failing to process decimal numbers without a leading zero ## 2.0.0-RC6 FIXED * Collection field not showing even when the License key has been provided ## 2.0.0-RC5 ADDED * onFocus event when table field is opened * onBlur event when table field is closed * onFocus event when collection field is opened * onBlur event when collection field is closed * onBlur event for multi select field * onFocus event for multi select field * Removal of date value from fields CHANGED * Performance improvement on table when bulk selecting * Performance improvement on table when scrolling * Signature field undo button to always show * Tables without rows should not be selectable * Submit button when signature field has not be entered or updated FIXED * Hidden rows being selected * Table without rows not being selectable or clickable * Image Scaling issue * Move down should be disabled if the selected row is last * Move up should be disabled if the selected row is first * Crash if empty Json is passed into the Form * Table Column ordering ## 2.0.0-RC4 FIXED * OnFieldChange get triggered even when no image has been captured in onUpload * Other misc bug fixes not reported by ST ## 2.0.0-RC3 FIXED * Crashing when a multi select field has been selected * Crashing with a class cast exception when dealing with dates ## 2.0.0-RC2 FIXED * Images uploaded on Android are now visible on all other platforms * Images uploaded on all other platforms are now visible on Android ## 2.0.0-RC1 ADDED * (JF-195) Additional params added for upload * (JF-199) License Key Support for collections FIXED * (JF-193) Schema validation error after image upload ## 2.0.0-Beta20 CHANGED * Scroll performance on TableField * Scroll performance on Collection Field * Filtering performance * Validation performance * Template loading performance FIXED * Conditional Logic fixes in collection field * (JF-136) Performance support for 1000 rows * (JF-148) Drawing signature out of bounds fixed * (JF-109) Fix for different ordering of items ## 2.0.0-Beta19 ADDED * (JF-158) Implemented search and filter on table field CHANGED * Table field performance enhancements for 1000 total rows * (JF-161) Collection field performance enhancements for 1000 total rows * Data validation and error reporting enhancements * UI enhancement for fields with really long titles * Enhanced performance for bulk edit FIXED * (JF-109) Field ordering consistency * (JF-152) Hidden fields are being displayed * (JF-154) Making subsequent sort choices causes sort to reset ## 2.0.0-Beta17 ADDED * (JF-20) Data validation error handling and validateSchema helper * (JF-22) OnChange handler with live UI refresh * (JF-94) Detailed changelogs for collection and table fields * (JF-39) Calculated Fields * (JF-151) onCapture parameter is missing from RawBarcodeField ### Known Issues * Uploading large images (\~30Mb) causes an Out of Memory Error on Android 14 * Validation Schema treats a Joydoc without the files property as valid (it shouldn't) ## 2.0.0-Beta15 ADDED * (JF-20) Data validation error handling and validateSchema helper * (JF-22) OnChange handler with live UI refresh * (JF-94) Detailed changelogs for onChange handler * (JF-39) Calculated Fields Beta * (JF-151) onCapture parameter for RawBarcodeField ## 2.0.0-Beta13 ADDED * Collection Field Search And Filter (ST:M1.2) CHANGED * Performance improvements in the collection field FIXED * Crash when adding row to the table in 'NFPA 10 -2022' form (JF-103) ## 2.0.0-Beta09 ADDED * JF-90: Added support for single image fields * Changelog emissions for collection fields * Descriptive error message when image fields fails to load the url * ImageReplacementSample composable to demonstrate how to replace an image in the ImageField CHANGED * Improved the size of the required asterisk on required fields (it was too small) * Compose Multiplatform: 1.7.3 -> 1.8.1 FIXED * JF-35: UI now updates when the underlying image url changes * JF-84: Field validation was still failing when a new table row was added without fields * JF-87: Some table fields are still editable when the form is in read-only mode * Changelog emission for non collection fields ## 2.0.0-Beta08 FIXED * Block Field with negative padding crashing the app * Validation always returning FieldsInvalid (again) ## 1.1.14 FIXED * Empty spaces when there are soo many hidden fields (apparently every single hidden field took 8dpi height) * Last updated TextArea not persisting * Last updated NumberField not persisting ## 1.1.13 CHANGED * Kotlin from 2.0.20 to 2.1.10 * Compose Multiplatform from 1.7.0 to 1.7.3 FIXED * Conditional Logic not resolving on a single input * Date Field Capturing wrong time * Last text field entry not updating accordingly ## 1.1.12 ADDED * Table field: Sort a specific column by either ascending or descending order * Table field: Filter text columns by a certain search phrase * Table field: Filter multiple cell values from different columns at once * Table field: Duplicate multiple selected rows * Table field: Edit multiple selected rows at once CHANGED * Kotlin version from 2.0.10 to 2.0.21 * Compose multiplatform version from 1.6.11 to 1.7.0 ## 1.1.11 FIXED * Conditional logic: multiselect fields would occasionally select deleted values * Conditional logic: Dropdown fields would not trigger for fields with multiple values * Conditional logic: Less than operation not triggering same time as greater operation * Conditional logic: is (and in turn is not) check was performing only case sensitive matches * Conditional logic: empty check was not triggering on null values * Conditional logic: wrong is filled notation was been used ## 1.1.7 ADDED * Chart preview * Chart full view * Chart coordinate editing * Chart Line colors ## 1.1.6 FIXED * Number field crashing on invalid values ## 1.1.5 FIXED * Conditional logic for choice fields (multiselect and dropdown) now fires correctly when conditions are not met * Mobile View regression * Validation now only validates fields in the required view ## 1.1.4 ADDED * Field tooltips are now supported * Table row duplication * Validation Plugin with field validation support * Validation Plugin with form validation support * Validation Plugin with document validation support FIXED * Conditional logic for choice fields now works as expected * Fixed multiselect field showing deleted options ## 1.1.2 FIXED * Default values on conditional logic are now working as expected ## 1.1.1 FIXED * Conditional Logic now works as expected ## 1.1.0 ADDED * Conditional Logic Support FIXED * Fixed crashing issues with the image field when it is open with an invalid value ## 1.0.6 CHANGED * Capitalise button on the keyboard is not set by default when setting the cursor in text fields and text columns * Made the asterisk (stars) on required fields red FIXED * Required text fields would not display the asterisk * Add Row button is no longer display on readonly forms * Add Row button is no longer displayed on readonly tables (even when the form is in fill mode) * Tables with long names now have a visible close button * Radio buttons are now de-selectable * Undo-Redo on signature fields is hidden when the user types their signature out * Dropdown field now saves on choice (instead of saving after onBlur event) * Tables now preview uniformly even when there are few rows to preview * Number input fields no longer adds an extra zero while deleting ## 1.0.5 ADDED * rgb color support on rich text field * text alignment support for rich text field * calendar icon on DateField CHANGED * Enhanced all field labels to be semibold FIXED * Fixed font size issue on block field * Fixed crashing when pageOrder is null * Fixed crashing when default date has decimal values ## 0.1.0-RC1 ADDED * Initial release of Joyfill Kotlin Multiplatform SDK * Supported Fields: Text, TextArea, Number, DropDown, MultiSelect, Signature, DateTime, Image, Table * Supported Callbacks: onFocus, onBlur, onChange, onFieldChange, onUpload * External page Navigation * Internal page Navigation * Deleted fields support * Readonly fields support * Platform support: Android, Linux (through JVM), Windows (through JVM), MacOs (through JVM) # Getting started Source: https://docs.joyfill.io/kotlin/getting-started This guide helps you integrate Joyfill Android SDK into either a new or existing Android project. It shows you how to install the SDK and display your first form using Jetpack Compose. ## **Install Joyfill SDK** **Creating or opening your project** If you already have an Android project, open it in Android Studio. Otherwise, create a new one by following the steps below. 1. Open Android Studio, select **File** > **New** > **New Project**. 2. Choose the **Empty Activity** (Jetpack Compose) template. 3. Set your app name (for example, Joyfill Demo), desired save location, language (Kotlin), and minimum SDK (21+ recommended). 4. Click **Finish** to create the project. **Adding Joyfill to your project** In your `app/build.gradle.kts` file, add the Joyfill dependency: ```kotlin theme={null} dependencies { implementation("io.joyfill:compose:") } ``` Check `versions` for the latest version. > ⚠️ See migration notes in `migration-guide` if you are upgrading from v1 **Sync your project** After adding the dependency, sync your project by clicking the **Sync Now** button that appears at the top of the file, or by selecting **File** > **Sync Project with Gradle Files**. ## **Load your first form** **Download the sample JSON** Download the sample JSON to get started without any API calls: **`first-form.json`** This JSON file contains a simple form with: * A text field for "Full Name" * A number field for "Age" * A multi-select field for "Do you like football?" ### **Provide JSON** After downloading `first-form.json`, you have two ways to use it in your app: **Method 1: Copy JSON content directly** Open the downloaded file, copy its entire content, and paste it into your code: ```kotlin theme={null} val json = """ { "_id": "68d101ee8f5793b7cd6031fc", "type": "template", "name": "New Template", // ... rest of your JSON content from first-form.json ... } """.trimIndent() ``` **Method 2: Load from assets (recommended)** 1. Place first-form.json in your `app/src/main/assets/` folder 2. Load it in your code: ```kotlin theme={null} @Composable fun loadJsonFromAssets(context: Context, fileName: String): String { return remember(fileName) { context.assets.open(fileName).bufferedReader().use { it.readText() } } } // Usage val context = LocalContext.current val json = loadJsonFromAssets(context, "first-form.json") ``` **Create your DocumentEditor** Once you have the JSON, create a DocumentEditor: ```kotlin theme={null} val editor = rememberDocumentEditor(json = json) ``` **Display the Form** Add the Form to your UI: ```kotlin theme={null} Form(editor = editor) ``` ## **Listen for form changes** Track user interactions and form modifications using event callbacks: **Basic change tracking** ```kotlin theme={null} val editor = rememberDocumentEditor( json = json, onChange = { changeEvent -> println("Form changed! ${changeEvent.changelogs.size} updates") // Access the updated document val updatedDocument = changeEvent.document // Process each change changeEvent.changelogs.forEach { change -> println("Field '${change.fieldId}' was modified") } } ) ``` **Field-level events** You can also listen for specific field interactions: ```kotlin theme={null} Form( editor = editor, onFieldChange = { event -> println("Field changed: ${event.fieldId}") }, onFocus = { event -> println("Field focused: ${event.fieldId}") }, onBlur = { event -> println("Field lost focus: ${event.fieldId}") } ) ``` >  💡 **Learn more**: For detailed information about all available events and their parameters, see the [Form Events Documentation](https://www.notion.so/Handling-Form-Events-28bdef37c9a0802883a9d0497181682f?pvs=21). # Decorators Source: https://docs.joyfill.io/kotlin/guides/decorators Decorators are tappable indicators (icon + label) attached to a **field**, **column**, **row**, or **cell**. Taps are delivered through **`onFocus`** so your app can run custom logic — navigation, uploads, etc. ## Decorator model ```kotlin theme={null} val decorator = Decorator().apply { action = "openHelp" // optional in Kotlin, unique within scope when set icon = "circle-info" // optional, see Supported icons label = "Help" // optional color = "#3B82F6" // optional, must be #RRGGBB } ``` | Property | Type | Description | | -------- | --------- | ------------------------------------------------------- | | `action` | `String?` | Non-empty and unique among decorators at the same path. | | `icon` | `String?` | Icon name (see [Supported icons](#supported-icons)). | | `label` | `String?` | Text shown next to or instead of the icon. | | `color` | `String?` | 6-digit hex color: `#RRGGBB` (e.g. `#3B82F6`). | The control is shown only when there is a non-empty **`icon`** or **`label`** (`isDisplayable`). Action-only entries are stored but not displayed. ## Constructing a path `DocumentEditor` resolves decorators using a slash-separated path. Every path starts with `pageId/fieldPositionId`. What you append after that determines what gets decorated. > **Reserved keywords.** The path grammar uses three reserved tokens — **`schemas`**, **`rows`**, and **`columns`**. Anything else in a path slot is treated as an id (page id, field-position id, row id, column id, or schema key). Don't use these keywords as ids. ### Field decorators Just the two ids. Applies to the field's header. ```kotlin theme={null} val fieldPath = "$pageId/$fieldPositionId" ``` ### Table — `/rows`, `/columns/colId`, or specific `rowId` / `rowId/colId` A table has four decorator scopes, two **common** (defaults applied everywhere) and two **specific** (overrides for one row or cell): | What you want | Append | Example | | ----------------------------------------------- | ---------------- | -------------------------------- | | Common decorators on **every row** | `/rows` | `pageId/fpId/rows` | | Decorators on **one specific row** | `/rowId` | `pageId/fpId/row_42` | | Common decorators on **every cell in a column** | `/columns/colId` | `pageId/fpId/columns/col_status` | | Decorators on **one specific cell** | `/rowId/colId` | `pageId/fpId/row_42/col_status` | Specific paths inherit from the matching common path on the first write — anything you set on `/rows` shows on `row_42` until you write to `row_42` directly. ### Collection — same as table, plus `/schemas/schemaKey/…` for nested rows A collection's **root** rows behave like a table — the four scopes above use the exact same path shapes. Take a "People" collection where each person row holds a nested "Addresses" schema: ``` schema "people" (root, children: [addresses]) schema "addresses" (nested under people) Rows: p_alice ← row in "people" addresses → [ addr_home, addr_work ] ← rows in "addresses", under p_alice p_bob addresses → [ addr_apt ] ``` **Common rows / columns** of any schema — root or nested — are schema-level defaults. Address them directly with `schemas/schemaKey/…`, no parent walk needed: | What you want | Path shape | Example | | ---------------------------- | -------------------------------------- | ----------------------------------------------- | | Common rows in any schema | `pageId/fpId/schemas/sk/rows` | `pageId/fpId/schemas/addresses/rows` | | Common columns in any schema | `pageId/fpId/schemas/sk/columns/colId` | `pageId/fpId/schemas/addresses/columns/col_zip` | **A specific nested row or cell** lives under a particular parent. Walk through that parent's row id, then `schemas/sk/`, then the nested row id: | What you want | Path shape | Example | | -------------------- | -------------------------------------- | --------------------------------------------------------- | | Specific nested row | `…/rowId/schemas/sk/nestedRowId` | `pageId/fpId/p_alice/schemas/addresses/addr_home` | | Specific nested cell | `…/rowId/schemas/sk/nestedRowId/colId` | `pageId/fpId/p_alice/schemas/addresses/addr_home/col_zip` | If `addresses` itself had children, you'd chain another `schemas/.../rowId/…` after `addr_home` — the same pattern repeats for every level. > **Schema keys** come from the field's `schema` map. The schema marked `root: true` holds top-level rows; its `children` array names the nested schemas reachable from a row in this schema. ## API Four methods, all on `documentEditor.decorators`. Errors are reported via **`onError`** as `JoyfillError.DecoratorError(DecoratorError)`. ```kotlin theme={null} val fieldPath = "$pageId/$fieldPositionId" documentEditor.decorators.get(fieldPath) // -> List documentEditor.decorators.add(fieldPath, decorator) documentEditor.decorators.update(fieldPath, action = "openHelp", decorator = updated) documentEditor.decorators.remove(fieldPath, action = "openHelp") ``` Same shape for every path scope. A few examples: ```kotlin theme={null} // Common row decorators on a table — applied to every row documentEditor.decorators.add("$pageId/$fpId/rows", duplicate) // Override on a specific row documentEditor.decorators.add("$pageId/$fpId/$rowId", archive) // Cell-specific decorator documentEditor.decorators.add("$pageId/$fpId/$rowId/$colId", upload) // Nested collection row val nestedPath = "$pageId/$fpId/$parentRowId/schemas/$nestedSK/$nestedRowId" documentEditor.decorators.add(nestedPath, comment) ``` ## Behavior to know * **Copy-on-write seed.** First write to a row-self / cell scope seeds from the matching common scope, so existing common decorators stay visible on that row alongside your override. Subsequent writes diverge freely. * **Collection license gating.** Writes against a collection field require a license that enables collection features. Without it, the call emits `decoratorError` and is rejected. ## Handling taps Decorator taps come through **`onFocus`** with the decorator's `action` exposed on the field event's `type` / `target`. `rowIds` / `columnId` / `parentPath` on `FieldIdentifier` tell you where the user tapped. ```kotlin theme={null} onFocus = { event -> val field = event.fieldEvent ?: return@onFocus val action = field.type if (!action.isNullOrEmpty()) { // Decorator tap println("Decorator: $action, field: ${field.fieldID}, rows: ${field.rowIds ?: []}, column: ${field.columnId ?: "-"}") } else { // Ordinary field focus } } ``` See [Event handling](../architecture/v2/event-system) for the full focus/blur flow. ## Errors All four APIs report through `onError` as `JoyfillError.DecoratorError(DecoratorError)`: * Path didn't resolve (bad ids, deleted row, malformed grammar) * Validation (`action` empty, `color` not `#RRGGBB`) * Duplicate `action` in batch or against an existing entry * `remove` / `update` with an unknown `action` * Collection write without a valid license Reads (`get`) on an unresolvable path also emit `onError` and return `[]`. ## Display limits `DecoratorConfig`, passed to `DocumentEditor` at init, controls how many decorators render inline before the rest collapse into a kebab menu. ```kotlin theme={null} val config = DecoratorConfig( visibleLimitInFields = 2, // field + column scopes; default 2 visibleLimitInRows = 1 // row scopes; default 1 ) val editor = DocumentEditor(document = doc, decoratorConfig = config) ``` ## Supported icons The SDK maps common names to bundled artwork, including: `camera`, `import`, `paperclip`, `image`, `file`, `comment`, `comments`, `upload`, `download`, `rotate`, `cloud`, `filter`, `share`, `paper-plane`, `folder`, `folder-open`, `magnet`, `eye`, `circle-info`, `add`, `plus`, `print`, `flag`. Unknown names fall back to a default symbol. # Event Handling Source: https://docs.joyfill.io/kotlin/guides/event-handling This guide covers the event callbacks available in Joyfill forms and when they're triggered. ### **Available Events** | Event | When Triggered | Purpose | | ------------- | ---------------------- | ----------------------- | | onFieldChange | Field value changes | Track form changes | | onFocus | Field gains focus | Handle field focus | | onBlur | Field loses focus | Handle field blur | | onUpload | File upload requested | Handle file uploads | | onCapture | Barcode scan requested | Handle barcode scanning | ### **onFieldChange** Triggered when any field value changes: ```kotlin theme={null} Form( editor = rememberDocumentEditor(json), onFieldChange = { event -> println("Field '${event.fieldId}' changed") // Handle different event types when (event) { is ComponentEvent.FieldEvent -> { println("Regular field changed") } is ComponentEvent.CellEvent -> { println("Table cell changed in row: ${event.rowIds?.firstOrNull()}") } } } ) ``` ### **onFocus** Triggered when a field or a page gains focus: ```kotlin theme={null} onFocus = { event -> println("Page focused: ${event.pageId}") println("Field focused: ${event.fieldId}") // Your focus handling logic here } ``` ### **onBlur** Triggered when a field or a page loses focus: ```kotlin theme={null} onBlur = { event -> println("Page lost focus: ${event.pageId}") println("Field lost focus: ${event.fieldId}") // Your blur handling logic here } ``` ### **onUpload** Triggered when file upload is requested: ```kotlin theme={null} onUpload = { event -> // Return list of URLs for uploaded files listOf("https://picsum.photos/200/300") } ``` ### **onCapture** Triggered when barcode scanning is requested: ```kotlin theme={null} onCapture = { event -> // Return scanned barcode value "123456789" } ``` ### **Event Properties** All events provide context information: ```kotlin theme={null} onFieldChange = { event -> println("Document ID: ${event.id}") println("Field ID: ${event.fieldId}") println("Field Title: ${event.source?.component?.title}") println("Page ID: ${event.pageId}") } ``` **Note:** Events are not triggered when `mode = Mode.readonly`. # Form Configuration Source: https://docs.joyfill.io/kotlin/guides/form-configuration This document describes the configuration options available when initializing a `DocumentEditor` and rendering a `Form` for customizing form behavior, including page management, field interactions, and display options. ## Initialization with FormConfig The recommended way to configure a `DocumentEditor` (and the `Form` that renders it) is with a single `FormConfig` object. This groups every option — mode, event handlers, license, schema validation, page behavior, display, and formula functions — into one configuration surface. ```kotlin theme={null} import com.joyfill.editors.document.rememberDocumentEditor import com.joyfill.editors.document.Form import com.joyfill.editors.document.config.FormConfig import com.joyfill.editors.document.config.FormEvents import com.joyfill.editors.document.config.DisplayConfig import com.joyfill.editors.document.config.PageConfig import com.joyfill.editors.document.config.DecoratorConfig import com.joyfill.editors.document.Mode // 1. Event handlers val events = FormEvents( onChange = { event -> /* handle change */ }, onFocus = { event -> /* handle focus */ }, onBlur = { event -> /* handle blur */ }, onUpload = { event -> listOf("https://…") }, onCapture = { event -> "scanned-value" }, ) // 2. Page behavior val pageConfig = PageConfig( enableDuplicates = false, enableDeletes = false, currentPageId = null, ) // 3. Display & decorators val decoratorConfig = DecoratorConfig( visibleLimitInFields = 2, visibleLimitInRows = 1, ) val displayConfig = DisplayConfig( showUnsupportedFields = false, showUnsupportedColumns = true, showRowNumbers = true, inlineFields = false, singleClickRowEdit = false, decorators = decoratorConfig, ) // 4. Assemble the form config val config = FormConfig( mode = Mode.fill, license = "your-license", // optional events = events, display = displayConfig, pages = pageConfig, functions = null, // optional Wisdom functions validateSchema = true, ) // 5. Create the editor and render the form @Composable fun MyForm(document: Document) { val editor = rememberDocumentEditor( document = document, config = config, ) Form( editor = editor, // config defaults to editor.config — pass explicitly to override ) } ``` Every parameter has a default, so you only set what you need: ```kotlin theme={null} // Minimal — all defaults val editor = rememberDocumentEditor( document = myDocument, config = FormConfig(), ) Form(editor = editor) // Just an event handler val editor = rememberDocumentEditor( document = myDocument, config = FormConfig(events = FormEvents(onChange = ::handleChange)), ) ``` You can also build an editor from raw JSON without decoding to an intermediate `String`: ```kotlin theme={null} // From a JSON string val editor = rememberDocumentEditor(json = jsonString, config = config) // From UTF-8 bytes (preferred for large documents from files / network) val editor = rememberDocumentEditor(bytes = jsonBytes, config = config) ``` ### FormConfig | Parameter | Type | Default | Description | | ---------------- | ----------------------------------------- | ----------------- | --------------------------------------------------------------------------------------------------------------- | | `mode` | `Mode` | `Mode.fill` | Rendering mode. `Mode.fill` allows editing; `Mode.readonly` disables edits (and page duplication/deletion). | | `license` | `String?` | `null` | License token. A valid license enables licensed features such as Collection fields. | | `events` | `FormEvents` | `FormEvents()` | Field and page event handlers. See [Event Handling](/kotlin/guides/event-handling). | | `display` | `DisplayConfig` | `DisplayConfig()` | Field-interaction and decorator display behavior. | | `pages` | `PageConfig` | `PageConfig()` | Page-navigation capabilities. | | `functions` | `(ResolutionResourceBuilder.() -> Unit)?` | `null` | Optional Wisdom functions available to formulas during evaluation. | | `validateSchema` | `Boolean` | `true` | When `true`, validates the document schema on editor construction. See [Schema Validation](#schema-validation). | ### FormEvents | Parameter | Type | Default | Description | | ----------- | ------------------------------------------------------------------ | ------- | ---------------------------------------------------------------------- | | `onChange` | `((ComponentEvent<*>) -> Unit)?` | `null` | Invoked on field value changes and page create/delete events. | | `onFocus` | `((ComponentEvent<*>) -> Unit)?` | `null` | Invoked when a field or page gains focus, and on decorator taps. | | `onBlur` | `((ComponentEvent<*>) -> Unit)?` | `null` | Invoked when a field or page loses focus. | | `onUpload` | `(suspend (ComponentEvent) -> List)?` | `null` | Suspend handler for file uploads; return the resulting URLs. | | `onCapture` | `(suspend (ComponentEvent) -> String?)?` | `null` | Suspend handler for barcode / scan capture; return the captured value. | ### PageConfig | Parameter | Type | Default | Description | | ------------------ | --------- | ------- | ------------------------------------------------------------------ | | `enableDuplicates` | `Boolean` | `false` | Allow users to duplicate pages (`Mode.fill` only). | | `enableDeletes` | `Boolean` | `false` | Allow users to delete pages (`Mode.fill` only). | | `currentPageId` | `String?` | `null` | Page to open initially. `null` falls back to the first valid page. | ### DisplayConfig | Parameter | Type | Default | Description | | ------------------------ | ----------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | `showUnsupportedFields` | `Boolean` | `false` | When `true`, renders a placeholder for fields whose type isn't supported by the current SDK. | | `showUnsupportedColumns` | `Boolean` | `true` | When `true`, renders unsupported column types (as read-only placeholders) inside table/collection rows. | | `showRowNumbers` | `Boolean` | `true` | When `true`, displays row-number indicators on table and collection rows. | | `inlineFields` | `Boolean` | `false` | When `true`, renders supported fields inline instead of using the default stacked layout. | | `singleClickRowEdit` | `Boolean` | `false` | Open table/collection rows for editing with a single tap. | | `decorators` | `DecoratorConfig` | `DecoratorConfig.Default` | Controls how many decorators show inline before overflowing into a kebab menu. See [Decorators](/kotlin/guides/decorators). | The per-parameter overloads on `rememberDocumentEditor(document, mode, events, ...)` and `Form(editor, mode, ...)` are **deprecated**. They still work and forward their arguments into a `FormConfig`, but new code should use the config-based APIs above. ## Page Management ### Page Navigation The page navigation UI is provided via a composable slot on `Form`, not a boolean flag. The default slot renders a page selector; pass `null` to hide it entirely, or supply your own composable to fully customize navigation. ```kotlin theme={null} // Default page navigation (recommended) Form(editor = editor) // Hide the page navigation UI Form( editor = editor, navigation = null, ) // Custom navigation Form( editor = editor, navigation = { navigator, state -> MyCustomPageBar(state = state, onSelect = { navigator.navigate(it) }) }, ) ``` | Parameter | Type | Default | Description | | ------------ | -------------------------------------------------------------- | --------------------- | -------------------------------------------------------------------------------------- | | `navigation` | `@Composable ((PageCollection, PageCollectionState) -> Unit)?` | Default page selector | Composable slot for the page navigation UI. Set to `null` to hide navigation entirely. | **Behavior:** * The default navigation displays a page selector that allows users to switch between pages * The page selector respects conditional logic — hidden pages are not shown in the list * The default selector reads `config.pages.enableDuplicates` and `config.pages.enableDeletes` (combined with `config.mode`) to decide which page actions are exposed ### Page Duplication Control whether users can duplicate existing pages. ```kotlin theme={null} val config = FormConfig( mode = Mode.fill, // duplication only works in Mode.fill pages = PageConfig(enableDuplicates = true), ) val editor = rememberDocumentEditor(document = myDocument, config = config) Form(editor = editor) ``` | Parameter | Type | Default | Description | | ------------------------ | --------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `pages.enableDuplicates` | `Boolean` | `false` | When `true`, users can duplicate pages via the page navigation UI. The duplicated page includes all field values from the original. | **Behavior:** * Duplicated pages retain all field values from the source page, including conditional logic and formulas * The new page is inserted immediately after the source page in the page order * Duplication emits a `ComponentEvent.PageEvent.PageCreate` through `events.onChange` * Only takes effect in `Mode.fill` ### Page Deletion Control whether users can delete pages from multi-page forms. ```kotlin theme={null} val config = FormConfig( mode = Mode.fill, // deletion only works in Mode.fill pages = PageConfig(enableDeletes = true), ) val editor = rememberDocumentEditor(document = myDocument, config = config) Form(editor = editor) ``` | Parameter | Type | Default | Description | | --------------------- | --------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `pages.enableDeletes` | `Boolean` | `false` | When `true`, users can delete pages via the page navigation UI. A confirmation dialog appears before deletion to prevent accidental data loss. | **Behavior:** * Delete is only available when there is more than one page (you cannot delete the last remaining page) * A confirmation dialog is shown to the user before deletion * Page deletion is permanent and cannot be undone * Deletion emits a `ComponentEvent.PageEvent.PageDelete` through `events.onChange` * Only takes effect in `Mode.fill` ### Initial Page Open the form on a specific page rather than the first valid one. ```kotlin theme={null} val config = FormConfig( pages = PageConfig(currentPageId = "page_456"), ) ``` | Parameter | Type | Default | Description | | --------------------- | --------- | ------- | ------------------------------------------------------------------ | | `pages.currentPageId` | `String?` | `null` | Page to open initially. `null` falls back to the first valid page. | ## Field Interactions ### Single Click Row Edit Simplify the process for opening table and collection rows for editing. ```kotlin theme={null} val config = FormConfig( display = DisplayConfig(singleClickRowEdit = true), ) ``` | Parameter | Type | Default | Description | | ---------------------------- | --------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `display.singleClickRowEdit` | `Boolean` | `false` | When `true`, users can open a row for editing with a single tap. When `false`, users must go through multiple steps to edit a row. | **Behavior:** * **Default (`false`)**: Users must follow multiple steps to open a row form * **Enabled (`true`)**: Users can open the row form for editing with a single tap, providing a faster and easier editing experience * Applies to both Table fields and Collection fields * The edit behavior respects the form's mode — in `Mode.readonly`, rows cannot be edited regardless of this setting ### Inline Fields Render supported fields inline instead of using the default stacked layout. ```kotlin theme={null} val config = FormConfig( display = DisplayConfig(inlineFields = true), ) ``` | Parameter | Type | Default | Description | | ---------------------- | --------- | ------- | ----------------------------------------------------------------------------------------------------- | | `display.inlineFields` | `Boolean` | `false` | When `true`, supported field types render inline; when `false`, they use the default vertical layout. | ### Unsupported Fields and Columns Control whether the form renders placeholders for field or column types that aren't supported by the current SDK version. ```kotlin theme={null} val config = FormConfig( display = DisplayConfig( showUnsupportedFields = false, // hide unsupported top-level fields showUnsupportedColumns = true, // show unsupported columns as read-only placeholders ), ) ``` | Parameter | Type | Default | Description | | -------------------------------- | --------- | ------- | ------------------------------------------------------------------------------------------------------- | | `display.showUnsupportedFields` | `Boolean` | `false` | When `true`, renders a placeholder for unsupported top-level field types. | | `display.showUnsupportedColumns` | `Boolean` | `true` | When `true`, renders unsupported column types (as read-only placeholders) inside table/collection rows. | ### Row Numbers Toggle row-number indicators on table and collection rows. ```kotlin theme={null} val config = FormConfig( display = DisplayConfig(showRowNumbers = false), ) ``` | Parameter | Type | Default | Description | | ------------------------ | --------- | ------- | ------------------------------------------------------------------------- | | `display.showRowNumbers` | `Boolean` | `true` | When `true`, displays row-number indicators on table and collection rows. | ## Schema Validation Control whether the document schema is validated during initialization. ```kotlin theme={null} val editor = rememberDocumentEditor( document = myDocument, config = FormConfig(validateSchema = true), ) ``` | Parameter | Type | Default | Description | | ---------------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `validateSchema` | `Boolean` | `true` | When `true`, validates the document schema version and structure during construction. Validation errors are exposed via `editor.error` and reported to the configured error handler. | **Behavior:** * If validation fails, `editor.error` will contain a non-null error value * The `Form` composable renders the `error` slot instead of the document content * Set to `false` to skip validation (useful for testing or when you're certain the document is valid) See [Schema Validation](/kotlin/guides/schema-validation) for the full validation flow. ## Programmatic Page Operations You can also perform page operations programmatically using the `PageCollection` API exposed via `editor.pages`. ### Navigate to a Page ```kotlin theme={null} editor.pages.navigate("page_456") ``` ### Duplicate a Page ```kotlin theme={null} editor.pages.duplicate("page_123", "Copy of Page") ``` ### Delete a Page ```kotlin theme={null} editor.pages.delete("page_123") ``` # Image Upload Handling Source: https://docs.joyfill.io/kotlin/guides/image-upload-handling This guide shows you how to handle image uploads in Joyfill forms. ### **How It Works** When a user uploads an image, the `onUpload` callback is triggered. You need to: 1. Handle file selection (file picker or camera) 2. Process the files (validate, resize, etc.) 3. Upload to your server 4. Return the uploaded file URLs ### **Basic Example** ```kotlin theme={null} Form( editor = rememberDocumentEditor(json), onUpload = { event -> listOf("https://picsum.photos/200/300") } ) ``` ### **Using the Event Data** The `onUpload` callback receives information about the upload request: ```kotlin theme={null} onUpload = { event -> // Check which field requested upload println("Upload for field: ${event.fieldId}") println("Field title: ${event.source?.component?.title}") // Check if field supports multiple images val allowMultiple = event.source?.component?.multi == true // Return appropriate number of URLs if (allowMultiple) { listOf("https://picsum.photos/200", "https://picsum.photos/300", "https://picsum.photos/400") } else { listOf("https://picsum.photos/200") } } ``` ### **Async Uploads** For uploads that take time, use suspend functions: ```kotlin theme={null} onUpload = { event -> // Your async upload logic here delay(2000) // Simulate upload time // Return URLs when upload completes listOf("https://picsum.photos/200") } ``` **Note:** The `onUpload` callback only handles the Joyfill integration. You're responsible for implementing file selection, processing, and server upload according to your app's needs. # Form Modes Source: https://docs.joyfill.io/kotlin/guides/modes Joyfill forms can operate in two modes that control whether users can edit fields. ### **Available Modes** ```kotlin theme={null} enum class Mode { fill, readonly } ``` ### **Setting the Mode** Set the mode when creating your Form: ```kotlin theme={null} Form( editor = rememberDocumentEditor(json), mode = Mode.readonly // default is Mode.fill ) ``` ### **Mode Comparison** | Feature | Mode.fill (Default) | Mode.readonly | | -------------------------- | ---------------------------------------------------- | --------------------------------------------------- | | **Description** | Users can interact with and edit all form fields | Users can view the form but cannot make any changes | | **Field editing** | Enabled | Disabled | | **Events** | Fire normally (onFieldChange, onFocus, onBlur, etc.) | Don't fire | | **Upload/capture actions** | Enabled | Disabled | | **Field values** | Can be changed | Visible but locked | # Navigation Source: https://docs.joyfill.io/kotlin/guides/navigation This document describes how to programmatically navigate to pages, fields, table/collection rows, and individual cells within a form using the `goto` API on `DocumentEditor`. ## Overview The `goto` method enables programmatic navigation to specific locations within a form. This is useful for: * Guiding users to required fields after validation * Implementing custom navigation flows * Deep linking to specific form sections * Auto-scrolling to specific fields * Opening a specific table or collection row in its row form (modal) * Focusing a specific cell within a table or collection row ## Path-Based Navigation Navigate using a slash-separated path string. Up to four segments are supported. ```kotlin theme={null} // Navigate to a page editor.goto("page_123") // Navigate to a field on a page editor.goto("page_123/fieldPos_456") // Navigate to a row on a table/collection field on a page editor.goto("page_123/fieldPos_456/row_1") // Navigate to a row and open the row form modal editor.goto("page_123/fieldPos_456/row_1", GotoConfig(open = true)) // Navigate to a specific cell in a table or collection row editor.goto("page_123/fieldPosition_456/row_789/column_012", GotoConfig(open: true, focus: true)) ``` | Path Format | Description | | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `"pageId"` | Navigate to the top of the specified page | | `"pageId/fieldPositionId"` | Navigate to the page and scroll to the field | | `"pageId/fieldPositionId/rowId"` | Navigate to the page, scroll to the table/collection field, and select the row. Use `GotoConfig(open = true)` to also open the row form modal. | | `"pageId/fieldPositionId/rowId/columnId"` | Navigate to the page, scroll to the table/collection field, select the row, and target a specific column/cell. Use `GotoConfig(focus: true)` to auto-focus the cell. | **Important:** Use `fieldPositionId` from `page.fieldPositions[]._id`, not `fieldId` from `document.fields[]._id`. Using the wrong ID will cause navigation to fail. For row-level paths, the field must be a **table** or **collection** type. The `rowId` must match an existing row's ID in that field's value; otherwise `goto` returns `.failure`. For column-level paths, the `columnId` must match an existing visible column in the field; otherwise `goto` returns `.failure` (but still navigates to the row). ## GotoConfig Navigation behavior is configured via `GotoConfig`. Pass it as the second argument to `goto`. ```kotlin theme={null} /** * Configuration for [DocumentEditor.goto] navigation. * * @param open If true, automatically open row form when navigating to a row. * Only applies to table/collection row navigation; ignored otherwise. * @param animate Whether to animate the scroll. Defaults to true. */ class GotoConfig( val open: Boolean = false, val focus: Boolean = false, val animate: Boolean = true ) { companion object { val default = GotoConfig() } } ``` | Property | Default | Description | | --------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `open` | `false` | When `true`, automatically opens the row form modal for table/collection rows | | `animate` | `true` | Whether to animate the scroll | | `focus` | `false` | When `true`, triggers the SDK's `onFocus` event for the target field or cell. For text, number, and barcode cells this also opens the keyboard. | ## NavResponse The `goto` method returns a `NavResponse` indicating success or failure. ```kotlin theme={null} val response = editor.goto("page_123/fieldPos_456") when (response) { is NavResponse.Success -> { // Navigation succeeded - target exists and is visible } is NavResponse.Failure -> { // Navigation failed - target doesn't exist, is hidden, or unsupported } } ``` | Response | Description | | --------- | ---------------------------------------------------------------------- | | `Success` | The target exists, is supported, and is visible | | `Failure` | The target does not exist, is hidden, or has an unsupported field type | **Failure reasons include:** * Page does not exist * Page is hidden (due to conditional logic) * Field position does not exist * Field is hidden (due to conditional logic) * Field type is unsupported (`file`, `unknown`) * Row is hidden * Row does not exist * Column is hidden * Column does not exist ## Page Change Events Page changes triggered by `goto()` are emitted through the `onBlur` and `onFocus` callbacks. When the user navigates from one page to another (either via `goto()` or manual navigation), the callbacks receive a `ComponentEvent.PageEvent`: ```kotlin theme={null} Form( editor = editor, onBlur = { event -> if (event is ComponentEvent.PageEvent) { // User left this page println("Left page: ${event.pageId}") } }, onFocus = { event -> if (event is ComponentEvent.PageEvent) { // User arrived at this page println("Arrived at page: ${event.pageId}") } } ) ``` | Event | Trigger | | -------------------------- | ---------------------------------------- | | `onBlur` with `PageEvent` | Emitted when navigating away from a page | | `onFocus` with `PageEvent` | Emitted when navigating to a page | **`PageEvent` Properties:** | Property | Type | Description | | -------- | -------- | ----------------------------------------- | | `page` | `Page` | The page object that gained or lost focus | | `pageId` | `String` | The page ID | # Populating and Extracting Data Source: https://docs.joyfill.io/kotlin/guides/populating-and-extracting-data This guide shows you how to populate form fields and extract data from completed forms. ### **Accessing Fields** Use the `DocumentEditor.fields` property to access form fields. ### **Finding Fields** ```kotlin theme={null} val editor = rememberDocumentEditor(json) // Find by ID, identifier, or title val nameField = editor.fields.find("field_id") val emailField = editor.fields.find("Email Address") // by title // Find specific field types val textField = editor.fields.text("Full Name") val numberField = editor.fields.number("Age") val imageField = editor.fields.image("Profile Picture") val tableField = editor.fields.table("Employee Data") ``` ### **Extracting Data** **Document-Level Extraction** ```kotlin theme={null} // Extract as Document object val document: Document = editor.toDocument() // Extract as JSON string val jsonString: String = editor.toJsonString() // Extract as Map val dataMap: Map = editor.toMap() ``` **Field-Level Extraction** ```kotlin theme={null} // Get individual field values val name = editor.fields.text("Full Name")?.state?.value?.data val age = editor.fields.number("Age")?.state?.value?.data val email = editor.fields.text("Email")?.state?.value?.data val profileImages = editor.fields.image("Profile Picture")?.state?.value?.data ``` **Note:** To get a reactive state, use `state.watchAsLive()`. ### **Working with Tables** **Populating Table Data** ```kotlin theme={null} // Get table editor val employeeTable = editor.fields.table("Employees") // Add new rows and populate data employeeTable?.let { table -> // Add a new row val newRow = table.rows.append() // Populate row cells newRow.text("First Name")?.value("John") newRow.text("Last Name")?.value("Doe") newRow.number("Salary")?.value(75000.0) newRow.dropdown("Department")?.value("Engineering") } ``` **Extracting Table Data** ```kotlin theme={null} fun extractTableData(editor: DocumentEditor): List> { val table = editor.fields.table("Employees") ?: return emptyList() return table.rows.all().map { row -> mapOf( "firstName" to row.text("First Name")?.state?.value?.data, "lastName" to row.text("Last Name")?.state?.value?.data, "salary" to row.number("Salary")?.state?.value?.data, "department" to row.dropdown("Department")?.state?.value?.data?.value ) } } ``` ### **Document Properties** **Setting Document Properties** ```kotlin theme={null} // Set document-level properties editor.name = "User Registration Form" editor.identifier = "user_reg_2024" editor.stage = Stage.published // Set custom properties editor.set("customField", "customValue") editor.set("metadata", mapOf("version" to "1.0")) ``` **Getting Document Properties** ```kotlin theme={null} // Get document properties val documentName = editor.name val documentId = editor.id val documentStage = editor.stage // Get custom properties val customValue: String = editor.get("customField") val metadata: Map = editor.get("metadata") ``` ### **Things to Know** Use `editor.fields.find()` or type-specific methods like `text()` and `number()` to access fields. Field changes automatically update the underlying document state. To extract complete form data, use `toDocument()`, `toJsonString()`, or `toMap()`. Cast field values to appropriate types when extracting data. Tables require row-level access for both population and extraction. # Required Field Validation Source: https://docs.joyfill.io/kotlin/guides/required-field-validation This guide shows you how to validate required fields in Joyfill forms. ### **How It Works** Joyfill automatically validates required fields based on: * **Field requirement**: Fields marked as required = true * **Field visibility**: Hidden fields are always filtered out of the validation Output * **Field values**: Required fields must have non-empty values to be valid ### **Document-Level Validation** Validate all fields in your document: ```kotlin theme={null} val editor = rememberDocumentEditor(json) // Validate entire document val validity = editor.validate() when (validity) { is FieldsValid -> { // All required fields have values println("All fields are valid!") } is FieldsInvalid -> { // Handle validation errors println("Some fields are invalid") } } ``` ### **Handling Invalid Fields** When some required fields are empty: ```kotlin theme={null} when (val validity = editor.validate()) { is FieldsInvalid -> { // Access valid fields directly validity.validFields.forEach { field -> println("${field.component.title} is valid") } // Access invalid fields directly validity.invalidFields.forEach { field -> println("${field.component.title} is invalid") field.messages.forEach { message -> println("Error: $message") } } } } ``` ### **Individual Field Validation** Validate specific fields: ```kotlin theme={null} // Get a field editor val nameField = editor.fields.text("Full Name") // Validate the field val fieldValidity = nameField?.validate() when (fieldValidity) { is ComponentValid -> { println("Field is valid") } is ComponentInvalid -> { println("Field is invalid:") fieldValidity.messages.forEach { message -> println("- $message") } } } ``` ### **Validation Messages** Required field validation provides standard error messages: ```kotlin theme={null} when (val validity = editor.validate()) { is FieldsInvalid -> { validity.invalidFields.forEach { field -> // Standard format: "Component [title] is required" println(field.messages.first()) // "Component Full Name is required" } } } ``` ### **Checking Field Requirements** Check if a field is required: ```kotlin theme={null} val nameField = editor.fields.text("Full Name") val isRequired = nameField?.component?.required == true if (isRequired) { println("This field is required") } ``` ### **Validation Rules** | Field Type | Condition | Valid? | | ---------- | ------------------- | ------------ | | Required | Has non-empty value | Yes | | Required | Empty or null | No | | Required | Hidden | Yes (always) | | Optional | Any value | Yes (always) | ### **Key Points** * Required fields are automatically validated based on the `required` property * Hidden fields are always filtered out of the validation Output * Validation errors use the format: "Component \[title] is required" * Call `editor.validate()` anytime to get current validation status * Document validation checks all visible required fields at once # Schema Validation Source: https://docs.joyfill.io/kotlin/guides/schema-validation This guide shows you how to validate document structure using Joyfill's schema validation system. ### **What is Schema Validation** Schema validation checks if your JSON document follows the correct Joyfill document structure before creating a form. ### **Enabling/Disabling Schema Validation** Schema validation is **enabled by default** in `rememberDocumentEditor`: ```kotlin theme={null} // Disable schema validation val editor = rememberDocumentEditor( json = jsonString, validateSchema = false // default is true ) ``` ### **How It Works** When `validateSchema = true`: 1. Document is validated against the Joyfill schema during editor creation 2. If validation fails, an error is set and the `onError` callback is triggered 3. If validation passes, the editor is created normally ### **Schema Error Types** | Error Type | Code | When It Happens | Example | | ------------------------- | ------------------------- | --------------------------------------------- | ------------------------------------------------ | | **SchemaError.Assertion** | ERROR\_SCHEMA\_VALIDATION | Document structure doesn't match schema rules | Missing required properties like files or fields | | **SchemaError.Format** | ERROR\_SCHEMA\_FORMAT | JSON format is invalid | Empty/blank string or malformed JSON | | **SchemaError.Version** | ERROR\_SCHEMA\_VERSION | Document version incompatible with SDK | Document created with newer SDK version | ### **Error Monitoring** Monitor schema errors reactively: ```kotlin theme={null} @Composable fun FormWithSchemaValidation() { val editor = rememberDocumentEditor(json = jsonString) val error = editor.error.watchAsState() if (error != null) { // Schema validation failed Text("Schema Error: ${error.message}") } else { // Schema is valid, show form Form(editor = editor) } } ``` ## Things to know Schema validation is enabled by default (`validateSchema = true`). Disabling schema validation can lead to crashes and should only be used for testing. Schema errors include both user-friendly messages and technical details. Validation happens during editor creation, not during form interaction. # React Native Source: https://docs.joyfill.io/react-native/introduction Learn how to render your first form using the Joyfill React Native SDK. Joyfill's React Native SDK supports raw react native and expo projects. The SDK is NOT a wrapped web-view. The SDK uses all pure react native components in order to provide the best possible performance and support across your react native and expo apps. # Requirements * [Review SDK README](https://www.npmjs.com/package/@joyfill/components-react-native#project-requirements) # 1. Install * [Review SDK Installation](https://www.npmjs.com/package/@joyfill/components-react-native#install-dependency) # 2. Usage * Add the Template identifier from the previous setup step to the `App.js` file. * Add the User Access Token from the preview setup step to the `api.js` file. ```bash bash theme={null} import React, { useState, useEffect } from 'react'; import { Dimensions } from 'react-native'; import { retrieveTemplate } from '../api.js'; import { JoyDoc } from '@joyfill/components-react-native'; const screenWidth = Dimensions.get('window').width; function App() { const [ template, setTemplate ] = useState(null); /** * Add your template identifier */ const identifier = ''; /** * Retrieve template via the Joyfill API */ useEffect(() => { const handleRetrieveTemplate = async () => { const response = await retrieveTemplate(identifier); setTemplate(response); }; handleRetrieveTemplate(); }, []); return ( { /** * Changelogs represent the individual change that was made * Data represents the entire data structure with all new changes applied. */ console.log('>>>>>>>>: ', changelogs, doc); }} /> ); } ``` ```bash bash theme={null} const userAccessToken = ""; const apiBaseUrl = "https://api-joy.joyfill.io"; export const retrieveTemplate = async (identifier) => { //See API Doc Here: https://docs.joyfill.io/reference/retrieve-a-template const response = await fetch(`${apiBaseUrl}/v1/templates/${identifier}`, { method: 'GET', mode: 'cors', headers: { Authorization: `Bearer ${userAccessToken}`, 'Content-Type': 'application/json' }, }); const data = await response.json(); return data; } ``` ### Do not wrap JoyDoc component inside of a ScrollView. JoyDoc rendering optimizations will not work properly inside of ScrollView and will introduce unintended bugs. # 3. Try it yourself If you’re looking for a full example project that shows many more of the Joyfill SDK capabilities and workflows then head over to our full example project and try it for yourself. * [Joyfill React Native Example](https://github.com/joyfill/examples/tree/main/react-native) * [SDK NPM Package](https://www.npmjs.com/package/@joyfill/components-react-native) *** # Security Source: https://docs.joyfill.io/web/about/security **Active security advisory (JOYFILL-SA-2026-001).** Six `@joyfill` npm **beta** releases published on July 28, 2026 contained malicious code. All carry the `2773` prerelease build marker and have been removed from npm. If you install `@joyfill/layouts` or `@joyfill/components` from npm, [check whether you are affected](/web/about/security-advisory). # Overview Joyfill follows industry best-practices to keep your data safe: You can only access the Joyfill service via TLS (https). When you submit data to for storage or to generate a PDF, this data is encrypted at rest using AES-256. All stored files are encrypted at rest, using the AWS Key Management Service. This includes template PDFs, generated PDFs, and any other files that are stored in Amazon S3. Passwords are salted and hashed with high level expansion rounds. We do not store plaintext passwords in our database. We subscribe to security mailing lists and patch any vulnerabilities as soon as possible. # Compliance (HITRUST, HIPAA, SOC 2, PCI DSS, etc.) To maintain compliance we offer a self-hosting option. Our self-hosting option allows you to retain and manage all template, submission, file and any other data directly in your own system. [See Self-Hosting](/joyfill/self-hosted) or reach out to our team directly via live chat at [https://joyfill.io](https://joyfill.io) # Security Advisories * **[JOYFILL-SA-2026-001 — Compromised `@joyfill` npm beta releases](/web/about/security-advisory)** · Published July 29, 2026 · Investigation ongoing # Vulnerability Disclosures Joyfill welcomes vulnerability disclosures. Please send an email to [contact@joyfill.io](mailto:contact@joyfill.io) to report any security vulnerabilties. # Questions Reach out directly to our team using our live chat at [https://joyfill.io](https://joyfill.io) *** # Security Advisory: Compromised @joyfill npm beta releases and public github repos Source: https://docs.joyfill.io/web/about/security-advisory JOYFILL-SA-2026-001 — compromised @joyfill npm beta releases and public github repos: affected versions, how to check whether you are impacted, and remediation steps. **Advisory ID:** JOYFILL-SA-2026-001 · **Published:** July 29, 2026 · **Last updated:** August 2, 2026 (Revision 2) **Status:** Investigation ongoing · **Severity:** Critical for affected installs *** > **Update, August 2, 2026 — npm verification complete; scope expanded to cover source repositories.** We have finished verifying our published npm packages and found no additional affected versions. We have separately identified the malicious code in three public Joyfill GitHub repositories. If you cloned any Joyfill repository directly from GitHub, see [Cloned GitHub repositories](#cloned-github-repositories). *** ## Start here: are you affected? There are two separate ways to have been exposed. Run both checks — they are independent, and a clean result on one says nothing about the other. ### Check 1 — did you install an affected npm version? Six **beta** releases were published on July 28, 2026 containing malicious code. All carry the `2773` prerelease build marker, and no release without that marker has been found to contain the implant. **All six have been removed from npm. Stable releases are not currently believed to be affected.** | Package | Affected versions | Confirmed clean | | --------------------- | ---------------------------------------------------------------------------- | --------------- | | `@joyfill/layouts` | `0.1.2-2773.beta.0`, `0.1.2-2773.beta.1`, `0.1.2-2773.beta.2` | `0.1.1` | | `@joyfill/components` | `4.0.0-rc24-2773-beta.4`, `4.0.0-rc24-2773-beta.5`, `4.0.0-rc24-2773-beta.6` | `4.0.0-rc24` | Run this from the root of any project that uses `@joyfill` packages — the directory containing your `package.json`. In a monorepo, run it once at the repository root; the search recurses into workspace lockfiles beneath it. ```bash theme={null} grep -rEn --include=package-lock.json --include=yarn.lock --include=pnpm-lock.yaml \ 'joyfill.*2773' . 2>/dev/null ``` On Windows, from the same directory (PowerShell): ```powershell theme={null} Get-ChildItem -Recurse -Include package-lock.json,yarn.lock,pnpm-lock.yaml | Select-String "joyfill.*2773" ``` To see what actually resolved rather than what the lockfile records, run this from a directory where `node_modules` is installed: ```bash theme={null} npm ls @joyfill/layouts @joyfill/components ``` Repeat for every repository or checkout that depends on `@joyfill`. A check on your own machine does not cover lockfiles that exist only in CI caches, container images, or a colleague's environment. **If this finds anything**, the affected versions execute code at import time, so an install alone is enough — go to [If you installed or imported an affected version](#if-you-installed-or-imported-an-affected-version). ### Check 2 — did you clone a Joyfill repository from GitHub? Separately from npm, three public Joyfill repositories on GitHub contained the same malicious code. This is a distinct exposure path: it affects anyone who cloned those repositories directly, whether or not they ever installed a `@joyfill` package. **If you cloned, pulled, or fetched any Joyfill repository from GitHub between May 25, 2026 and August 1, 2026** — including an older clone you updated during that window — go to [Cloned GitHub repositories](#cloned-github-repositories). ### Both checks clean? You are not affected by this advisory and can stop after the two items below. **Do not install** `@joyfill/layouts `**or** `@joyfill/components `**from the npm** `beta `**dist-tag** until we confirm remediation is complete, since that tag can resolve to a prerelease build on your next install. **Pin to an exact version that is not a `2773` build.** Whatever version you are on today is fine, as long as it does not carry the `2773` marker. Pin that exact version rather than a range: ```bash theme={null} npm install --save-exact @joyfill/components@ ``` Your `package.json` should read `"@joyfill/components": "4.0.0-rc24"` — an exact version with no `^` or `~` in front of it. **This advisory is not a reason to upgrade.** If you are running an older release, stay on it. Older releases predate this entirely. Upgrading across a major version to respond to this incident would take on breakage risk you do not need. If you want a version that has been independently confirmed clean by the analyses linked below, those are `@joyfill/layouts@0.1.1` and `@joyfill/components@4.0.0-rc24`. **Where the range prefix actually matters.** If your components dependency is written as `^4.0.0-rc24` or `~4.0.0-rc24`, change it to the exact version. Because `4.0.0-rc24` is itself a prerelease, those ranges **do** match the compromised `2773` builds — under semver they sort above `4.0.0-rc24`, so they are valid upgrades within the range, and your next install could pull one. Any other range — `^3.x`, `^2.x`, anything anchored on a stable version, or any range on `@joyfill/layouts` — cannot reach a `2773` build. Pinning exactly is still good practice, but there is no urgency for you here. If you want to be extra careful, re-run Check 1 after your next `npm install`, in case a transitive update pulls a different version. *** ## If you installed or imported an affected version One thing to know before you start: this implant runs when Node.js **loads** the module, not when npm installs it. `npm install --ignore-scripts` does not prevent it. Any process that imported the package — test runner, bundler, dev server, SSR, CI job — is enough to have triggered it. Because the payload can execute arbitrary commands, treat the machine as compromised rather than merely exposed. **The clean remediation is to reimage it.** Work through these in order, and do steps 2 onward from a different, known-clean machine. 1. **Take the affected machine offline.** Preserve logs, shell history, and dependency artifacts before cleaning anything. If forensics may be needed later, image the disk first. 2. **Rotate credentials reachable from that machine or process** — npm tokens, cloud keys, CI secrets, SSH keys, GitHub personal access tokens, database credentials, local `.env` contents. 3. **Rotate browser-stored credentials and sessions** on that machine: saved passwords, cookies, and any password-manager or wallet extension data. 4. **Reimage the machine.** We recommend a full reimage rather than targeted cleanup. The recovered payload injects self-reloading code into local developer tooling — editors, desktop applications, and the global npm CLI — so it can survive both removal of the package and rotation of credentials, and steal the new credentials in turn. There is also no reliable way to confirm from the outside that a given machine is clean: the payload is fetched at runtime, so machines may have received different code, and the credential-stealing branch can run and exit without leaving anything behind to find. If reimaging is genuinely not possible, the linked third-party analyses below list the specific injection targets and file markers needed for manual cleanup. Treat that as the weaker option, and contact us if you want help working through it. 5. **Purge the package everywhere it persists.** Delete `node_modules` and reinstall from a corrected lockfile so the compromised bundle is gone from disk, then clear it from local and CI caches, internal registry mirrors, container build layers, and deployment artifacts. Block the affected versions in your registry proxy or dependency policy tooling so automated resolution can't restore them. 6. **Work out how far it reached.** Reimaging fixes one machine. It does not tell you whether other machines imported the same package, whether the implant ever connected, or how long it was live — and wiping the disk destroys the local evidence that would. Network and CI logs survive the rebuild, so they are your best remaining source. Look for detached `node` processes and outbound blockchain RPC traffic from build agents or developer workstations; blockchain RPC calls from a CI runner are a strong signal on their own. Indicators of compromise are in the third-party analyses linked below. If you need help assessing exposure, contact us directly — see [Contact](#contact). *** ## Cloned GitHub repositories Separately from npm, we identified malicious code in project files in three public Joyfill repositories on GitHub. This section is about environments that cloned these repositories directly from GitHub. **These repositories were made inaccessible on August 1, 2026.** They can no longer be cloned or pulled from. This section applies to any clone that was created, pulled, or fetched between May 25, 2026 and August 1, 2026 — including a clone made years earlier that was updated during that window. | Repository | File | Runs when | | ---------------------------------------------- | ----------------------------------- | ---------------------- | | `https://github.com/joyfill/react-grid-layout` | `webpack.config.js` | a webpack build runs | | `https://github.com/joyfill/examples` | `VueJs/babel.config.js` | Babel runs | | `https://github.com/joyfill/layout-engine` | `src/utils/reactGridLayoutUtils.js` | the module is imported | **If you cloned any of these, inspect the file listed above in your clone.** The code is hidden behind a long run of whitespace padding on an existing line, so it will not be visible during a normal read. Two reliable tells: an unexpected jump in file size, and any line longer than about 2,000 characters. ```bash theme={null} # From the root of your clone awk 'length > 2000 {print FILENAME": line "FNR" is "length" chars"}' \ $(git ls-files '*.js' '*.mjs' '*.cjs') 2>/dev/null ``` **Finding the code in a clone does not mean your machine was affected.** The code is inert on disk and runs only when the file it sits in is executed or imported. If you cloned a repository and never built, tested, or ran anything from it, delete the clone and there is nothing further to do. The threshold is lower than it sounds, though. For the two configuration files, running any build from the project is enough. For `layout-engine`, importing the module is enough — a dev server, a test run, or a bundling step will do it, without anyone deliberately running that file. If you built, tested, or ran anything from one of these clones, follow the steps in [If you installed or imported an affected version](#if-you-installed-or-imported-an-affected-version). *** ## If you publish npm packages Read this even if you have already cleaned the affected machine. The payload patches the global npm CLI. Once that file is modified, every subsequent `npm` command on that machine re-executes the loader — including `npm publish`. Any package built or published from an affected machine may therefore carry the implant onward to your own users, independently of anything to do with `@joyfill`. If you maintain packages and an affected machine was used to build or publish them: 1. Treat any release published from that machine since it was affected as suspect, and verify the published tarballs rather than the source they were built from. The implant is injected into built bundles, so a clean repository does not mean a clean artifact. 2. Rotate your npm publishing tokens from a clean machine before publishing anything else. 3. Publish subsequent releases from CI with provenance rather than from a workstation. We recognise how much work that is. We would rather flag it than leave anyone to discover it downstream. *** ## What the malicious code does For readers who need to scope their response. Two teams independently identified and analyzed this compromise, and we are grateful for their work. Both write-ups are recommended reading and together carry the full set of indicators of compromise: * Socket Research Team — [https://socket.dev/blog/joyfill-npm-beta-releases-compromised](https://socket.dev/blog/joyfill-npm-beta-releases-compromised) * StepSecurity — [https://www.stepsecurity.io/blog/joyfill-npm-supply-chain-compromise](https://www.stepsecurity.io/blog/joyfill-npm-supply-chain-compromise) The implant retrieves its payload dynamically at runtime, which means its behavior could change without a new package being published. The recovered payloads are capable of: * Establishing an interactive remote-control channel to attacker infrastructure * Executing arbitrary JavaScript and shell commands on the host * Collecting host and environment information, and reading clipboard contents * Uploading files from the machine * Harvesting browser-stored credentials, wallet and password-manager extension data, Git credentials, GitHub CLI configuration, and editor storage * Persisting by modifying local developer tooling, including the global npm CLI Socket links the implant to the DEV#POPPER / Contagious Interview malware family. **As of this publication, we have found no indication that Joyfill's hosted platform, production infrastructure, or customer data stored in Joyfill are affected.** That review is still in progress, so we are describing this as a preliminary assessment rather than a final finding, and we will update this page if it changes. The risk we have identified from this incident is to developer and build machines that imported the affected packages. *** ## Investigation status Our investigation has made substantial progress. We have established how the malicious code reached our published packages, and we have already acted on what we found — the remediation work described below reflects specific findings rather than precautionary guesswork. When our investigation is complete, we will publish a full technical post-mortem covering root cause, timeline, and remediation, and we will link it from this page. If you have a specific question in the meantime — as a customer, a downstream maintainer, or a researcher — please contact us directly rather than inferring from what is not yet published. See [Contact](#contact). *** ## Scope verification **Verification complete for npm.** We have manually inspected every version we published to npm over the past 12 months (over 800+ versions), along with the dependency packages we publish and manage. We did not find the known malicious code in any of them. The affected versions are the beta releases listed above, and no others at this time. This closes the verification we committed to when this advisory was first published. Our npm packages outside those beta releases are clean from the known malicious code. *** ## What we are changing * Removing the code from all affected branches and re-verifying full repository history * Auditing and restricting repository access across the organization * Rotating all publishing tokens and third-party integration credentials * Moving publishing off individual machines onto CI with npm trusted publishing, so future releases carry verifiable provenance * Adding automated checks for code introduced outside of normal review, and requiring Joyfill maintainer review on all merges *** ## Contact For anything related to this advisory — security questions, security reports, or help assessing your own exposure — contact [**support@joyfill.io**](mailto:support@joyfill.io). We respond within one business day. If you believe you were affected and want help working through the steps above, reach out and we will work through it with you. *** ## Changelog * **2026-08-02 (Revision 2)** — Completed verification of all npm versions published in the past 12 months; no additional affected versions found. Added a section covering three public GitHub repositories found to contain the same code, none of which were published to npm. Those repositories were made inaccessible on August 1, 2026. * **2026-07-29 (Revision 1)** — Initial publication. # API Overview Source: https://docs.joyfill.io/web/api-reference/overview Default export object containing all public APIs of `@joyfill/components`. ## Core Components JoyDoc and JoyDocExporter components PublicAPI utility functions ## Quick Reference ### JoyDoc Component The main form builder component for creating and editing JoyDoc forms. ```jsx React theme={null} import { JoyDoc, getDefaultDocument } from '@joyfill/components'; function App() { return ( console.log('Document updated:', doc)} features={{ validateSchema: true, readableIds: true }} /> ); } ``` ```js JavaScript theme={null} // Option 1: Using CDN (include this script tag in your HTML) // // Option 2: Using ES Modules // import Joyfill from "@joyfill/components/dist/joyfill.min.js"; // Initialize JoyDoc const container = document.getElementById('joydoc-container'); const myDocument = Joyfill.getDefaultDocument(); Joyfill.JoyDoc( container, { doc: myDocument, mode: 'edit', view: 'desktop', onChange: (changelogs, doc) => console.log('Document updated:', doc), features: { validateSchema: true, readableIds: true } } ); ``` **Key Props:** * `doc` - The JoyDoc document object containing form structure and data * `mode` - Display mode: 'edit', 'view', or 'preview' * `onChange` - Callback fired when document changes * `features` - Feature flags for enabling/disabling functionality ### JoyDocExporter Component PDF export component for rendering JoyDoc forms as PDF-ready layouts. ```jsx React theme={null} import { JoyDocExporter, getDefaultDocument } from '@joyfill/components'; function App() { const myDocument = getDefaultDocument(); return ( ); } ``` ```js JavaScript theme={null} // Option 1: Using CDN (include this script tag in your HTML) // // Option 2: Using ES Modules // import Joyfill from "@joyfill/components/dist/joyfill.min.js"; // Initialize JoyDocExporter const container = document.getElementById('exporter-container'); const myDocument = Joyfill.getDefaultDocument(); Joyfill.JoyDocExporter( container, { doc: myDocument, config: { page: { height: 1056, width: 816, padding: 20 } }, theme: { fontFamily: 'sans-serif', field: { margin: 4 } } } ); ``` **Note:** For JavaScript usage, you must import from `dist/joyfill.min.js` because it's a UMD bundle that includes React and provides browser-friendly wrapper functions. The main package exports (`@joyfill/components`) are React components that require React as a peer dependency. ## Common Patterns ### Creating a New Document ```javascript theme={null} import { getDefaultDocument } from '@joyfill/components'; const newDoc = getDefaultDocument(); console.log('Document ID:', newDoc._id); ``` ### Duplicating a Template ```javascript theme={null} import { duplicate } from '@joyfill/components'; const copy = duplicate(template, { name: "Template Copy" }); ``` ### Validating a Document ```javascript theme={null} import { validator, validateSchema } from '@joyfill/components'; // Field validation const result = validator(doc, { view: 'desktop' }); // Schema validation const schemaError = validateSchema(doc); if (schemaError) { console.error('Schema validation failed:', schemaError.message); } ``` ## Next Steps Complete component documentation Detailed function documentation # Release Notes Source: https://docs.joyfill.io/web/changelogs/releases Complete changelog of Joyfill Web SDK releases > Source: [https://github.com/joyfill/components/releases](https://github.com/joyfill/components/releases) *** ## 4.0.0-rc25 **Release Date:** August 28, 2026 This release introduces **Conditional Required Logic** and **Cell Visibility Logic**, plus an explicit `multi` property for multiple choice fields. 🚀 **New Features** ADDED * **Conditional Required Logic:** Required validation can now be driven by conditions instead of a fixed flag. Rules can be configured on fields, table or collection columns, individual cells (evaluated per row), and nested collection child tables. Use `enforce` to make something required when conditions are met, or `unenforce` to lift a requirement. Cell-level rules can react to sibling columns in the same row. * **Cell Visibility Logic:** Individual cells within a table or collection column can now be shown or hidden per row, based on sibling columns or fields elsewhere in the document. Column headers stay in place, hidden cells keep their stored values, and hidden cells are exempt from required validation. 🛠️ **Enhancements** CHANGED * **Multiple Choice `multi` Property:** Multiple choice fields now use an explicit `multi` property to control whether more than one option can be selected. Set `multi: true` to allow multiple selections. Without it, the field behaves as single choice. *** ## 4.0.0-rc24 **Release Date:** July 9, 2026 This release introduces support for **Landscape Mode** and includes several fixes to improve document rendering and exports. 🚀 **New Features** ADDED * **Landscape Mode:** Documents can now be created and viewed in landscape orientation, providing greater flexibility for wider layouts and content. (NO-1991) 🛠️ **Bug Fixes** FIXED * **Overlapping Fields in Exports:** Resolved an issue where block fields could overlap collection fields in exported documents. (NO-2223) * **Collection Table Rendering in RTE View:** Fixed an issue where collection field tables were missing their left borders in the Rich Text Editor (RTE) view. (NO-2240) *** ## 4.0.0-rc23 **Release Date:** June 25, 2026 🚀 **New Features** ADDED * **Custom Titles for Nested Schemas:** * Added support for custom titles on nested schemas within Collection fields, allowing builders to define descriptive titles instead of relying on default naming conventions. (NO-2222) * This introduces a dedicated title configuration for nested schemas, providing clearer context, improved labeling, and more intuitive form layouts while separating title styling from column styling. 🛠️ **Bug Fixes** FIXED * **Rich Text Editor Top Borders:** * Resolved an issue where top borders were not rendered correctly within the Rich Text Editor (RTE), causing inconsistencies in the visual layout of formatted content. (NO-2125) * Top borders now render consistently, ensuring structured content and section dividers maintain their intended appearance. * **Missing Table Headers on Mobile:** * Resolved an issue where table headers would disappear when tables were viewed on mobile devices, making tabular data difficult to interpret. (NO-2209) * Table headers are now preserved across smaller viewport sizes, ensuring tables remain readable and responsive on mobile. * **Date Picker Month/Year Selector Inside Modals:** * Resolved an issue where the Date Picker's month and year selectors would briefly appear and immediately disappear when a JoyDoc was rendered inside a modal. (NO-2069) * This was caused by the interaction between the modal's focus management and the Date Picker's dropdown rendering. The month and year selectors are now rendered reliably within the modal, allowing users to navigate and select dates without interruption. *** ## 4.0.0-rc22 **Release Date:** June 16, 2026 🛠️ **Bug Fixes** FIXED * **PDF Page Breaking Mismatch:** * Resolved an issue where page breaks in exported PDFs could occur earlier than expected, causing large blank areas to appear at the bottom of pages and pushing content of the collection field onto subsequent pages unnecessarily. (NO-2203) * This was caused by inconsistencies between the layout measurement phase and the final PDF rendering phase, resulting in incorrect page height calculations. Page break calculations now accurately reflect the final rendered content, improving content distribution across pages and reducing unused page space. *** ## 4.0.0-rc21 **Release Date:** June 10, 2026 ## 🚀 Highlights This release introduces dynamic decorator management APIs, enhanced support for row-specific decorators, and improvements to image rendering across page breaks in PDF exports. ## 🛠️ Enhancements * **NO-2179**: Added support for dynamic decorator manipulation through the new `DecoratorManager` API, allowing decorators to be added, updated, retrieved, and removed programmatically at runtime. * **NO-2178**: Added support for row-specific decorators and improved the behavior of common and specific row decorator customization. * **NO-2165**: Fixed an issue where images could be split across pages during PDF generation. Images that would otherwise be cut by a page break are now rendered in full on the next page. ## JSON Schema Updates * Added support for dynamic decorator management via `DecoratorManager`. * Standardized decorator lookups to use `action` as the canonical identifier. * Decorator `_id` is no longer required and is no longer generated for decorator objects. * Added support for row-specific decorators through `rows.decorators.all`. *** ## 4.0.0-rc20 **Release Date:** May 23, 2025 ## 🚀 Highlights This release includes improvements to decorator customization, card layout image rendering, export view spacing, and column header overflow handling. ## 🛠️ Enhancements * **NO-2089**: Added support to customize decorator count. * **NO-2162**: Added ability to wrap or ellipsize column headers. * **NO-1987**: Fixed an issue where image sizing was not displaying properly in card layout. * **NO-2161**: Fixed excessive padding for horizontal multi-select fields in export view. ## JSON Schema Updates * Decorator `_id` is now optional. * Added `columnTitleTextOverflow` to the `FieldPosition` schema. *** ## 4.0.0-rc19 **Release Date:** May 11, 2026 🛠️ **Bug Fixes** FIXED * **Dropdown Default Value Rendering:** * Fixed an issue where dropdown fields failed to properly render configured default values during initialization and rendering flows. (NO-2141) * This fix ensures dropdown components now correctly display their expected default selections across supported use cases. *** ## 4.0.0-rc18 **Release Date:** May 11, 2026 🛠️ **Bug Fixes & Improvements** FIXED * **New Decorator icons:** This release adds the new **pencil** and **pen-square** icons for decorators *(NO-2123)* * **Smart Point Normalization:** * Points with missing or partially defined coordinates are now automatically normalized or cleaned. * **Null Value Cleanup:** To reduce payload size, points with no coordinates are stripped of unnecessary `null` properties. * Before: `{ label: "Point", x: null, y: null }` * After: `{ label: "Point" }` * **Partial Coordinate Support:** Points with only one axis defined will now explicitly maintain the missing axis as `null`. * Before: `{ label: "Point", x: 20 }` * After: `{ label: "Point", x: 20, y: null }` * **Validator Updates:** Refined `JoyDoc` Validator to mark `x` and `y` as optional chart properties, aligning the codebase with these new data standards. (NO-2116) * **Multi-Select PDF Layouts:** * Resolved a bug that caused excessive padding in multi-select fields during PDF generation. Exports now feature a tighter, more professional layout. (NO-2096) *** ## 4.0.0-rc17 **Release Date:** May 1, 2026 🛠️ **Bug Fixes** FIXED * **Copy Without Values Retains Block and Read-Only Field Values:** * Added support to ensure copying without values retains values for block fields and read-only fields. (NO-2093) * **Validator Duplicate Field Position Warnings:** * Validator will now emit console warnings when two field positions have the same `_id`. (NO-2110) * **Chart Field Blank Values:** * Fixed chart field coordinate handling so deleting an `x` or `y` value now stores `x: null` and `y: null` instead of coercing to `0`. (NO-2111) *** ## 4.0.0-rc16 **Release Date:** April 27, 2026 🛠️ **Bug Fixes** FIXED * **PDF Export with Duplicate Field Position IDs:** * Fixed a bug where PDF export would fail or render fields multiple times when two field positions shared the same `_id`. Duplicate field positions are now detected and skipped during export processing. (NO-2026) * **Chart Field Empty Points:** * Fixed a bug where chart fields were treating empty or incomplete data points as zeros, distorting the chart. Points with missing, null, or non-numeric `x`/`y` values are now ignored and excluded from rendering. (NO-1942) *** ## 4.0.0-rc15 **Release Date:** April 21, 2026 🚀 **New Features & Enhancements** ADDED * **Compact View for Table & Collection Fields:** * Table and Collection fields now support a compact view when `displayType` is set to `rte`. This renders rows in a streamlined, rich-text layout using column templates with merge codes, replacing the traditional grid format. (NO-2044) * Merge codes can be configured using the `{{columnId}}` syntax within RTE Editor in edit mode to dynamically resolve row data. 🛠️ **Bug Fixes** FIXED * **Mobile Disconnected View Conditions:** * Fixed a bug where conditional logic applied in the mobile disconnected view was incorrectly carried over to the desktop view. Conditions now apply only to their intended view. (NO-1985) *** ## 4.0.0-rc14 **Release Date:** March 26, 2026 🚀 **New Features & Enhancements** ADDED * **Per-page action controls:** * Page objects now support configuration properties that allow disabling deletion and/or duplication on a per-page basis; pages with disabled actions hide the corresponding buttons in the pagination UI. (NO-2008) * **Input Group Cell Text Wrapping:** * Added support for text wrapping in Input Group cells so long content wraps to fit across all the cells of the input group field when `textOverflow` is set to `wrap` on the cell settings. (NO-1988) *** ## 4.0.0-rc13 **Release Date:** March 11, 2026 🚀 **New Features & Enhancements** ADDED * **Single Field Validation:** * You can now validate individual fields on demand using the `validateField` function, returning a valid or invalid status with reasons, without triggering full-document validation. * Handles special cases for table fields (per-column row validation) and collection fields (nested table validation). (NO-1960) **Example: Validate a single field inside onChange** ```tsx theme={null} import { JoyDoc, validator } from '@joyfill/components'; { // Get the changed field ID from the first changelog entry const changedFieldId = changelogs[0]?._id; // Find the updated field in the doc const field = updatedDoc.fields.find((f) => f._id === changedFieldId); if (field) { const result = validator.validateField(field, 'desktop', updatedDoc.fields); if (result.status === 'invalid') { console.log('Field is invalid:', result.reasons); // e.g. ['Required'] } else { console.log('Field is valid'); } } }} /> ``` * **Decorators:** * Fields now support a `decorators` property, and table fields support a `rowDecorators` property — both are customizable action pills with an icon, label, color, and action identifier. * Up to 3 decorators render inline; 4 or more collapse into a popover menu. * Decorators are hidden in readonly and pdf modes and are only clickable in fill mode. (NO-1973) *** ## 4.0.0-RC12 **Release Date:** February 25, 2026 ### 🚀 New Features & Enhancements * **Conditional Logic on Columns:** You can now apply conditional logic to table columns, allowing columns to show or hide based on field values or conditions. (**NO-1919**) * **Force Hide By View:** Fields and table columns can now be hidden per view (desktop, mobile, PDF), giving you control over what appears in each view for cleaner, more tailored form layouts. (**NO-1913**) * **Filter Hidden Fields in Validation Output:** Fields that are hidden — whether by conditional logic, global settings, or view-specific hiding — are now excluded from validation results, ensuring validation only applies to visible fields. (**NO-1941**) ### 🛠️ Bug Fixes * **Formula Handling for Page Duplication:** Resolved an issue where formulas were not properly carried over when duplicating a page. Formulas are now cloned with updated field references so they continue to work correctly on the duplicated page. (**NO-1888**) * **Formula Saving:** Fixed a bug where formula changes could be lost when saving due to stale document state. The formula editor now correctly tracks and persists all pending changes. (**NO-1922**) ### ⚠️ Deprecation Notice * **Important:** We will be deprecating the use of the `hidden` property on `fieldPosition.tableColumns`. Going forward, the `hidden` property will be stored on `field` and `field.tableColumns`. The new `hiddenViews` property will also be stored on `field` and `field.tableColumns`. *** ## 4.0.0-RC11 **Release Date:** February 17, 2026 ### 🚀 New Features & Enhancements * **Navigator:** This release introduces the Navigator, establishing the foundation for enhanced cross-platform navigation and seamless integration. (**NO-1834**) * **Nested Table Styling:** You can now apply custom styles to nested tables within collection fields, allowing for better visual hierarchy and data readability. (**NO-1906**) * **Cleaner Read-Only Views:** To reduce UI clutter, the barcode icon is now hidden when table and collection fields are set to read-only mode. (**NO-1904**) ### 🛠️ Bug Fixes * **Validation Stability:** Resolved an issue where default helper functions were triggering incorrect validation errors. Logic checks should now behave as expected. (**NO-1828**) * **Collection Field Patch:** Resolved a bug that caused Collection fields to crash when opened or scrolled. (**NO-1921**) *** ## 4.0.0-RC10 **Release Date:** January 27, 2026 🚀 **Platform Update: Auto Resize Fields & React v19** This release introduces a new auto-resize feature that allows form fields to automatically adjust their height based on content, eliminating manual field sizing and improving the form-filling experience. Additionally, we have upgraded the core SDK to React v19. ✨ **Key Improvements** ADDED **Auto Resize Fields (Beta)** * Enable this beta feature via **`features={{ autoResizeFields: true }}`**. * Form fields can now automatically resize to fit their content, ensuring all information is visible without manual height adjustments. * Fields intelligently measure their content and adjust height on initialization and whenever content changes such as value updates, row creation, or row deletion. * **Important Note:** This feature should not be used with very complex or large forms containing large tables of data. This feature reduces the SDKs ability to optimize performance. Be aware that utilization of this feature could cause performance degrade. **Supported Field Types** * **Full Support:** Block, MultiSelect, Table, InputGroup, Image, File, and Chart fields fully support auto-resizing in all modes (edit, fill, readonly). * **PDF Generation:** Textarea fields support auto-resizing during PDF generation only. * **Enhanced Export:** PDF export fully supports auto-resizing, ensuring tables, input groups, and multi-select fields expand to display complete content without truncation. CHANGED **Internal Upgrades** * We have officially migrated the SDK core to React v19 to leverage the latest performance and stability improvements. * As a result, support for React v17 has been discontinued. The last stable SDK version compatible with React v17 is v3.16.3. 📥 **Recommendation** * To benefit from auto-resizing fields and the latest performance improvements, we recommend updating to the latest version and adding `autoResizeFields: true` to your features prop. *** ## 4.0.0-RC9 **Release Date:** January 15, 2026 🚀 **Platform Update: UI Consistency & Visual Refinements** This release introduces focused user interface improvements aimed at enhancing visual consistency and reducing layout shifts within form tables. ✨ **Key Improvements** FIXED * Improved Input Group Row Alignment — We addressed an issue where input group field rows could appear visually misaligned due to inconsistent cell heights. Baseline heights are now standardized across common field types, while fields with variable content expand naturally. Additional layout refinements ensure smoother row behavior and prevent unexpected shifts during interaction. 📥 **Recommendation** To benefit from these UI refinements and ensure the best visual experience, we recommend updating to the latest version. *** ## 4.0.0-RC8 **Release Date:** December 17, 2025 🚀 **Platform Update: Performance & UI Enhancements** This release introduces targeted stability improvements and user interface refinements designed to optimize form rendering and streamline data entry across all devices. ✨ **Key Improvements** FIXED * Optimized Image Field Constraints (JF-235 / NO-1727) — Addresses a layout regression where image fields exceeded container dimensions, ensuring proper scaling, visual consistency, and preventing overflow. * Enhanced Mobile Date Navigation (NO-1728) — Improves the mobile user experience by including a more efficient year-selection method in the date picker, reducing interactions for navigating across multiple years. **Recommendation** To ensure optimal platform stability and benefit from these UI enhancements, we recommend that all users update to the latest version. *** ## 4.0.0-rc7 **Release Date:** December 2, 2025 🚀 **Release Notes: Platform Enhancements** These release notes detail several important fixes and stability improvements across the platform, focusing on form rendering and application stability. ✨ **What's New & Improved** FIXED * Fixed Table Cell Sizing (NO-1626) — Resolved an issue where table cell sizes were not consistently maintained, ensuring a stable layout. * Resolved Form Field Duplication (NO-1709 & NO-1718) — Fixed a critical issue where text or displayText fields incorrectly shared the same ID, ensuring each form field instance now has a unique ID. * Enhanced Page Ordering Stability (NO-1721) — Fixed an application crash when users moved pages with inconsistent page orders between desktop and mobile views, improving reliability. **Recommendation:** All users should update to the latest version for these stability and form-rendering fixes. *** ## 4.0.0-rc6 **Release Date:** November 13, 2025 🚀 **Fixes & Enhancements** FIXED * Corrected page duplication behavior so that Readable IDs are no longer used when the feature is disabled. (NO-1629) * Added a clear button to the new Date display component, allowing users to remove selected dates easily. (NO-1664) * Fixed a Safari-specific issue that caused formulas to break during evaluation. (NO-1596) * Resolved a field ordering issue in mobile SDKs where fields with identical X/Y coordinates were rendered in the wrong order. (NO-1650) *** ## 4.0.0-rc5 **Release Date:** October 31, 2025 ADDED * New date format helper function `replaceLegacyDateTimeFormats(data, formatReplacementsObject)` to allow overwriting default US date and time formats within the Joyfill SDK. See the example below or check the README.md for more details. (NO-1622) CHANGED * Replaced Moment.js with Day.js — Reduces bundle size and provides better long-term maintenance support. (NO-1623) * Enhanced validation output — Form and field validation statuses now include the `pageId` property, making it easier to identify which page an error is on. You can view this in the output of the `validator.validate` helper function. (NO-1628) FIXED * PDF generation edge case — Resolved an issue where the field measuring cycle could run indefinitely for certain table field input types in the VanillaJS module. (NO-1647) REMOVED * Legacy RTE field — The old Rich Text Editor field is now view-only but continues to display all previously saved text. It can be replaced with the newer Block field, which removes a package vulnerability, reduces bundle size, and improves load performance. (NO-1648) *** ### Replacing Default US Date Formats The example below demonstrates how to use the new helper function to replace US date and time formats with EU-style formats. ```tsx theme={null} /** * Example: Replace US date/time formats with European formats in JoyDoc */ let updatedDoc = ; if (Intl.DateTimeFormat().resolvedOptions().timeZone === 'Europe/London') { updatedDoc = replaceLegacyDateTimeFormats(updatedDoc, { dateTime: "DD/MM/YYYY HH:mm", date: "DD/MM/YYYY", time: "HH:mm", defaultFormat: "DD/MM/YYYY" }); } console.log(JSON.stringify(updatedDoc, null, 2)); ``` *** ## 4.0.0-rc4 **Release Date:** October 16, 2025 ADDED * New date/time element for all date/time fields and columns. * Support for optional custom date/time formats to provide consistent behavior across time zones and browsers. * This update addresses long-standing inconsistencies between local time zones, browsers, and native date/time input behavior. (NO-1574) FIXED * Crash affecting tables on PDF Forms and PDF Forms rendered in Microsoft Edge. (NO-1543) * Issue where underlying table cell formulas were appearing in generated PDFs. These cells will now display as empty. (NO-1452) * Issue where chart field line point labels were duplicated, causing a blurring effect. (NO-1575) ## 🗓️ New Date/Time Element & Custom Formats — Details The native browser date/time input element was unreliable and inconsistent across browsers, especially with different local time zones. This often caused truncated or incorrectly formatted values in table cells and fields. To solve this, we’ve replaced the native input with a **custom date/time picker** and added the ability to define custom display formats using tokens like `MM`, `DD`, `YYYY`, etc. ### 📌 Visual Impact of Removing Native Browser Date/Time Inputs Previously, browsers attempted to “predict” the date/time format based on the user’s local settings (e.g., `DD/MM/YYYY` vs `MM/DD/YYYY`, or 12-hour vs 24-hour time). This prediction was **the root cause of formatting issues** and is no longer applied. Going forward, date/time formats will be strictly enforced based on either: * Existing saved formats (`MM/DD/YYYY`, `MM/DD/YYYY hh:mma`, `hh:mma`), or * Your **explicitly defined custom format**. If your workflow relied on the browser’s formatting, you’ll need to configure the desired format using the new **custom date format functionality**. (See below.) ### 🛠️ Custom Format Usage Custom formats are currently available **only on desktop (JS SDK)** and are **disabled by default**. To enable them, use the `customDateTimeFormat` field setting: ```tsx theme={null} // Field Settings { field: { customDateTimeFormat: true, // Enable for date/time field tableColumns: { customDateTimeFormat: true, // Enable for date/time columns ... }, ... }, ... } ``` Once enabled, a **Custom Format** input will appear in the field settings panel of date fields and date columns. ### 📱 Native Mobile SDKs > **Important:** If you’re using the native iOS, Kotlin, or React Native SDKs, it’s recommended to keep custom date formats **disabled** for now. > Custom formats are **not yet fully supported** across Joyfill’s native SDKs and may cause unintended behavior. Full support for custom date formats will be introduced in a future release, at which point this feature will be enabled by default across all platforms. *** ## 3.19.7 **Release Date:** October 6, 2025 FIXED * Maximum depth call exceeded crash when tables are added to PDF forms. *** ## 3.19.6 **Release Date:** June 13, 2025 We've rolled out a set of targeted improvements to ensure a more reliable and consistent experience across collection and table field interactions. Below is a summary of the recent updates: ADDED * Support for Hidden Table Columns — Hidden columns within collection fields are now fully supported and integrated, offering greater control over field layout without impacting functionality. (NO-1202 / JF-58) CHANGED * Conditional Logic Reliability — Misc enhancements to conditional logic making it consistent and more reliable. (NO-1187) * Block Column Type Configuration — Block column types within collection and table fields can no longer be marked as required, aligning with expected usage patterns and improving form design flexibility. (NO-1186) * Rich Text Editor in Fill Mode — Rich Text Editor (RTE) fields are now fully editable while in fill mode, enabling more seamless data entry without requiring additional steps. (NO-1180) FIXED * Hidden Columns and Validation — Resolved an issue where hidden columns in collection fields were incorrectly triggering validation errors. Hidden fields are now excluded from validation unless explicitly required. (JF-58 pt2) *** ## 3.19.5 **Release Date:** May 30, 2025 FIXED * Misc bug fixes. *** ## 3.19.4 **Release Date:** May 14, 2025 FIXED * Misc PDF bug fixes. *** ## 3.19.3 **Release Date:** May 9, 2025 FIXED * Misc bug fixes. *** ## 3.19.2 **Release Date:** May 5, 2025 FIXED * Misc bug fixes. *** ## 3.19.1 **Release Date:** May 4, 2025 FIXED * Misc bug fixes. *** ## 3.19.0 **Release Date:** April 30, 2025 ADDED * New enhancements to the Table field. CHANGED * Improved page breaking in PDF exports for dynamic fields. FIXED * Bug fixes and enhancements. *** ## 3.18.1 **Release Date:** March 4, 2024 FIXED * Misc bug fixes. *** ## 3.18.0 **Release Date:** February 27, 2025 ADDED * Tree Structure v1 (NO-689) — A powerful new way to organize hierarchical data with nested data representation, expandable/collapsible nodes, and improved performance for complex structures. FIXED * Image Upload Experience (NO-990) — Fixed various issues to improve the reliability and performance of image uploads. * Validator Fields Sorting (NO-985) — Validator fields now appear in the correct order based on their position on the page. *** ## 3.17.0 **Release Date:** February 14, 2025 We're excited to roll out the latest updates and improvements! CHANGED * Image Field Resizing (NO-946) — Miscellaneous improvements to ensure proper image scaling. * Display Text (Block) Enhancements (NO-838) — Improved behavior and display of block elements. * Android Upload Experience (NO-948) — Enhanced image/file upload functionality on Android devices. FIXED * Table Text Column Editing Bug (NO-940) — Resolved an issue with text column cells not editing properly. * Custom Icons (NO-970) — Custom icons now correctly render within field groups in the field list. * Multi-line Text in PDFs (NO-957) — Adjusted text measurement to prevent content from being cut off in PDF exports. * PDF Export Table (NO-949) — Fixed an issue where tables were cutting off bottom rows when spanning across page breaks. Enjoy the latest improvements, and as always, let us know if you have any feedback! *** ## 3.16.0 **Release Date:** January 7, 2025 ADDED * Full-width Min/Max inputs for chart field settings. (NO-814) * Signature Field V2 (NO-680) — Enhanced signature field with improved design and functionality for seamless user experience. CHANGED * Performance Enhancements for Very Large Forms (NO-783) — Optimized performance for handling large-scale forms efficiently. * Layout Engine Phase 1 (NO-578) — Improved PDF generation with better page-breaking for non-dynamic fields. FIXED * Duplicate Field Bug (NO-832) — Resolved an issue with duplicate fields in horizontal display type for MultiSelect. * Chart Coordinates (NO-764) — Addressed x and y label cut-off issues in chart coordinates. **Breaking Changes:** * JoyDocExporter no longer defaults to sans-serif font family. You are now required to set the fontFamily by using the theme property. See SDK README.md for how to properly set the theme font family. * JoyDocExporter now requires an initial measurement phase before PDF is ready to be captured. An element with `.pdf-capture-ready` will be added to the DOM when measuring is completed and PDF is ready to be captured. *** ## 3.15.1 **Release Date:** December 6, 2024 **Form Building, Now with Superpowers!** ADDED * Field Duplication (NO-662) — Clone fields faster with our new duplication feature. * Page Names In Page Navigation Bar (NO-657) — Page names now appear in the page navigation items. * Custom Field Option Icons (NO-817) — Customize your field options with custom icons using SVG Components and image urls. * New onSelect SDK Event (NO-762) — New onSelect handler to keep you in the loop every time a field is clicked. CHANGED * Formula Hide-and-Seek (NO-813) — Formulas on individual table cells fields properly handle formulas, staying hidden unless the field is focused. FIXED * Signature Distortion (NO-822) — Signatures now look flawless, no matter the size of the field. * Corrupted Table Data Crashing (NO-795) — Missing row\.cells values within table field causing an SDK crash has been patched. * Selection Fields Border (NO-761) — Checkbox and radio button borders are now visible at all times. We're always working to make your experience better (and maybe crack a smile or two along the way). Thanks for being part of this journey! *** ## 3.14.1 **Release Date:** October 23, 2024 ADDED * Ability to re-order pages within the builder (NO-656) * Confirmation modal for toggling mobile syncing (NO-653) * Validation helper method. See README helper methods to learn more. (NO-677) CHANGED * Updated peer dependencies to support React version >18.0.0 (NO-684) * Metadata field settings structure has been updated to follow consistent pattern (NO-684) FIXED * Multiple UI fixes and bug fixes (NO-686, NO-647, NO-650, NO-679, NO-639) *** ## 3.13.0 **Release Date:** August 9, 2024 ADDED * Validation helper (NO-579) * Support to sort fields based on x and y coordinates inside conditional logic modal (NO-643) FIXED * Bug that would make a field disappear when zIndex is set as the negative number (NO-644) * Bug that would cut off field settings due to accordion height (NO-639) * Filter out orphaned fields (ie fields without any field positions) from the Joydoc (NO-559) * Style bug that cut off the field settings as a whole (NO-614) *** ## 3.12.0 **Release Date:** July 26, 2024 CHANGED * Enhanced mobile device responsive web forms (NO-592) FIXED * Invalid JSON payload freezing SDK (NO-613) *** ## 3.11.0 **Release Date:** July 21, 2023 ADDED * `getDefaultPage` helper method (#NO-604) REMOVED * Vulnerable dependencies (#NO-604) *** ## 3.10.0 **Release Date:** July 10, 2024 ADDED * Loading indicator for image / file uploads (NO-298) — A new loading indicator lets you know exactly when your images and files are being uploaded. * Support for scaling pdf background images (NO-470) CHANGED * Updated JS SDK Internals (NO-528) * Enhanced table column settings (NO-352) *** ## 3.9.1 **Release Date:** May 13, 2024 ADDED * Transparent color support to the Settings Color Picker (NO-451) — Now you can create designs that are truly see-through. FIXED * Adjust height of field scroll selector (NO-522) — Prevents menu items of field identifiers from overflowing their wrapper. *** ## 3.9.0 **Release Date:** May 4, 2024 This update is so fly, it'll make your form builder blush. ADDED * File Upload Field enhancements (NO-498) — Multiple files support, drag-and-drop, customizable styles, colors, and layouts, and conditional logic support. * Enable/Disable Field Settings Options (NO-479) — You can hide any setting now by passing it as false in the field settings object. FIXED * Default values for table columns (NO-499) — Your default values will now show up properly. * Identifier Filter not clearing post search (NO-492) — Your search results will no longer be haunted by the ghosts of past searches. * Page loading bug (NO-484) — Solved the mysterious loading indicator that haunted the Form builder after deleting the current page. * "Exports is Undefined" (NO-494) — Fixed the missing "Exports" issue. *** ## 3.8.0 **Release Date:** April 2, 2024 Bug Squashing Bonanza! ADDED * Identifier Enhancements (NO-469) — You can now enable or disable custom identifier creation. * Field margin control for a polished look (NO-433) * Default page padding for a consistent design (NO-433) * Read-only mode to make any field read-only by the read-only option on field settings (NO-433) FIXED * Bug where the option width couldn't be removed from multi-select fields (NO-459) * Bug that caused the SDK to crash when encountering invalid JoyDoc payloads (NO-432) * Identifier filter results not clearing after removing search query (NO-469) *** ## 3.7.1 **Release Date:** March 11, 2024 ADDED * Developer-friendly SDK for PDF generation (NO-355) * SDK Themes to elevate your forms (NO-396) CHANGED * Tidied up some errors for a sparkling console (NO-398) * Improved performance for a smoother experience (NO-450) * Revamped SDK for a stunning experience (NO-58) *** ## 3.6.1 **Release Date:** February 19, 2024 FIXED * Crashes after Loading External Form (NO-395) *** ## 3.6.0 **Release Date:** February 6, 2024 ADDED * Support for original file dimensions in PDF Form Engine (NO-349) * Text wrapping for text based fields (NO-307) * Field icon added to field settings (NO-315) FIXED * Display corresponding field settings of individual table cells when focused (NO-310) * Field wrapping doesn't cut off text within pdf and pdf exports (NO-107) * Hidden property causing validation issue in older SDK version (NO-377) *** ## 3.5.0 **Release Date:** December 16, 2023 ADDED * Conditional logic (#NO-288) * `onFocus` and `onBlur` event handlers now supported in SDK (#NO-223) * Support for `initialPageId` on SDK for setting the initial page when the form is displayed (#NO-297) CHANGED * Enhancements to PDF exports (#NO-250) * Enhancements to responsive web view for mobile form filling (#NO-252) * PDF form view enhancements (#NO-279) * Enhancements to required field indicators (#NO-295) FIXED * Style fix for Identifier selector (#NO-278) **Breaking Changes:** * Improved vanilla JS implementation for JS modules and CDN approach. See new implementation methods below (#NO-277) **Javascript Usage (CDN)** ``` Joyfill for Javascript Example
``` **Javascript Usage (Module)** ``` //index.html Joyfill for Javascript Example
//index.js import Joyfill from "@joyfill/components/dist/joyfill.min.js"; Joyfill.JoyDoc( document.getElementById('joyfill'), { mode: 'edit', onChange: (changelogs, doc) => console.log('onChange: ', changelogs, doc), } ); ``` *** ## 3.4.2 **Release Date:** October 19, 2023 FIXED * Chart field label read-only error in build. * Misc enhancements and bug fixes. *** ## 3.4.1 **Release Date:** October 18, 2023 ADDED * Identifier selector field customization. Learn more [https://docs.joyfill.io/docs/customize-settings](https://docs.joyfill.io/docs/customize-settings) (NO-87) CHANGED * PDF dynamic field measurement enhancements (NO-113) * Auto remove individual table cell field positions associated with deleted columns (NO-254) FIXED * Misc enhancements and bug fixes. *** ## 3.3.1 **Release Date:** October 4, 2023 ADDED * Page duplication support with page menu and `duplicatePage` helper method (#NO-95) * Table field now supports image uploading directly to table image cells (#NO-114) * Field tooltips now supported for additional field context and instructions (#NO-130) FIXED * Misc enhancements and bug fixes. *** ## 3.2.0 **Release Date:** September 12, 2023 ADDED * Joyfill template library UI and visual requirements support (#NO-17) * Field groups display mode for table fields (#NO-17) * Rich text field support (#NO-17) CHANGED * Table columns are responsive by default now (#NO-17) * Multi image upload adds to current images and no longer overwrites them (#NO-149) * Table field settings enhancements (#NO-163) FIXED * Signature is properly scaled to fit within field dimensions (#NO-172) *** ## 3.1.0 **Release Date:** August 22, 2023 ADDED * Multi-user collaboration support via changelogs. * Changelogs are now passed directly to the `onChange` handler. See package README. * Helper methods for generating and duplicating documents and templates: `getDefaultDocument`, `getDefaultTemplate`, `getDocumentFromTemplate`, `duplicate`. See package README. * Field lazy loading. See package README. **Breaking Changes:** * `onChange` argument list has been changed. `onChange` now only receives two arguments: `changelogs` and `doc`. See README. # Getting started Source: https://docs.joyfill.io/web/getting-started/Getting-Started ## Getting Started with Joyfill Components ## Overview The Joyfill Components SDK is a powerful React library for creating dynamic, interactive forms and documents. This guide will help you get started with installing and using the SDK in your React application. ## Installation ### Prerequisites * Node.js 18.20.0 or higher * React 18.3.1 or higher * A package manager (npm, yarn, or pnpm) ### Install the SDK ```bash theme={null} # Using npm npm install @joyfill/components # Using yarn yarn add @joyfill/components # Using pnpm pnpm add @joyfill/components ``` ## Quick Start ### Basic Usage ```jsx theme={null} import React, { useState } from 'react'; import { JoyDoc } from '@joyfill/components'; function App() { const [document, setDocument] = useState({ _id: 'my-document', identifier: 'my-document', name: 'My First Form', files: [{ _id: 'file1', name: 'Main File', pages: [{ _id: 'page1', name: 'Page 1', fieldPositions: [] }] }], fields: [] }); const handleChange = (changelogs, updatedDoc) => { console.log('Document changed:', changelogs); setDocument(updatedDoc); }; return (
); } export default App; ``` ### Loading your first form Here is a simple Joydoc that has a text field in it: ```jsx theme={null} import React, { useState } from 'react'; import { JoyDoc, getDefaultDocument } from '@joyfill/components'; function MyForm() { const [document, setDocument] = useState(() => { // Create a default document const doc = getDefaultDocument(); // Add a text field doc.fields.push({ _id: 'name-field', identifier: 'name', type: 'text', title: 'Your Name', value: '', file: doc.files[0]._id }); // Add field position for layout doc.files[0].pages[0].fieldPositions.push({ _id: 'name-position', field: 'name-field', x: 0, y: 0, width: 1, height: 1, displayType: 'original' }); return doc; }); const handleChange = (changelogs, updatedDoc) => { console.log('Form changed:', changelogs); setDocument(updatedDoc); }; const handleSubmit = () => { // Extract form data const formData = {}; document.fields.forEach(field => { formData[field.identifier] = field.value; }); console.log('Form data:', formData); alert(`Hello, ${formData.name || 'Anonymous'}!`); }; return (

My First Joyfill Form

); } export default MyForm; ``` # Listening for changes Source: https://docs.joyfill.io/web/getting-started/Listening-for-changes ## Overview The Joyfill SDK provides comprehensive event handling to track all user interactions and document changes. This guide covers how to listen for and handle various types of form changes. ## Basic Change Handling ### onChange Event The primary way to listen for form changes is through the `onChange` callback: ```jsx theme={null} import React, { useState } from 'react'; import { JoyDoc } from '@joyfill/components'; function MyForm() { const [document, setDocument] = useState(initialDocument); const handleChange = (changelogs, updatedDoc) => { console.log('Document changed:', changelogs); console.log('Updated document:', updatedDoc); // Process each changelog changelogs.forEach(changelog => { console.log('Change type:', changelog.target); console.log('Field ID:', changelog.fieldId); console.log('Change data:', changelog.change); console.log('Timestamp:', changelog.createdOn); }); // Update document state setDocument(updatedDoc); }; return ( ); } ``` ### Changelog Structure Each changelog contains detailed information about the change: ```jsx theme={null} { sdk: 'js', // SDK identifier v: 1, // Version number target: 'field.update', // Change type _id: 'document-id', // Document ID identifier: 'document-identifier', // Document identifier fieldId: 'field-id', // Field ID (for field-related changes) fieldIdentifier: 'field-identifier', // Field identifier change: { value: 'new value' }, // Change data createdOn: 1640995200000, // Timestamp formula: true // Formula-driven change (optional) } ``` ## Event Types ### Field Events ### Field Updates ```jsx theme={null} const handleChange = (changelogs, updatedDoc) => { // Filter field updates const fieldUpdates = changelogs.filter(changelog => changelog.target === 'field.update' ); if (fieldUpdates.length > 0) { console.log('Fields updated:', fieldUpdates); // Process each field update fieldUpdates.forEach(update => { console.log(`Field ${update.fieldIdentifier} changed to:`, update.change.value); }); } setDocument(updatedDoc); }; ``` ### Field Creation ```jsx theme={null} const handleChange = (changelogs, updatedDoc) => { // Filter field creation const fieldCreates = changelogs.filter(changelog => changelog.target === 'field.create' ); if (fieldCreates.length > 0) { console.log('Fields created:', fieldCreates); // Track new fields fieldCreates.forEach(create => { console.log(`New field created: ${create.fieldIdentifier}`); }); } setDocument(updatedDoc); }; ``` ### Field Deletion ```jsx theme={null} const handleChange = (changelogs, updatedDoc) => { // Filter field deletion const fieldDeletes = changelogs.filter(changelog => changelog.target === 'field.delete' ); if (fieldDeletes.length > 0) { console.log('Fields deleted:', fieldDeletes); // Track deleted fields fieldDeletes.forEach(del => { console.log(`Field deleted: ${del.fieldIdentifier}`); }); } setDocument(updatedDoc); }; ``` ### Page Events ### Page Creation ```jsx theme={null} const handleChange = (changelogs, updatedDoc) => { // Filter page creation const pageCreates = changelogs.filter(changelog => changelog.target === 'page.create' ); if (pageCreates.length > 0) { console.log('Pages created:', pageCreates); } setDocument(updatedDoc); }; ``` ### Page Updates ```jsx theme={null} const handleChange = (changelogs, updatedDoc) => { // Filter page updates const pageUpdates = changelogs.filter(changelog => changelog.target === 'page.update' ); if (pageUpdates.length > 0) { console.log('Pages updated:', pageUpdates); } setDocument(updatedDoc); }; ``` ### Table Row Events ### Row Creation ```jsx theme={null} const handleChange = (changelogs, updatedDoc) => { // Filter table row creation const rowCreates = changelogs.filter(changelog => changelog.target === 'field.value.rowCreate' ); if (rowCreates.length > 0) { console.log('Table rows created:', rowCreates); } setDocument(updatedDoc); }; ``` ### Row Updates ```jsx theme={null} const handleChange = (changelogs, updatedDoc) => { // Filter table row updates const rowUpdates = changelogs.filter(changelog => changelog.target === 'field.value.rowUpdate' ); if (rowUpdates.length > 0) { console.log('Table rows updated:', rowUpdates); } setDocument(updatedDoc); }; ``` # 🎯 Advanced Event Handling Guide ## Overview The Joyfill SDK provides comprehensive event handling to track all user interactions and document changes. This guide covers all available event handlers and their corresponding changelog structures for advanced form management. ## Event Handler Types The Joyfill SDK provides the following public event handlers: * `onFocus` - Field focus events * `onBlur` - Field blur events * `onChange` - Document changes (field, fieldposition, page, style changes) * `onCaptureAsync` - Barcode capture events * `onUploadAsync` - File upload events (images) * `onFileUploadAsync` - File upload events * `onFileClick` - File click events * `onFileDelete` - File deletion events ## Event Parameters All event handlers receive structured parameters with the following common properties: | Name | Type | Description | | --------------- | ------ | --------------------------------------------------------------------------------- | | v | Number | Changelog version number | | sdk | String | Specifies the name of the SDK that generated the changelog object | | target | String | Specifies the target change that was made | | \_id | String | Specifies the target document \_id for the change | | identifier | String | Specifies the target document identifier for the change | | fileId | String | Specifies the target file \_id for the change | | pageId | String | Specifies the target page \_id for the change | | fieldId | String | Specifies the target field \_id for the change | | fieldIdentifier | String | Specifies the target field identifier for the change | | fieldPositionId | String | Specifies the target field position \_id for the change | | change | Object | Object containing the properties and values that should be applied for the change | | createdOn | Number | Millisecond timestamp of the change event | ## Focus and Blur Events ### `onFocus` Triggered when a field receives focus. **Event Parameters:** * `params` (Object): Focus context information * `event` (Event): The DOM focus event (call `event.blur()` to blur the currently focused field) **Changelog Structure:** ```jsx theme={null} { sdk: 'js', type: 'fieldPosition.focus', v: 1, _id: documentId, identifier: documentIdentifier, fileId: fileId, pageId: pageId, fieldId: fieldId, fieldPositionId: fieldPositionId, fieldIdentifier: fieldIdentifier } ``` **Usage Example:** ```jsx theme={null} function MyForm() { const handleFocus = (params, event) => { const { sdk, type, v, _id, identifier, fileId, pageId, fieldId, fieldPositionId, fieldIdentifier, } = params; console.log('Field focused:', { sdk, type, v, _id, identifier, fileId, pageId, fieldId, fieldPositionId, fieldIdentifier, }); // Auto-blur after 3 seconds setTimeout(() => { console.log('blurring the field', event); if (event && event.blur) event.blur(); }, 3000); }; return ( ); } ``` ### `onBlur` Triggered when a field loses focus. **Event Parameters:** * `params` (Object): Blur context information * `event` (Event): The DOM blur event **Changelog Structure:** ```jsx theme={null} { sdk: 'js', v: 1, target: 'fieldPosition.blur', _id: documentId, identifier: documentIdentifier, fileId: fileId, pageId: pageId, fieldId: fieldId, fieldPositionId: fieldPositionId, fieldIdentifier: fieldIdentifier, } ``` **Usage Example:** ```jsx theme={null} function MyForm() { const handleBlur = (params, event) => { const { sdk, type, v, _id, identifier, fileId, pageId, fieldId, fieldPositionId, fieldIdentifier, } = params; console.log('Field blurred:', { sdk, type, v, _id, identifier, fileId, pageId, fieldId, fieldPositionId, fieldIdentifier, }); }; return ( ); } ``` ## Change Events ### `onChange` The primary event handler for all document changes. Triggered for: * **Field changes** - Field value or property updates * **Field position changes** - Field position, size, or display changes * **Page changes** - Page creation, updates, or deletion * **Style changes** - Theme and styling modifications **Event Parameters:** * `changelogs` (Array): Array of changelog objects describing the changes * `updatedDoc` (Object): The updated document state **Field Change Changelog:** ```jsx theme={null} { sdk: 'js', v: 1, target: 'field.update', _id: documentId, identifier: documentIdentifier, fileId: fileId, pageId: pageId, fieldId: fieldId, fieldIdentifier: fieldIdentifier, fieldPositionId: fieldPositionId, change: { value: 'new value', properties: { /* field properties */ } }, createdOn: 1640995200000, formula: false } ``` **Field Position Change Changelog:** ```jsx theme={null} { sdk: 'js', v: 1, target: 'fieldPosition.update', _id: documentId, identifier: documentIdentifier, fileId: fileId, pageId: pageId, fieldId: fieldId, fieldPositionId: fieldPositionId, change: { x: 100, y: 200, width: 200, height: 30, _id: fieldPositionId }, createdOn: 1640995200000 } ``` **Page Change Changelog:** ```jsx theme={null} { sdk: 'js', v: 1, target: 'page.update', _id: documentId, identifier: documentIdentifier, fileId: fileId, pageId: pageId, change: { name: 'Updated Page Name', properties: { /* page properties */ } }, createdOn: 1640995200000 } ``` **Style/Theme Change Changelog:** ```jsx theme={null} { sdk: 'js', v: 1, target: 'style.update', _id: documentId, identifier: documentIdentifier, fileId: fileId, change: { theme: 'dark', // or 'light' styles: { /* style properties */ } }, createdOn: 1640995200000 } ``` **Usage Example:** ```jsx theme={null} function MyForm() { const [document, setDocument] = useState(initialDocument); const handleChange = (changelogs, updatedDoc) => { console.log('Document changed:', changelogs); // Process each changelog changelogs.forEach(changelog => { console.log('Change type:', changelog.target); console.log('Field ID:', changelog.fieldId); console.log('Change data:', changelog.change); }); // Update document state setDocument(updatedDoc); }; return ( ); } ``` ## Capture Events ### `onCaptureAsync` - Barcode Capture Triggered when barcode capture is initiated (e.g., table barcode cell, barcode field). **Function Signature:** ```jsx theme={null} onCaptureAsync() => Promise ``` **Description:** * Called without any parameters when user clicks the barcode capture button * Expected to return a Promise that resolves to the captured barcode value (string) * The returned value will be automatically populated into the barcode field **Usage Example:** ```jsx theme={null} import { JoyDoc } from "@joyfill/components"; function MyForm() { const handleCaptureAsync = async () => { console.log('Barcode capture initiated'); // Example: Open camera/scanner and return barcode value // This is where you would integrate with your barcode scanning library const barcodeValue = await scanBarcode(); // Your barcode scanning logic return barcodeValue; // Return the captured barcode string }; return ( ); } ``` ## Upload Events ### `onUploadAsync` Triggered when file upload is initiated for image fields, both in normal fields and table field cells. **Note:** This function will be called only when `onFileUploadAsync` is not passed into JoyDoc. If `onFileUploadAsync` is present, it will be triggered instead of this. **Function Signature:** ```jsx theme={null} onUploadAsync(params, files) => Promise> ``` **Parameters:** * `params` (Object): Upload context information * `files` (Array\): Array of File objects selected by the user **Params Structure:** **Normal Field Upload:** ```jsx theme={null} { "target": "field.update", "_id": "68ef74d29e5f80ece781b8ed", "identifier": "my-document", "fileId": "file1", "pageId": "page1", "fieldId": "68ef7569f4642997bd016fec", "fieldIdentifier": "field_68ef7569f4642997bd016fec", "fieldPositionId": "68ef75696b31bf638f74c074", "multi": true } ``` **Table Field Upload:** ```jsx theme={null} { "target": "field.update", "_id": "68ef74d29e5f80ece781b8ed", "identifier": "my-document", "fileId": "file1", "pageId": "page1", "fieldId": "68ef7569f4642997bd016fec", "fieldIdentifier": "field_68ef7569f4642997bd016fec", "fieldPositionId": "68ef75696b31bf638f74c074", "rowId": "row_12345", "columnId": "column_67890", "multi": true } ``` **Files Structure:** ```jsx theme={null} [ { "path": "./IMG_4176.jpg", "relativePath": "./IMG_4176.jpg", "lastModified": 1636782919000, "lastModifiedDate": "Sat Nov 13 2021 05:55:19 GMT+0000 (Greenwich Mean Time)", "name": "IMG_4176.jpg", "size": 1024000, "type": "image/jpeg", "webkitRelativePath": "" } ] ``` **Usage Example:** ```jsx theme={null} import { useState } from "react"; import { JoyDoc } from "@joyfill/components"; function MyForm() { const [document, setDocument] = useState(initialDocument); const handleUploadAsync = async (params, fileUploads) => { console.log("onUploadAsync: ", params, fileUploads); const resultPromises = await fileUploads.map(async (fileUpload) => { console.log("files uploaded"); const dataUri = await getDataUriForFileUpload(fileUpload); return uploadFileAsync(params.identifier, dataUri); }); return Promise.all(resultPromises) .then((responses) => { const finalResponse = Array.isArray(responses[0]) ? responses[0] : responses; return finalResponse; }) .catch((error) => { if (error) return; }); }; const handleChange = (changelogs, updatedDoc) => { console.log("Document changed:", changelogs); // Process each changelog changelogs.forEach((changelog) => { if (changelog.target === "field.update") { console.log("Field updated:", changelog.fieldId); console.log("Multi upload:", changelog.multi); // Check if this was a table field upload if (changelog.rowId && changelog.columnId) { console.log("Table field upload:", { fieldId: changelog.fieldId, rowId: changelog.rowId, columnId: changelog.columnId, }); } else if (changelog.multi === true) { console.log( "Multiple images uploaded to normal field:", changelog.fieldId ); } } }); // Update document state setDocument(updatedDoc); }; return ( ); } // Helper functions const getDataUriForFileUpload = async (file) => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = reject; reader.readAsDataURL(file); }); }; const uploadFileAsync = async (identifier, dataUri, tableContext = null) => { const uploadData = { identifier, dataUri, ...(tableContext && { rowId: tableContext.rowId, columnId: tableContext.columnId, }), }; const response = await fetch("/api/upload", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(uploadData), }); const result = await response.json(); return { _id: `upload-${Date.now()}`, url: result.url, fileName: result.fileName, filePath: result.filePath, }; }; ``` ### `onFileUploadAsync` * onFileUploadAsync is an async callback in `fieldSettings.field` that handles file uploads for specific fields. It overrides the global onUploadAsync when provided. **Function Signature:** ```jsx theme={null} onFileUploadAsync(params, files) => Promise> ``` **Parameters:** * `params` (Object): Upload context information * `files` (Array\): Array of File objects selected by the user **Usage Example:** ```jsx theme={null} import { useState } from "react"; import { JoyDoc } from "@joyfill-components"; function MyForm() { const [document, setDocument] = useState(initialDocument); const handleFileUploadAsync = async (params, fileUploads) => { console.log("onFileUploadAsync params:", params); console.log("onFileUploadAsync files:", fileUploads); const uploadPromises = fileUploads.map(async (file) => { const dataUri = await getDataUriForFileUpload(file); return uploadFileAsync(params.identifier, dataUri); }); const results = await Promise.all(uploadPromises); return results; }; const handleChange = (changelogs, updatedDoc) => { console.log("Document changed:", changelogs); // Process each changelog changelogs.forEach((changelog) => { if (changelog.target === "field.update") { console.log("Field updated:", changelog.fieldId); // Check if this was a table field upload if (changelog.rowId && changelog.columnId) { console.log("Table field upload completed:", { fieldId: changelog.fieldId, rowId: changelog.rowId, columnId: changelog.columnId, multi: changelog.multi, }); } else { console.log("Normal field upload completed:", { fieldId: changelog.fieldId, multi: changelog.multi, }); } } }); setDocument(updatedDoc); }; const fieldSettings = { field: { onFileUploadAsync: async (params, fileUploads) => { console.log("onFileUploadAsync: ", params, fileUploads); return { _id: new Date().getTime(), url: "sampleImageUrl", }; }, }, }; return ( ); } ``` ## File Interaction Events ### `onFileClick` Triggered when a file is clicked or selected in an image field. **Function Signature:** ```jsx theme={null} onFileClick(params, urlObject) => Promise ``` **Parameters:** * `params` (Object): Context information about the file click * `urlObject` (Object): Information about the clicked file **Params Object:** ```jsx theme={null} { "_id": "68ef74d29e5f80ece781b8ed", "identifier": "my-document", "fileId": "file1", "pageId": "page1", "fieldId": "68ef81699ee8e13ed8dcd8e3", "fieldIdentifier": "field_68ef81699ee8e13ed8dcd8e3", "fieldPositionId": "68ef8169fb605e9648b562f1" } ``` **URL Object:** ```jsx theme={null} { "_id": "68ef8fb9874dbbcfd1189f88", "url": "", "fileName": "68ef8fb38f7d1d3e8348f894-1760530355509.jpg", "filePath": "68a47e5d32dddce3ee2c31a5/documents/my-document" } ``` **Usage Example:** ```jsx theme={null} function MyForm() { const handleFileClick = async (params, urlObject) => { console.log("File clicked:", { params, urlObject, }); const { fieldId, fieldIdentifier } = params; const { url, fileName, _id: fileId } = urlObject; console.log("File details:", { fieldId, fieldIdentifier, fileId, fileName, url, }); // Open file in new tab window.open(url, "_blank"); // Or handle file action based on file type const fileExtension = fileName.split(".").pop().toLowerCase(); if (["jpg", "jpeg", "png", "gif"].includes(fileExtension)) { // Handle image files console.log("Image file clicked:", fileName); } else if (["pdf"].includes(fileExtension)) { // Handle PDF files console.log("PDF file clicked:", fileName); } }; const fieldSettings = { field: { onFileClick: async (params, urlObject) => { console.log("onFileClick: ", params, urlObject); }, }, }; return ; } ``` ### `onFileDelete` Triggered when a file is deleted from an image field. **Function Signature:** ```jsx theme={null} onFileDelete(params, urlObject) => Promise ``` **Parameters:** * `params` (Object): Context information about the file deletion * `urlObject` (Object): Information about the file being deleted **Usage Example:** ```jsx theme={null} function MyForm() { const handleFileDelete = async (params, urlObject) => { console.log("File delete requested:", { params, urlObject, }); const { fieldId, fieldIdentifier } = params; const { url, fileName, _id: fileId } = urlObject; console.log("File to be deleted:", { fieldId, fieldIdentifier, fileId, fileName, url, }); // Confirm deletion const confirmed = confirm(`Are you sure you want to delete "${fileName}"?`); if (confirmed) { console.log("File deletion confirmed by user"); // File deletion is handled by the SDK // This handler is called before the actual deletion } else { console.log("File deletion cancelled by user"); // Prevent deletion (if the SDK supports it) } }; const fieldSettings = { field: { onFileDelete: async (params, urlObject) => { console.log("onFileDelete: ", params, urlObject); }, }, }; return ; } ``` ## Advanced Change Processing ### Filtering Changes by Type ```jsx theme={null} const handleChange = (changelogs, updatedDoc) => { // Filter by change type const fieldChanges = changelogs.filter(c => c.target.startsWith('field.')); const pageChanges = changelogs.filter(c => c.target.startsWith('page.')); const fileChanges = changelogs.filter(c => c.target.startsWith('file.')); const styleChanges = changelogs.filter(c => c.target.startsWith('style.')); console.log('Field changes:', fieldChanges); console.log('Page changes:', pageChanges); console.log('File changes:', fileChanges); console.log('Style changes:', styleChanges); setDocument(updatedDoc); }; ``` ## Real-World Examples ### Form Validation ```jsx theme={null} import { useState } from "react"; import { JoyDoc, validator } from "@joyfill-components"; function MyForm() { const [document, setDocument] = useState(initialDocument); const [errors, setErrors] = useState({}); /** * Validates a field using the JoyDoc validator * * @param {string} fieldId - The ID of the field to validate * @param {any} fieldValue - The value to validate * @param {Object} document - The JoyDoc document containing field definitions * @returns {Object} Validation result with isValid (boolean) and message (string) */ const validateField = (fieldId, fieldValue, document) => { // Find the field in the document const field = document?.fields?.find((f) => f._id === fieldId); // If field not found, return invalid if (!field) { return { isValid: false, message: `Field with ID ${fieldId} not found`, }; } // Create a field object with the updated value for validation const fieldToValidate = { ...field, value: fieldValue, }; // Use the JoyDoc validator to validate the field const validationResult = validator.validateField(fieldToValidate); // Convert the validation result to the expected format if (validationResult.status === "invalid") { return { isValid: false, message: field.title ? `${field.title} is required` : "This field is required", }; } return { isValid: true, message: "", }; }; const handleChange = (changelogs, updatedDoc) => { // Process changes changelogs.forEach((changelog) => { if (changelog.target === "field.update") { // Validate field const fieldId = changelog.fieldId; const fieldValue = changelog.change.value; // Perform validation const validationResult = validateField(fieldId, fieldValue); if (!validationResult.isValid) { setErrors((prev) => ({ ...prev, [fieldId]: validationResult.message, })); } else { setErrors((prev) => { const newErrors = { ...prev }; delete newErrors[fieldId]; return newErrors; }); } } }); setDocument(updatedDoc); }; return (
{Object.keys(errors).length > 0 && (
{Object.entries(errors).map(([fieldId, message]) => (
{message}
))}
)}
); } ``` ### Auto-Save ```jsx theme={null} import { useState, useCallback, useRef } from "react"; import { JoyDoc, validator } from "@builttocreate/joyfill-components"; import debounce from "lodash.debounce"; function MyForm() { const [document, setDocument] = useState(initialDocument); const [lastSaved, setLastSaved] = useState(null); const [isSaving, setIsSaving] = useState(false); // Create a save function const saveDocument = async (docToSave) => { try { setIsSaving(true); // Replace this with your actual save API call // Example: await fetch('/api/documents', { method: 'POST', body: JSON.stringify(docToSave) }); console.log("Saving document:", docToSave); // Simulate API call await new Promise((resolve) => setTimeout(resolve, 500)); setLastSaved(new Date()); setIsSaving(false); } catch (error) { console.error("Error saving document:", error); setIsSaving(false); } }; // Create a debounced save function using useRef to persist across renders const debouncedSave = useRef( debounce((docToSave) => { saveDocument(docToSave); }, 1000) ).current; const handleChange = useCallback( (changelogs, updatedDoc) => { console.log("Document changed:", changelogs); // Update document immediately setDocument(updatedDoc); // Trigger debounced auto-save debouncedSave(updatedDoc); }, [debouncedSave] ); return (
{isSaving && Saving...} {lastSaved && !isSaving && ( Last saved: {lastSaved.toLocaleTimeString()} )}
); } export default MyForm; ``` ### Analytics Tracking ```jsx theme={null} import { JoyDoc } from "@joyfill/components"; function MyForm() { const [document, setDocument] = useState(initialDocument); const handleChange = (changelogs, updatedDoc) => { // Track user interactions changelogs.forEach(changelog => { analytics.track('document_change', { changeType: changelog.target, fieldId: changelog.fieldId, fieldType: getFieldType(changelog.fieldId), timestamp: changelog.createdOn }); }); setDocument(updatedDoc); }; return ( ); } ``` ### Undo/Redo Functionality ```jsx theme={null} import { JoyDoc } from "@joyfill/components"; function MyForm() { const [document, setDocument] = useState(initialDocument); const [history, setHistory] = useState([]); const [historyIndex, setHistoryIndex] = useState(-1); const handleChange = (changelogs, updatedDoc) => { // Add to history const newHistory = history.slice(0, historyIndex + 1); newHistory.push({ changelogs, document: updatedDoc, timestamp: Date.now() }); setHistory(newHistory); setHistoryIndex(newHistory.length - 1); setDocument(updatedDoc); }; const undo = () => { if (historyIndex > 0) { const previousState = history[historyIndex - 1]; setDocument(previousState.document); setHistoryIndex(historyIndex - 1); } }; const redo = () => { if (historyIndex < history.length - 1) { const nextState = history[historyIndex + 1]; setDocument(nextState.document); setHistoryIndex(historyIndex + 1); } }; return (
); } ``` ## Best Practices ### 1. Efficient Change Processing ```jsx theme={null} const handleChange = useCallback((changelogs, updatedDoc) => { // Only process necessary changes const importantChanges = changelogs.filter(c => c.target === 'field.update' || c.target === 'field.create' ); if (importantChanges.length > 0) { setDocument(updatedDoc); } }, []); ``` ### 2. Error Handling ```jsx theme={null} const handleChange = (changelogs, updatedDoc) => { try { // Process changes changelogs.forEach(changelog => { if (changelog.target === 'field.update') { validateFieldChange(changelog); } }); setDocument(updatedDoc); } catch (error) { console.error('Error processing changes:', error); // Handle error gracefully } }; ``` ## Troubleshooting ### Common Issues ### 1. onChange Not Firing **Problem:** The onChange callback isn't being called. **Solution:** Ensure the callback is properly provided: ```jsx theme={null} ``` ### 2. Event Handlers Not Working **Problem:** Event handlers like onFocus, onBlur, etc. are not being triggered. **Solution:** Check that the handlers are properly defined and passed to JoyDoc: ```jsx theme={null} const fieldSettings = { field:{ onFileUploadAsync: async (params, fileUploads) => { console.log('onFileUploadAsync: ', params, fileUploads); return { _id: new Date().getTime(), url: 'sample fileUrl' }; }, onFileClick: async (params, urlObject) => { console.log('onFileClick: ', params, urlObject); }, onFileDelete: async (params, urlObject) => { console.log('onFileDelete: ', params, urlObject); }, } } ``` This comprehensive guide covers all available event handlers in the Joyfill SDK, providing developers with the complete information needed to implement robust form handling and user interaction tracking. # Conditional Logic Source: https://docs.joyfill.io/web/guides/conditional-logic Conditional logic allows you to show or hide fields and pages based on the values of other fields. This enables dynamic forms that adapt based on user input, creating a more intuitive and streamlined user experience. ## **Overview** Conditional logic in JoyDoc uses a rule-based system where you define conditions that determine when fields or pages should be shown or hidden. The logic is evaluated in real-time as users interact with the form, automatically showing or hiding elements based on the current field values. ## **How Conditional Logic Works** Conditional logic consists of three main components: 1. **Action** - What should happen when conditions are met (`show` or `hide`) 2. **Evaluation** - How conditions should be combined (`and` or `or`) 3. **Conditions** - The actual rules to check (field values, comparisons, etc.) ## **Logic Structure** ### **Basic Structure** ```jsx theme={null} { action: 'show' | 'hide', // Action to take when conditions match eval: 'and' | 'or', // How to combine conditions conditions: [ // Array of conditions to check { file: 'file_id', // File ID containing the field page: 'page_id', // Page ID containing the field field: 'field_id', // Field ID to check condition: '=', // Comparison operator value: 'some_value' // Value to compare against } ] } ``` ### **Actions** The `action` property determines what happens when the conditions are met: * **`show`** - Show the field/page when conditions match (field/page starts hidden) * **`hide`** - Hide the field/page when conditions match (field/page starts visible) ### **Evaluation Types** The `eval` property determines how multiple conditions are combined: * **`and`** - All conditions must be true for the action to trigger * **`or`** - Any condition must be true for the action to trigger ### **Condition Operators** The `condition` property supports different comparison operators depending on the field type: ### **Text and Textarea Fields** * `*=` - **is filled** - Field has any value * `null=` - **is empty** - Field is empty or null * `=` - **is** - Field value equals the specified value * `!=` - **is not** - Field value does not equal the specified value * `?=` - **contains** - Field value contains the specified substring (case-insensitive) ### **Number Fields** * `*=` - **is filled** - Field has a numeric value * `null=` - **is empty** - Field is empty or null * `=` - **is** - Field value equals the specified number * `!=` - **is not** - Field value does not equal the specified number * `>` - **is greater than** - Field value is greater than the specified number * `<` - **is less than** - Field value is less than the specified number ### **Dropdown and MultiSelect Fields** * `*=` - **is filled** - Field has at least one option selected * `null=` - **is empty** - Field has no options selected * `=` - **is** - Field value equals the specified option * `!=` - **is not** - Field value does not equal the specified option ## **Field-Level Conditional Logic** Fields can have conditional logic applied to show or hide themselves based on other field values. ### **Example: Show Field When Another Field Has Value** ```jsx theme={null} { _id: 'conditional_field', type: 'text', title: 'Additional Information', value: '', hidden: true, // Start hidden logic: { action: 'show', eval: 'and', conditions: [ { file: 'file1', page: 'page1', field: 'main_field', condition: '*=', // is filled value: null // Not needed for "is filled" } ] } } ``` ### **Example: Hide Field Based on Number Comparison** ```jsx theme={null} { _id: 'discount_field', type: 'number', title: 'Discount Amount', value: 0, logic: { action: 'hide', eval: 'and', conditions: [ { file: 'file1', page: 'page1', field: 'total_amount', condition: '<', value: 100 } ] } } ``` ### **Example: Multiple Conditions with AND** ```jsx theme={null} { _id: 'premium_field', type: 'text', title: 'Premium Feature', value: '', hidden: true, logic: { action: 'show', eval: 'and', // All conditions must be true conditions: [ { file: 'file1', page: 'page1', field: 'user_type', condition: '=', value: 'premium' }, { file: 'file1', page: 'page1', field: 'account_status', condition: '=', value: 'active' } ] } } ``` ### **Example: Multiple Conditions with OR** ```jsx theme={null} { _id: 'notification_field', type: 'text', title: 'Notification', value: '', hidden: true, logic: { action: 'show', eval: 'or', // Any condition can be true conditions: [ { file: 'file1', page: 'page1', field: 'priority', condition: '=', value: 'high' }, { file: 'file1', page: 'page1', field: 'status', condition: '=', value: 'urgent' } ] } } ``` ### **Example: Text Contains Condition** ```jsx theme={null} { _id: 'follow_up_field', type: 'textarea', title: 'Follow-up Details', value: '', hidden: true, logic: { action: 'show', eval: 'and', conditions: [ { file: 'file1', page: 'page1', field: 'main_description', condition: '?=', // contains value: 'issue' // Case-insensitive substring match } ] } } ``` ## **Page-Level Conditional Logic** Pages can also have conditional logic applied to show or hide entire pages based on field values. ### **Example: Show Page When Condition Met** ```jsx theme={null} { _id: 'conditional_page', name: 'Additional Details', hidden: true, // Start hidden logic: { action: 'show', eval: 'and', conditions: [ { file: 'file1', page: 'page1', // Reference to the page with the condition field field: 'show_details', condition: '=', value: 'yes' } ] }, fieldPositions: [ // ... field positions ] } ``` ### **Example: Hide Page Based on Number Range** ```jsx theme={null} { _id: 'summary_page', name: 'Summary', logic: { action: 'hide', eval: 'and', conditions: [ { file: 'file1', page: 'page1', field: 'total_score', condition: '<', value: 50 } ] }, fieldPositions: [ // ... field positions ] } ``` ## **Supported Field Types** Conditional logic can reference the following field types in conditions: * **Text** (`text`) - Supports all text operators * **Textarea** (`textarea`) - Supports all text operators * **Number** (`number`) - Supports all number operators * **Dropdown** (`dropdown`) - Supports dropdown operators * **MultiSelect** (`multiSelect`) - Supports dropdown operators **Note:** Fields used in conditions must be on a different page than the field/page being controlled (you cannot reference a field from the same page). ## **Complete Example** ```jsx theme={null} { fields: [ { _id: 'customer_type', type: 'dropdown', title: 'Customer Type', value: '', options: [ { _id: 'opt1', value: 'individual' }, { _id: 'opt2', value: 'business' } ] }, { _id: 'business_name', type: 'text', title: 'Business Name', value: '', hidden: true, // Hidden by default logic: { action: 'show', eval: 'and', conditions: [ { file: 'file1', page: 'page1', field: 'customer_type', condition: '=', value: 'business' } ] } }, { _id: 'individual_ssn', type: 'text', title: 'Social Security Number', value: '', hidden: true, // Hidden by default logic: { action: 'show', eval: 'and', conditions: [ { file: 'file1', page: 'page1', field: 'customer_type', condition: '=', value: 'individual' } ] } }, { _id: 'total_amount', type: 'number', title: 'Total Amount', value: 0 }, { _id: 'discount_field', type: 'number', title: 'Discount', value: 0, logic: { action: 'hide', eval: 'and', conditions: [ { file: 'file1', page: 'page1', field: 'total_amount', condition: '<', value: 1000 } ] } } ], files: [ { _id: 'file1', pages: [ { _id: 'page1', name: 'Customer Information', fieldPositions: [ // ... field positions ] }, { _id: 'page2', name: 'Additional Details', hidden: true, logic: { action: 'show', eval: 'and', conditions: [ { file: 'file1', page: 'page1', field: 'total_amount', condition: '>', value: 5000 } ] }, fieldPositions: [ // ... field positions ] } ] } ] } ``` ## **Best Practices** 1. **Start with Hidden State** - When using `show` action, set `hidden: true` initially so the field/page starts hidden 2. **Use Specific Field IDs** - Always reference fields by their `_id` property 3. **Cross-Page References** - Conditions must reference fields from different pages 4. **Test Edge Cases** - Test with empty values, null values, and boundary conditions 5. **Keep Conditions Simple** - Complex logic with many conditions can be harder to maintain 6. **Use AND for Required Conditions** - Use `and` when all conditions must be met 7. **Use OR for Optional Conditions** - Use `or` when any condition can trigger the action ## **Common Patterns** ### **Pattern 1: Show/Hide Based on Dropdown Selection** ```jsx theme={null} // Show field when dropdown equals specific value logic: { action: 'show', eval: 'and', conditions: [ { file: 'file1', page: 'page1', field: 'dropdown_field', condition: '=', value: 'selected_option' } ] } ``` ### **Pattern 2: Show/Hide Based on Text Contains** ```jsx theme={null} // Show field when text contains specific substring logic: { action: 'show', eval: 'and', conditions: [ { file: 'file1', page: 'page1', field: 'text_field', condition: '?=', value: 'keyword' } ] } ``` ### **Pattern 3: Show/Hide Based on Number Range** ```jsx theme={null} // Show field when number is greater than threshold logic: { action: 'show', eval: 'and', conditions: [ { file: 'file1', page: 'page1', field: 'number_field', condition: '>', value: 100 } ] } ``` ### **Pattern 4: Multiple Required Conditions** ```jsx theme={null} // Show field when multiple conditions are all true logic: { action: 'show', eval: 'and', conditions: [ { file: 'file1', page: 'page1', field: 'field1', condition: '=', value: 'value1' }, { file: 'file1', page: 'page1', field: 'field2', condition: '>', value: 50 } ] } ``` ### **Pattern 5: Any Condition Can Trigger** ```jsx theme={null} // Show field when any condition is true logic: { action: 'show', eval: 'or', conditions: [ { file: 'file1', page: 'page1', field: 'status', condition: '=', value: 'pending' }, { file: 'file1', page: 'page1', field: 'priority', condition: '=', value: 'high' } ] } ``` ## **Troubleshooting** ### **Field Not Showing/Hiding** 1. **Check `hidden` property** - Ensure `hidden: true` when using `show` action 2. **Verify field references** - Ensure field IDs in conditions match actual field IDs 3. **Check page references** - Ensure page IDs are correct 4. **Verify condition syntax** - Check that condition operators match field types 5. **Test condition values** - Ensure comparison values match actual field values ### **Condition Not Working** 1. **Field type compatibility** - Verify the condition operator is supported for the field type 2. **Value matching** - For dropdown/multiSelect, ensure the value matches exactly 3. **Empty vs null** - Check if field is empty (`null=`) or filled (`*=`) 4. **Case sensitivity** - Text contains (`?=`) is case-insensitive, but equals (`=`) is case-sensitive ### Bonus Tip: * Checkout the github repo here - [https://github.com/joyfill/conditional-logic](https://github.com/joyfill/conditional-logic) # Customize Draggable Fields Source: https://docs.joyfill.io/web/guides/customize-draggable-fields Customize the draggable field options for field creation in the left panel ## Overview You can define custom draggable field options for your users. This allows you to preconfigure fields with styles, settings, and any other property supported by the JoyDoc Spec. When your user drags and drops one of your pre-configured fields onto the page, that field will have the properties pre-set when the field is created. You can use any of the JoyDoc JSON supported properties on your list of custom fieldOptions. ## Examples ```jsx React theme={null} const fieldOptions = [...] return ( ) ``` ```js JavaScript theme={null} const fieldOptions = [...] return ( ) ``` ## Customize You can customize the list of available draggable options within the Joyfill Builder by providing your own JS Objects in the list. For instance, in the example below we define some pre-configured styles (fontColor, fontWeight, and fontSize) and an identifier to be used when the field is created. When a user drags and drops your custom pre-configured fields onto the page it will have these properties set by default. ```js theme={null} const fieldOptions = [ { title: 'Customer Name', type: 'text', displayType: 'original', identifier: 'custom_customer_name', fontColor: '#0096FF', fontWeight: 'bold', }, { title: 'Customer Signature', type: 'signature', displayType: 'original', identifier: 'custom_customer_signature', fontSize: 22 }, ... ] ``` ## Group Fields You can group field options into a collapsable accordion by using the fieldGroup type in the field option list and nesting your field objects under the fields property of the field group. This is a great tool for organizing the field options list, especially if you have a lot of custom fields. ```js theme={null} const fieldOptions = [ { type: 'fieldGroup', title: 'Custom Fields', open: false, fields: [ { title: 'Grouped Text', type: 'text', displayType: 'original', }, { title: 'Grouped Multiline Text', type: 'textarea', displayType: 'original', }, ... ], }, ... ] ``` ## Joyfill Default Draggable Fields Below is the default list of JS Objects used by Joyfill internally to define our standard list of draggable field options. You can add to this list, customize it, and do whatever you need to provide the proper experience to your customers. In the example below, the `generateObjectId` method is provided by the Joyfill Components Module as an export. You can use it to generate a valid `_id`. See guide [Generating Object Ids](/web/guides/generating-object-ids). You can also pass custom icons to field options. The custom icons can either be an image url or svg. The `iconType` property must be specified as `url` incase of an imageUrl whereas it should be specified as `custom` incase of a custom svg ```js theme={null} const fieldOptions = [ { title: 'Image', type: 'image', displayType: 'original', }, { title: 'File', type: 'file', displayType: 'original', }, { title: 'Heading Text', type: 'block', displayType: 'original', value: 'Heading', fontSize: 28, fontWeight: 'bold', }, { title: 'Display Text', type: 'block', displayType: 'original', value: 'Display text', }, { title: 'Empty Space', type: 'block', displayType: 'original', borderColor: 'transparent', backgroundColor: 'transparent', }, { title: 'Text', type: 'text', displayType: 'original', }, { title: 'Multiline Text', type: 'textarea', displayType: 'original', }, { title: 'Number', type: 'number', displayType: 'original', }, { title: 'Date Time', type: 'date', displayType: 'original', format: 'MM/DD/YYYY hh:mma' }, { title: 'Dropdown', type: 'dropdown', displayType: 'original', options: [ { _id: generateObjectId(), value: 'Yes' }, { _id: generateObjectId(), value: 'No' }, { _id: generateObjectId(), value: 'N/A' }, ], }, { title: 'Multiple Choice', type: 'multiSelect', multi: true, displayType: 'original', options: [ { _id: generateObjectId(), value: 'Yes' }, { _id: generateObjectId(), value: 'No' }, { _id: generateObjectId(), value: 'N/A' }, ], }, { title: 'Single Choice', type: 'multiSelect', multi: false, displayType: 'original', options: [ { _id: generateObjectId(), value: 'Yes' }, { _id: generateObjectId(), value: 'No' }, { _id: generateObjectId(), value: 'N/A' }, ], }, { title: 'Signature', type: 'signature', displayType: 'original', }, { title: 'Table', type: 'table', displayType: 'original', tableColumns: [ { _id: generateObjectId(), type: 'text', title: 'Text Column', }, { _id: generateObjectId(), type: 'dropdown', title: 'Dropdown Column', options: [ { _id: generateObjectId(), value: 'Yes' }, { _id: generateObjectId(), value: 'No' }, { _id: generateObjectId(), value: 'N/A' }, ], }, { _id: generateObjectId(), type: 'image', title: 'Image Column', maxImageWidth: 190, maxImageHeight: 120, }, ], value: [ { _id: generateObjectId(), cells: {} }, { _id: generateObjectId(), cells: {} }, { _id: generateObjectId(), cells: {} }, ], }, { title: 'Chart', type: 'chart', displayType: 'original', primaryDisplayOnly: true, yTitle: 'Vertical', yMax: 100, yMin: 0, xTitle: 'Horizontal', xMax: 100, xMin: 0, }, { title: 'Custom Icon SVG', type: FieldTypes.text, iconType: "custom", icon: ( ), displayType: 'original', }, { title: 'Custom Icon Url', type: FieldTypes.text, iconType: "url", icon: '', displayType: 'original', } ]; ``` # Decorators Source: https://docs.joyfill.io/web/guides/decorators Decorators are clickable indicators (icon + label) attached to a **field**, **column**, **row**, or **cell**. Clicks are delivered through **`onFocus`** so your app can run custom logic — navigation, uploads, help modals, etc. ## Decorator model | Property | Type | Notes | | -------- | --------- | -------------------------------------------------------- | | `action` | `string` | **Required**. Non-empty. Unique within its scope (path). | | `icon` | `string?` | See [Supported icons](#supported-icons). | | `label` | `string?` | Text label. | | `color` | `string?` | 6-digit hex (`#RRGGBB`). | A decorator renders only when it has a non-empty `icon` **or** `label`. Action-only entries are stored but not displayed. ## Setup Import `DecoratorManager` from `@joyfill/components` and pass an instance to `` via the `decoratorManager` prop: ```tsx theme={null} import { JoyDoc, DecoratorManager } from '@joyfill/components'; const decoratorManager = new DecoratorManager(); ``` All four API methods are called directly on the `decoratorManager` instance. ## Constructing a path Every path starts with `pageId/fieldPositionId`. What you append after that determines what gets decorated. > **Reserved keywords.** The path grammar uses three reserved tokens — **`schemas`**, **`rows`**, and **`columns`**. Anything else in a path slot is treated as an id (page id, field-position id, row id, column id, or schema key). Don't use these keywords as ids. ### Field decorators Just the two ids. Applies to the field's header. ```ts theme={null} const fieldPath = `${pageId}/${fieldPositionId}`; ``` ### Table — `/rows`, `/columns/colId`, or specific `rowId` / `rowId/colId` A table has four decorator scopes, two **common** (defaults applied everywhere) and two **specific** (overrides for one row or cell): | What you want | Path suffix | Example | | ----------------------------------------------- | ------------------ | -------------------------------- | | Common decorators on **every row** | `/rows` | `pageId/fpId/rows` | | Decorators on **one specific row** | `/{rowId}` | `pageId/fpId/row_42` | | Common decorators on **every cell in a column** | `/columns/{colId}` | `pageId/fpId/columns/col_status` | | Decorators on **one specific cell** | `/{rowId}/{colId}` | `pageId/fpId/row_42/col_status` | Specific paths inherit from the matching common path on the first write — anything you set on `/rows` shows on `row_42` until you write to `row_42` directly. ### Collection — same as table, plus `/schemas/schemaKey/…` for nested rows A collection's **root** rows behave like a table — the four scopes above use the exact same path shapes. Take a "People" collection where each person row holds a nested "Addresses" schema: ``` schema "people" (root, children: [addresses]) schema "addresses" (nested under people) Rows: p_alice ← row in "people" addresses → [ addr_home, addr_work ] ← rows in "addresses", under p_alice p_bob addresses → [ addr_apt ] ``` **Common rows / columns** of any schema — root or nested — are schema-level defaults: | What you want | Path shape | Example | | ---------------------------- | -------------------------------------- | ----------------------------------------------- | | Common rows in any schema | `pageId/fpId/schemas/sk/rows` | `pageId/fpId/schemas/addresses/rows` | | Common columns in any schema | `pageId/fpId/schemas/sk/columns/colId` | `pageId/fpId/schemas/addresses/columns/col_zip` | **A specific nested row or cell** lives under a particular parent. Walk through that parent's row id, then `schemas/sk/`, then the nested row id: | What you want | Path shape | Example | | -------------------- | -------------------------------------- | --------------------------------------------------------- | | Specific nested row | `…/rowId/schemas/sk/nestedRowId` | `pageId/fpId/p_alice/schemas/addresses/addr_home` | | Specific nested cell | `…/rowId/schemas/sk/nestedRowId/colId` | `pageId/fpId/p_alice/schemas/addresses/addr_home/col_zip` | > **Schema keys** come from the field's `schema` map. The schema marked `root: true` holds top-level rows; its `children` array names the nested schemas reachable from a row in this schema. ## API Four methods on `DecoratorManager`: ```ts theme={null} const path = `${pageId}/${fieldPositionId}`; decoratorManager.getDecorators(path); // → Decorator[] decoratorManager.addDecorators(path, [ { action: 'help', icon: 'circle-info', color: '#3B82F6' } ]); decoratorManager.updateDecorator(path, 'help', { action: 'help', icon: 'star', color: '#3B82F6' }); decoratorManager.removeDecorator(path, 'help'); ``` The same four methods work for every path scope. A few examples: ```ts theme={null} // Common row decorators on a table — applied to every row decoratorManager.addDecorators(`${pageId}/${fpId}/rows`, [ { action: 'duplicate', icon: 'copy', label: 'Duplicate' } ]); // Override on a specific row decoratorManager.addDecorators(`${pageId}/${fpId}/${rowId}`, [ { action: 'archive', icon: 'folder', color: '#6B7280' } ]); // Cell-specific decorator decoratorManager.addDecorators(`${pageId}/${fpId}/${rowId}/${colId}`, [ { action: 'upload', icon: 'paperclip', label: 'Attach' } ]); // Nested collection row const nestedPath = `${pageId}/${fpId}/${parentRowId}/schemas/${nestedSchemaKey}/${nestedRowId}`; decoratorManager.addDecorators(nestedPath, [ { action: 'comment', icon: 'comment', color: '#10B981' } ]); ``` ## Handling clicks Decorator clicks come through **`onFocus`**. When `params.type` is non-empty (and not `'fieldPositionFocus'`), the focus event is a decorator tap — the value of `params.type` is the decorator's `action`. Use `params.fieldRowId` and `params.fieldColumnId` to know exactly where the user clicked. ```tsx theme={null} { if (params.type && params.type !== 'fieldPositionFocus') { // Decorator tap console.log('Decorator action:', params.type); console.log('Field position:', params.fieldPositionId); console.log('Row:', params.fieldRowId); console.log('Column:', params.fieldColumnId); } else { // Ordinary field focus } }} /> ``` See [Event handling](/web/guides/event-handling) for the full `onFocus` parameter reference. ## Behavior to know * **Collection license gating.** Writes against a collection field require a license that enables collection features. Without it, the call emits `decoratorError` and is rejected. * **Decorator visibility in PDFs.** Decorators are hidden in `readonly` and `pdf` modes and are only clickable in `fill` mode. ## Errors All four APIs report errors through the `onError` event handler: * Path didn't resolve (bad ids, deleted row, malformed grammar) * Validation (`action` empty, `color` not `#RRGGBB`) * Duplicate `action` in batch or against an existing entry * `removeDecorator` / `updateDecorator` with an unknown `action` * Collection write without a valid license Reads (`getDecorators`) on an unresolvable path also emit `onError` and return `[]`. ## Display limits `DecoratorConfig`, passed to `` via the `decoratorManager`, controls how many decorators render inline before the rest collapse into a kebab menu. ```tsx theme={null} const decoratorManager = new DecoratorManager({ visibleLimitInFields: 2, // field + column scopes; default 2 visibleLimitInRows: 1 // row scopes; default 1 }); ``` ## Supported icons The SDK supports the following named icons: `camera`, `import`, `paperclip`, `image`, `file`, `comment`, `comments`, `upload`, `download`, `rotate`, `cloud`, `filter`, `share`, `paper-plane`, `folder`, `folder-open`, `magnet`, `eye`, `circle-info`, `add`, `plus`, `print`, `flag`, `pencil`, `pen-to-square`. Unknown names fall back to a default icon. # Event Handling Source: https://docs.joyfill.io/web/guides/event-handling This document provides comprehensive information about all available events in JoyDoc and how to handle them. ## Event Overview Table | Event Name | Event Triggers | Purpose | | ------------------- | -------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | | `onFocus` | When a field receives focus | Track field focus events for analytics, validation, or UI updates | | `onChange` | When document data changes (field changes, field position changes, page changes, style changes, theme changes) | Handle document updates, save changes, or sync with external systems | | `onBlur` | When a field loses focus | Track field blur events for validation or cleanup operations | | `onUploadAsync` | When images or files are uploaded | Handle file uploads to external storage or processing services | | `onCaptureAsync` | When barcode is captured for table barcode cells | Handle camera capture functionality for barcode fields | | `onFileUploadAsync` | When images or files are uploaded | Handle field-specific file uploads with custom logic | | `onFileClick` | When a file in a field is clicked | Handle file interactions like opening, previewing, or downloading | | `onFileDelete` | When a file in a field is deleted | Handle file deletion with cleanup or confirmation logic | | `onError` | When errors occur (schema validation, license, etc.) | Handle and display errors to users | ## Event Details ### onFocus * **Triggered when**: A field receives focus * **Purpose**: Track field focus events for analytics, validation, or UI updates **Event signature:** ```jsx theme={null} onFocus(params, e) => { // params contains field and document information // e is the native focus event } ``` **Parameters:** * `params`: Object containing: * `sdk`: 'js' * `v`: 1 * `type`: 'fieldPositionFocus' * `_id`: Document ID * `identifier`: Document identifier * `fileId`: File ID * `pageId`: Page ID * `fieldId`: Field ID * `fieldIdentifier`: Field identifier * `fieldPositionId`: Field position ID * `fieldRowId`: Row ID (for table fields) * `fieldColumnId`: Column ID (for table fields) * `fieldColumnIdentifier`: Column identifier (for table fields) * `e`: Native DOM focus event **Example:** ```jsx theme={null} { console.log('Field focused:', params.fieldId); // Call e.blur() to blur the field programmatically setTimeout(() => e.blur(), 3000); }} /> ``` ### onChange * **Triggered when**: Document data changes * **Purpose**: Handle document updates, save changes, or sync with external systems **Triggered for:** * **Field changes** - Field value or property updates * **Field position changes** - Field position, size, or display changes * **Page changes** - Page creation, updates, or deletion * **Style changes** - Theme and styling modifications * **Theme changes** - Toggle between themes All these will cause changes to the document data **Event signature:** ```jsx theme={null} onChange(changelog, doc) => { // changelog contains the changes made // doc contains the updated document } ``` **Parameters:** * `changelog`: Array of change objects describing what was modified * `doc`: The updated document object **Example:** ```jsx theme={null} { console.log('Document changed:', changelog); console.log('Updated document:', doc); // Save changes to server saveDocument(doc); // Update local state setDocument(doc); }} /> ``` ### onBlur * **Triggered when**: A field loses focus * **Purpose**: Track field blur events for validation or cleanup operations **Event signature:** ```jsx theme={null} onBlur(params, e) => { // params contains field and document information // e is the native blur event } ``` **Parameters:** * `params`: Object containing: * `sdk`: 'js' * `v`: 1 * `target`: 'fieldPositionBlur' * `_id`: Document ID * `identifier`: Document identifier * `fileId`: File ID * `pageId`: Page ID * `fieldId`: Field ID * `fieldIdentifier`: Field identifier * `fieldPositionId`: Field position ID * `fieldRowId`: Row ID (for table fields) * `fieldColumnId`: Column ID (for table fields) * `fieldColumnIdentifier`: Column identifier (for table fields) * `e`: Native DOM blur event **Example:** ```jsx theme={null} { console.log('Field blurred:', params.fieldId); // Validate field on blur validateField(params.fieldId); }} /> ``` ### onUploadAsync * **Triggered when**: Images or files are uploaded * **Purpose**: Handle file uploads to external storage or processing services **Event signature:** ```jsx theme={null} onUploadAsync(params, fileUploads) => { // params contains upload context // fileUploads contains the files to upload // Must return array of file objects with url, fileName, filePath } ``` **Parameters:** * `params`: Object containing upload context and document information * `fileUploads`: Array of file objects to upload **Return value:** Array of file objects with: * `url`: Public URL of uploaded file * `fileName`: Name of the file * `filePath`: Path to the file **Example:** ```jsx theme={null} { console.log('Uploading files:', fileUploads); const uploadPromises = fileUploads.map(async (file) => { // Upload to your storage service const uploadedFile = await uploadToS3(file); return { url: uploadedFile.url, fileName: uploadedFile.fileName, filePath: uploadedFile.filePath }; }); return await Promise.all(uploadPromises); }} /> ``` ### onCaptureAsync * **Triggered when**: Barcode is captured for table barcode cells * **Purpose**: Handle camera capture functionality for barcode fields **Event signature:** ```jsx theme={null} onCaptureAsync(params) => { // params contains capture context // Must return URL string of captured barcode } ``` **Parameters:** * `params`: Object containing capture context **Return value:** String data of the captured barcode **Example:** ```jsx theme={null} { console.log('Barcode capture initiated:', params); // Open camera and capture barcode const barcodeData = await captureBarcode(); return barcodeData; }} /> ``` ### onFileUploadAsync * **Triggered when**: Images or files are uploaded * **Purpose**: Handle field-specific file uploads with custom logic **Event signature:** ```jsx theme={null} onFileUploadAsync(params, fileUploads) => { // params contains field and upload context // fileUploads contains the files to upload // Must return array of file objects with url, fileName, filePath } ``` **Parameters:** * `params`: Object containing field information and upload context * `fileUploads`: Array of file objects to upload **Return value:** Array of file objects with: * `url`: Public URL of uploaded file * `fileName`: Name of the file * `filePath`: Path to the file **Example:** ```jsx theme={null} // Configure in fieldSettings const fieldSettings = { field: { onFileUploadAsync: async (params, fileUploads) => { console.log('Field-specific upload:', params.fieldId, fileUploads); // Custom upload logic for this specific field const uploadPromises = fileUploads.map(async (file) => { const uploadedFile = await customUploadService(file, params.fieldId); return { url: uploadedFile.url, fileName: uploadedFile.fileName, filePath: uploadedFile.filePath }; }); return await Promise.all(uploadPromises); } } }; ``` ### onFileClick * **Triggered when**: A file in a field is clicked * **Purpose**: Handle file interactions like opening, previewing, or downloading **Event signature:** ```jsx theme={null} onFileClick(params, urlObject) => { // params contains field and file context // urlObject contains file information } ``` **Parameters:** * `params`: Object containing: * `fileId`: File ID * `pageId`: Page ID * `fieldId`: Field ID * `fieldIdentifier`: Field identifier * `fieldPositionId`: Field position ID * `urlObject`: Object containing: * `url`: File URL * `fileName`: File name * `filePath`: File path **Example:** ```jsx theme={null} // Configure in fieldSettings const fieldSettings = { field: { onFileClick: (params, urlObject) => { console.log('File clicked:', urlObject); // Open file in new tab window.open(urlObject.url, '_blank'); } } }; ``` ### onFileDelete * **Triggered when**: A file in a field is deleted * **Purpose**: Handle file deletion with cleanup or confirmation logic **Event signature:** ```jsx theme={null} onFileDelete(params, urlObject) => { // params contains field and file context // urlObject contains file information } ``` **Parameters:** * `params`: Object containing: * `fileId`: File ID * `pageId`: Page ID * `fieldId`: Field ID * `fieldIdentifier`: Field identifier * `fieldPositionId`: Field position ID * `urlObject`: Object containing: * `url`: File URL * `fileName`: File name * `filePath`: File path **Example:** ```jsx theme={null} // Configure in fieldSettings const fieldSettings = { field: { onFileDelete: async (params, urlObject) => { console.log('File deleted:', urlObject); // Show confirmation dialog const confirmed = await showConfirmDialog( `Are you sure you want to delete ${urlObject.fileName}?` ); if (confirmed) { // Delete from storage service await deleteFromStorage(urlObject.filePath); } return confirmed; } } }; ``` ### onError * **Triggered when**: Errors occur (schema validation, license validation, etc.) * **Purpose**: Handle and display errors to users **Event signature:** ```jsx theme={null} onError(error) => { // error contains error information } ``` **Parameters:** * `error`: Object containing error information: * For schema validation errors: Validation error details * For license errors: `{ code: 'LICENSE_VALIDATION_ERROR', message: string, type: 'license' }` **Example:** ```jsx theme={null} { console.error('JoyDoc Error:', error); if (error.type === 'license') { showLicenseError(error.message); } else { showGenericError('An error occurred. Please try again.'); } }} /> ``` ## Complete Example Here's a complete example showing how to use all events: ```jsx theme={null} import React, { useState } from 'react'; import JoyDoc from './JoyDoc'; const MyJoyDocApp = () => { const [doc, setDoc] = useState(initialDoc); const [isLoading, setIsLoading] = useState(false); const fieldSettings = { field: { onFileUploadAsync: async (params, fileUploads) => { setIsLoading(true); try { const uploadPromises = fileUploads.map(async (file) => { const uploadedFile = await uploadToS3(file); return { url: uploadedFile.url, fileName: uploadedFile.fileName, filePath: uploadedFile.filePath }; }); return await Promise.all(uploadPromises); } finally { setIsLoading(false); } }, onFileClick: (params, urlObject) => { window.open(urlObject.url, '_blank'); }, onFileDelete: async (params, urlObject) => { const confirmed = await showConfirmDialog( `Delete ${urlObject.fileName}?` ); if (confirmed) { await deleteFromStorage(urlObject.filePath); } return confirmed; } } }; return (
{isLoading &&
Uploading...
} { console.log('Document changed:', changelog); setDoc(updatedDoc); saveDocument(updatedDoc); }} onFocus={(params, e) => { console.log('Field focused:', params.fieldId); analytics.track('field_focused', params); }} onBlur={(params, e) => { console.log('Field blurred:', params.fieldId); validateField(params.fieldId); }} onUploadAsync={async (params, fileUploads) => { const uploadPromises = fileUploads.map(async (file) => { const uploadedFile = await uploadToS3(file); return { url: uploadedFile.url, fileName: uploadedFile.fileName, filePath: uploadedFile.filePath }; }); return await Promise.all(uploadPromises); }} onCaptureAsync={async (params) => { const barcodeData = await captureBarcode(); return barcodeData; }} onError={(error) => { console.error('JoyDoc Error:', error); showError(error.message || 'An error occurred'); }} />
); }; ``` ## Best Practices 1. **Async Event Handlers**: Always use `async/await` for upload and capture events 2. **Error Handling**: Implement proper error handling in all async event handlers 3. **Loading States**: Show loading indicators during file uploads 4. **Validation**: Use `onBlur` for field validation 5. **Analytics**: Use `onFocus` and `onChange` for user interaction tracking 6. **File Management**: Implement proper cleanup in `onFileDelete` handlers 7. **Performance**: Debounce `onChange` events if needed for frequent updates # Navigation Source: https://docs.joyfill.io/web/guides/external-navigation This document describes how to programmatically navigate to pages and fields within a form using the `goto` API on `Navigator`. ## Overview The `goto` method enables programmatic navigation to specific locations within a form. This is useful for: * Guiding users to required fields after validation * Implementing custom navigation flows * Deep linking to specific form sections * Auto-scrolling to specific fields Instantiate `Navigator` and pass it to the `JoyDoc` component via the `navigator` prop. Then call `navigator.goto(path)` with a path string whenever you want to navigate. ```jsx theme={null} import { JoyDoc, Navigator } from '@joyfill/components'; const navigator = new Navigator(); // Later, from your code: navigator.goto('page_123'); navigator.goto('page_123/fieldPosition_456'); ``` ## Path-Based Navigation Navigate using a slash-separated path string. ```javascript theme={null} // Navigate to a page navigator.goto("page_123") // Navigate to a field on a page (with auto-scroll) navigator.goto("page_123/fieldPosition_456") ``` | Path Format | Description | | -------------------------- | -------------------------------------------- | | `"pageId"` | Navigate to the top of the specified page | | `"pageId/fieldPositionId"` | Navigate to the page and scroll to the field | **Important:** Use `fieldPositionId` from `page.fieldPositions[]._id`, not `fieldId` from `document.fields[]._id`. Using the wrong ID will cause navigation to fail. ## NavResponse The `goto` method returns an object indicating success or failure. ```javascript theme={null} const result = navigator.goto("page_123/fieldPosition_456") if (result.status === "success") { // Navigation succeeded - target exists and is visible } else { // Navigation failed - target doesn't exist, is hidden, or unsupported } ``` | Response | Description | | ----------- | ---------------------------------------------------------------------- | | `"success"` | The target exists, is supported, and is visible | | `"failure"` | The target does not exist, is hidden, or has an unsupported field type | **Failure reasons include:** * Page does not exist * Page is hidden (due to conditional logic) * Field position does not exist * Field is hidden (due to conditional logic) * Field type is unsupported (e.g. `file`, `unknown`) * Navigating to a hidden field: the page still switches and scrolls to the top of the page, but `status` is `"failure"` ## Page Change Events Page changes triggered by `goto()` or by the user clicking a page in the UI are emitted through the `onBlur` and `onFocus` callbacks (the **navigation listener**). When the active page changes, the SDK fires a page event so you can track which page is focused. Check the `type` property on the event to distinguish page events from field focus/blur events: ```javascript theme={null} { if (params.type === 'page.focus') { console.log('Arrived at page:', params.page._id, params.page.name) } }} onBlur={(params) => { if (params.type === 'page.blur') { console.log('Left page:', params.page._id) } }} /> ``` | Event | Trigger | | ----------------------------------- | ---------------------------------------- | | `onBlur` with `type: 'page.blur'` | Emitted when navigating away from a page | | `onFocus` with `type: 'page.focus'` | Emitted when navigating to a page | **Page event payload:** | Property | Description | | ------------ | ---------------------------------------------- | | `sdk` | `'js'` | | `v` | `1` | | `type` | `'page.focus'` or `'page.blur'` | | `_id` | Document ID | | `identifier` | Document identifier | | `fileId` | File ID | | `page` | The full page object that gained or lost focus | For all event options (including field-level focus/blur), see [Event Handling](/web/guides/event-handling). # Configuring Field Settings Source: https://docs.joyfill.io/web/guides/fieldsettings-configuration ## What Are Field Settings? Field settings allow you to customize the form builder interface by: * **Adding custom controls** for data collection * **Hiding unnecessary options** to simplify the interface * **Creating targeted settings** for different field types * **Collecting metadata** for your business needs ## Quick Start ```jsx theme={null} import { JoyDoc } from '@joyfill/components'; function MyForm() { const fieldSettings = { // Your custom settings go here }; return ( ); } ``` ## Understanding Targets Targets determine **where** your settings appear in the form builder: | Target | Scope | Example Use Case | | ------------------------------------- | ------------------- | -------------------------------------- | | `file` | Entire document | Document metadata, global settings | | `page` | Specific page | Page sections, completion requirements | | `field` | All field types | Universal field behavior | | `text`, `number`, `dropdown`, `table` | Specific field type | Type-specific settings | | `field_12345` | Individual field | Custom field requirements | ## Setting Types ### 1. Identifier Settings Predefined field identifier options with custom option support: ```jsx theme={null} identifier: { label: 'Identifier', // Custom label options: [ { title: 'Plain Identifier Option', description: 'Identifier details for text', value: 'customer_text_customidentifierselector' } ] } ``` Sample output: ### 2. Metadata Settings Custom controls that save data to field metadata: | Type | Use Case | Properties | | ---------- | ---------------- | -------------------------------- | | `text` | Short text | `label`, `key`, `value`, `addon` | | `textarea` | Long text | `label`, `key`, `value` | | `number` | Numeric values | `label`, `key`, `value`, `addon` | | `checkbox` | Yes/No options | `label`, `key`, `value` | | `button` | Actions | `label`, `key`, `color` | | `divider` | Visual separator | `key` only | **Metadata Example:** ```jsx theme={null} metadata: { options: [ { type: FieldSettingsMetadataTypes.checkbox, label: 'Require Photo Uploads', description: 'Should a photo be required?', key: 'required' }, { type: FieldSettingsMetadataTypes.number, label: 'Quantity', description: 'How many photos are required?', key: 'count' }, { type: FieldSettingsMetadataTypes.divider, key: 'divider' }, { type: FieldSettingsMetadataTypes.checkbox, label: 'Failing Option', key: 'fail' } ] } ``` **Output:** ### 3. Table Columns Settings Customize table field columns: ```jsx theme={null} tableColumns: { types: ['text','dropdown', 'image', 'number' 'multiSelect', 'date', 'block', 'barcode','signature'], identifier: { label: 'Col Identifier', options: [ { type: 'text', title: 'Deficiencies', description: 'Details about identifer for table', value: 'table_customidentifierselector' } ] }, metadata: { options: [ { type: 'number', label: 'Require Photo Col', description: 'Should a photo be required?', key: 'required' } ] }, options: { metadata: { options: [ { type: 'checkbox', label: 'Failing Option', description: 'Trigger deficiency capture.', key: 'failure' } ] } } } ``` ### **Metadata for columns:** ### Custom column identifier ### 4. Options Settings Customize dropdown/select field options: ```jsx theme={null} options: { metadata: { options: [ { type: FieldSettingsMetadataTypes.checkbox, label: 'Failing Option', description: 'Trigger deficiency capture.', key: 'failure' } ] } } ``` ## Hiding Settings Hide default settings by setting them to `false`: ```jsx theme={null} const fieldSettings = { field: { title: false, // Hide field title required: false, // Hide required checkbox placeholder: false, // Hide placeholder input styles: { fontSize: false, // Hide font size fontColor: false, // Hide font color backgroundColor: false // Hide background color } }, page: { upload: false, // Hide page background upload duplicate: false // Hide page duplication } }; ``` ## Advanced Features ### File Upload Handlers ```jsx theme={null} onFileUploadAsync: async (params, fileUploads) => { console.log('onFileUploadAsync: ', params, fileUploads); return { _id: new Date().getTime(), url: 'https://s3.amazonaws.com/docspace.production.documents/6702de67c6ba43a423ca035f/documents/template_680bc498b6890a1324423764/680fbf96e97c7b42ec58f988-1745862550459.jpg' }; }, onFileClick: async (params, urlObject) => { console.log('onFileClick: ', params, urlObject); }, onFileDelete: async (params, urlObject) => { console.log('onFileDelete: ', params, urlObject); } ``` ### Global Settings ```jsx theme={null} const fieldSettings = { autoResize: false, // Set to true to see supported fields auto resize field: { systemFilePicker: false // Disable system file picker } }; ``` ## Complete Example Here's the exact fieldSettings example from JoyDoc.stories.js: ```jsx theme={null} import React, { useState } from "react"; import { JoyDoc } from "@joyfill/components"; import FieldSettingsMetadataTypes from "../constants/FieldSettingsMetadataTypes"; import fieldColumnTypes from "../constants/FieldTableColumnTypes"; function CompleteFieldSettingsExample() { const [document, setDocument] = useState(initialDocument); const fieldSettings = { autoResize: false, // Set to true to see supported fields auto resize page: { metadata: { options: [ { type: FieldSettingsMetadataTypes.checkbox, label: "Require Photo", description: "Should a photo be required?", key: "required", }, ], }, }, field: { systemFilePicker: false, onFileUploadAsync: async (params, fileUploads) => { console.log("onFileUploadAsync: ", params, fileUploads); return { _id: new Date().getTime(), url: "https://s3.amazonaws.com/docspace.production.documents/6702de67c6ba43a423ca035f/documents/template_680bc498b6890a1324423764/680fbf96e97c7b42ec58f988-1745862550459.jpg", }; }, onFileClick: async (params, urlObject) => { console.log("onFileClick: ", params, urlObject); }, onFileDelete: async (params, urlObject) => { console.log("onFileDelete: ", params, urlObject); }, identifier: { label: "Identifier", options: [ { title: "Plain Identifier Option", description: "Identifier details for text", value: "customer_text_customidentifierselector", }, ], }, metadata: { options: [ { type: 'checkbox', label: "Require Photo Uploads", description: "Should a photo be required?", key: "required", }, { type: 'number', label: "Quantity", description: "How many photos are required?", key: "count", }, { type: 'divider', key: "divider", }, { type: 'checkbox', label: "Failing Option", key: "fail", }, ], }, options: { metadata: { options: [ { type: 'checkbox', label: "Failing Option", description: "Trigger deficiency capture.", key: "failure", }, ], }, }, tableColumns: { types: [ 'text', 'dropdown', 'image', 'number', 'multiSelect', 'date', 'block', 'barcode', 'signature', ], identifier: { label: "Col Identifier", options: [ { type: "text", title: "Deficiencies", description: "Details about identifer for table", value: "table_customidentifierselector", }, ], }, metadata: { options: [ { type: 'number', label: "Require Photo Col", description: "Should a photo be required?", key: "required", }, ], }, options: { metadata: { options: [ { type: 'checkbox', label: "Failing Option", description: "Trigger deficiency capture.", key: "failure", }, ], }, }, }, }, }; const handleChange = (changelogs, updatedDoc) => { console.log("Document changed:", changelogs); setDocument(updatedDoc); }; return (

Complete Field Settings Example

This example demonstrates the exact fieldSettings from JoyDoc.stories.js

); } export default CompleteFieldSettingsExample; ``` ## Best Practices ### 1. Keep It Simple Don't overwhelm users with too many custom settings. Focus on what's essential for your use case. ### 2. Use Clear Labels and Descriptions Help users understand what each setting does with descriptive labels and helpful descriptions. ### 3. Provide Default Values Set sensible defaults to improve user experience. ### 4. Group Related Settings Use dividers to organize settings logically. ## Troubleshooting ### Common Issues **Settings Not Appearing:** Check your target syntax and ensure the field type matches. **Metadata Not Saving:** Ensure you're using the correct `key` property and that it's unique. **Settings Hidden When They Should Show:** Check for conflicting `false` values in your settings. ## Summary Field settings customization in Joyfill allows you to: * ✅ **Add custom controls** for your specific needs * ✅ **Hide unnecessary options** to simplify the interface * ✅ **Target specific field types** with specialized settings * ✅ **Collect metadata** for your business processes * ✅ **Create a tailored experience** for your users The key is to understand your users' needs and create settings that help them build forms more efficiently while collecting the data you need for your application. For more advanced customization options, refer to the [official Joyfill documentation](https://docs.joyfill.io/docs/customize-settings#/). # Formulas Source: https://docs.joyfill.io/web/guides/formulas Formulas enable automatic calculation of field values based on expressions that reference other fields. When a user changes a field value, all dependent formulas are automatically recalculated in the correct order, keeping your forms dynamic and always up-to-date. ## Overview Formulas allow you to create calculated fields that automatically update based on other field values. The formula system handles dependency resolution automatically, ensuring formulas are evaluated in the correct order even when fields depend on each other. **Key Features:** * Automatic dependency resolution * Circular dependency detection * Support for arithmetic, functions, and array operations * Real-time recalculation when field values change * Type-safe comparisons (no automatic type coercion) ## Enabling Formulas Formulas are automatically resolved by the JoyDoc component when enabled in the `features` prop: ```jsx theme={null} import { JoyDoc } from '@builttocreate/joyfill-components'; function MyForm() { const [document, setDocument] = useState(myDocument); return ( { // Formulas are automatically resolved before onChange is called // updatedDoc contains fields with calculated values setDocument(updatedDoc); }} /> ); } ``` ## How Formulas Work Formulas consist of two parts: 1. **Formula Definition** - The calculation expression stored in `doc.formulas` 2. **Formula Application** - The link between a field and a formula stored in `field.formulas` When a field value changes: 1. JoyDoc automatically identifies all formulas that reference that field 2. Determines the correct evaluation order (handles dependencies automatically) 3. Evaluates all dependent formulas 4. Updates field values with calculated results 5. Calls `onChange` with the updated document ## Formula Structure ### Document-Level Formula Definitions Define formulas at the document level in a `formulas` array: ```jsx theme={null} { formulas: [ { _id: 'calculateTotal', // Unique formula identifier desc: 'Calculate total price', // Optional: Description expression: 'price * quantity', // The calculation expression type: 'calc', // Optional: Formula type scope: 'private' // Optional: Formula scope } ] } ``` ### Field-Level Formula Applications Fields that should receive formula results have a `formulas` property: ```jsx theme={null} { _id: 'total', type: 'number', title: 'Total', value: 0, formulas: [ { _id: 'applied_total', // Unique application ID formula: 'calculateTotal', // References formula._id key: 'value' // Field property to update (always 'value') } ] } ``` ## Field References Formulas reference fields by their `_id` property. You can reference any field in the document: ```jsx theme={null} { fields: [ { _id: 'price', type: 'number', value: 100 }, { _id: 'quantity', type: 'number', value: 3 }, { _id: 'total', type: 'number', value: 0, formulas: [{ formula: 'calculateTotal', key: 'value' }] } ], formulas: [ { _id: 'calculateTotal', desc: 'Calculate total', expression: 'price * quantity' // References field IDs: price and quantity } ] } ``` ## Basic Arithmetic Formulas support standard arithmetic operations: ```jsx theme={null} { formulas: [ { _id: 'add', desc: 'Add two values', expression: 'field1 + field2' }, { _id: 'subtract', desc: 'Subtract values', expression: 'total - discount' }, { _id: 'multiply', desc: 'Multiply values', expression: 'quantity * price' }, { _id: 'divide', desc: 'Divide values', expression: 'total / items' }, { _id: 'complex', desc: 'Complex calculation', expression: '(price * quantity) + tax - discount' } ] } ``` **Note:** Formulas can contain literal values (`"5 + 3"`), field references (`field1 + field2`), or a mix of both (`field1 + 5`). ## Built-in Functions The formula engine includes 50+ built-in functions across multiple categories. ### Math Functions ```jsx theme={null} sum(1, 2, 3) // 6 - Sum of numbers or arrays average(10, 20, 30) // 20 - Average of numbers min(5, 2, 8) // 2 - Minimum value max(5, 2, 8) // 8 - Maximum value round(3.14159, 2) // 3.14 - Round to decimal places abs(-5) // 5 - Absolute value pow(2, 3) // 8 - Power (2^3) ceil(4.2) // 5 - Round up floor(4.8) // 4 - Round down mod(10, 3) // 1 - Modulo (remainder) sqrt(16) // 4 - Square root ``` ### String Functions ```jsx theme={null} concat('Hello', ' ', 'World') // 'Hello World' - Concatenate strings contains('Hello World', 'World') // true - Check if contains substring (case-insensitive) upper('hello') // 'HELLO' - Convert to uppercase lower('HELLO') // 'hello' - Convert to lowercase toNumber('123') // 123 - Convert string to number ``` ### Array Functions Array functions work with table fields and other array values: ```jsx theme={null} count(array) // Count elements length(array) // Alias for count filter(array, predicate) // Filter array with function map(array, transform) // Transform array elements some(array, predicate) // Test if some elements match every(array, predicate) // Test if all elements match reduce(array, reducer, initial) // Reduce to single value find(array, predicate) // Find first matching element flat(array, depth) // Flatten nested arrays flatMap(array, transform) // Map and flatten countIf(array, value) // Count occurrences (case-insensitive) ``` ### Logic Functions ```jsx theme={null} iff(condition, trueValue, falseValue) // Conditional (if-then-else) not(value) // Logical NOT and(true, true, false) // Logical AND (all must be true) or(true, false, false) // Logical OR (any must be true) empty(value) // Check if empty/null/undefined ``` ### Date Functions ```jsx theme={null} now() // Current timestamp year(timestamp) // Extract year (returns null if timestamp is null) month(timestamp) // Extract month (1-12, returns null if timestamp is null) day(timestamp) // Extract day of month (returns null if timestamp is null) date(year, month, day) // Create timestamp from components dateAdd(timestamp, amount, unit) // Add time ('days', 'months', 'years', returns null if timestamp is null) dateSubtract(timestamp, amount, unit) // Subtract time (returns null if timestamp is null) ``` **Important:** Date functions preserve null values. If a date field is null/empty, date functions return null rather than treating it as the current date. ## Arrow Functions (Lambda Functions) Formulas support arrow functions for array operations. You can use either Notion-style (`->`) or JavaScript (`=>`) syntax: ```jsx theme={null} // Filter array elements - Both syntaxes work filter(scores, (score) -> score >= 80) filter(scores, (score) => score >= 80) // Map array elements map(items, (item) => item.price * item.quantity) // Complex nested operations filter( table1, (row) => and( row.status == "active", row.priority > 5 ) ) ``` ### Working with Table Fields When working with table fields in lambda functions, access columns directly by their column field ID: ```jsx theme={null} // Access table row by index table1[0].text1 // First row, text1 column // In lambda functions, access columns directly filter(table1, (row) => row.number1 > 80) map(table1, (row) => row.price * row.quantity) // Column names correspond to the field IDs defined in the table's column schema ``` ## Comparison Operators All comparison operators use strict type checking (no automatic type coercion): ### Equality (`==`) Type and value must be exactly the same: ```jsx theme={null} 1 == 1 // true "hello" == "hello" // true "1" == 1 // false (different types) [] == [] // false (arrays always false) ``` ### Inequality (`!=`) Returns true if types or values differ: ```jsx theme={null} 1 != 2 // true "1" != 1 // true (different types) [] != [] // true (arrays always true) ``` ### Greater Than (`>`) Only works with numbers: ```jsx theme={null} 12 > 10 // true "10" > 5 // false (not numbers) [] > 1 // false (arrays always false) ``` ### Less Than (`<`) Only works with numbers: ```jsx theme={null} 10 < 12 // true 5 < "10" // false (not numbers) 1 < [] // false (arrays always false) ``` ### Greater/Less Than or Equal (`>=`, `<=`) Work like `>` and `<` but include equality: ```jsx theme={null} 10 >= 10 // true 10 <= 12 // true ``` ## Complete Examples ### Example 1: Basic Calculation ```jsx theme={null} import { JoyDoc } from '@builttocreate/joyfill-components'; function InvoiceForm() { const [document, setDocument] = useState({ fields: [ { _id: 'price', type: 'number', value: 25.50 }, { _id: 'quantity', type: 'number', value: 3 }, { _id: 'total', type: 'number', title: 'Total', value: 0, formulas: [{ formula: 'calculateTotal', key: 'value' }] } ], formulas: [ { _id: 'calculateTotal', desc: 'Calculate total price', expression: 'price * quantity' } ], files: [/* ... */] }); return ( { // When price or quantity changes, total is automatically recalculated setDocument(updatedDoc); }} /> ); } ``` ### Example 2: Conditional Logic ```jsx theme={null} { fields: [ { _id: 'score', type: 'number', value: 85 }, { _id: 'grade', type: 'text', title: 'Grade', value: '', formulas: [{ formula: 'calculateGrade', key: 'value' }] } ], formulas: [ { _id: 'calculateGrade', desc: 'Calculate grade based on score', expression: 'iff(score >= 90, "A", iff(score >= 80, "B", "C"))' } ] } // Result: grade = "B" when score = 85 ``` ### Example 3: Table Operations ```jsx theme={null} { fields: [ { _id: 'orderItems', type: 'table', value: [ { _id: 'r1', cells: { itemName: 'Widget', price: 10, quantity: 2 } }, { _id: 'r2', cells: { itemName: 'Gadget', price: 20, quantity: 3 } }, { _id: 'r3', cells: { itemName: 'Thing', price: 15, quantity: 1 } } ] }, { _id: 'totalRevenue', type: 'number', title: 'Total Revenue', value: 0, formulas: [{ formula: 'calculateRevenue', key: 'value' }] }, { _id: 'highValueCount', type: 'number', title: 'High Value Items', value: 0, formulas: [{ formula: 'countHighValue', key: 'value' }] } ], formulas: [ { _id: 'calculateRevenue', desc: 'Sum of all item prices times quantities', expression: 'sum(map(orderItems, (row) => row.price * row.quantity))' }, { _id: 'countHighValue', desc: 'Count items with revenue > 50', expression: 'length(filter(orderItems, (row) => row.price * row.quantity > 50))' } ] } ``` **Table Access:** * `table1[0]` - Access first row (zero-indexed) * `table1[0].columnName` - Access specific column in first row * In lambda functions: `(row) => row.columnName` - Access column by field ID ### Example 4: String Operations ```jsx theme={null} { fields: [ { _id: 'first_name', type: 'text', value: 'John' }, { _id: 'last_name', type: 'text', value: 'Doe' }, { _id: 'full_name', type: 'text', title: 'Full Name', value: '', formulas: [{ formula: 'combineNames', key: 'value' }] } ], formulas: [ { _id: 'combineNames', desc: 'Combine first and last name', expression: 'concat(first_name, " ", last_name)' } ] } // Result: full_name = "John Doe" ``` ### Example 5: Date Operations ```jsx theme={null} { fields: [ { _id: 'start_date', type: 'date', value: 1748797200000 }, { _id: 'end_date', type: 'date', title: 'End Date', value: 0, formulas: [{ formula: 'calculateEndDate', key: 'value' }] }, { _id: 'year', type: 'number', title: 'Year', value: 0, formulas: [{ formula: 'extractYear', key: 'value' }] } ], formulas: [ { _id: 'calculateEndDate', desc: 'Calculate end date 30 days after start', expression: 'dateAdd(start_date, 30, "days")' }, { _id: 'extractYear', desc: 'Extract year from start date', expression: 'year(start_date)' } ] } ``` ### Example 6: Complex Table Operations ```jsx theme={null} { fields: [ { _id: 'orders', type: 'table', value: [ { _id: 'r1', cells: { product: 'Product A', status: 'active', quantity: 10, price: 50 } }, { _id: 'r2', cells: { product: 'Product B', status: 'pending', quantity: 5, price: 30 } }, { _id: 'r3', cells: { product: 'Product C', status: 'active', quantity: 8, price: 40 } } ] }, { _id: 'activeOrdersTotal', type: 'number', title: 'Active Orders Total', value: 0, formulas: [{ formula: 'calculateActiveTotal', key: 'value' }] } ], formulas: [ { _id: 'calculateActiveTotal', desc: 'Sum of active orders only', expression: `sum( map( filter(orders, (row) => row.status == "active"), (row) => row.quantity * row.price ) )` } ] } ``` ## Dependency Resolution Formulas automatically resolve dependencies and evaluate in the correct order: ```jsx theme={null} { fields: [ { _id: 'a', type: 'number', value: 1 }, { _id: 'b', type: 'number', value: 2 }, { _id: 'sum', type: 'number', value: 0, formulas: [{ formula: 'addAB', key: 'value' }] }, { _id: 'product', type: 'number', value: 0, formulas: [{ formula: 'multiplySum', key: 'value' }] } ], formulas: [ { _id: 'addAB', desc: 'Add field a and b', expression: 'a + b' // Evaluated first }, { _id: 'multiplySum', desc: 'Multiply sum by 2', expression: 'sum * 2' // Evaluated after sum is calculated } ] } ``` The system automatically detects that `multiplySum` depends on `sum`, so it evaluates `addAB` first, then `multiplySum`. ## Field Types and Formulas ### Supported Field Types Formulas can write calculated values to: * `text` - String results * `textarea` - String results * `number` - Numeric results * `dropdown` - String results * `multiSelect` - Array results * `date` - Timestamp results * `chart` - Array of chart line objects ### Read-Only Field Types These field types do NOT support formula writes: * `signature` * `image` * `file` * `table` * `collection` **Note:** While table and collection fields cannot receive formula results, you can read their values in formulas (e.g., `sum(map(table1, (row) => row.price))`). ## Common Patterns ### Pattern 1: Calculate Total from Table ```jsx theme={null} expression: 'sum(map(orderItems, (row) => row.price * row.quantity))' ``` ### Pattern 2: Conditional Calculation ```jsx theme={null} expression: 'iff(quantity > 10, price * quantity * 0.9, price * quantity)' ``` ### Pattern 3: Filter and Count ```jsx theme={null} expression: 'length(filter(items, (item) => item.status == "active"))' ``` ### Pattern 4: Average with Filter ```jsx theme={null} expression: 'average(map(filter(scores, (s) => s > 0), (s) => s.value))' ``` ### Pattern 5: String Concatenation ```jsx theme={null} expression: 'concat(first_name, " ", last_name)' ``` ### Pattern 6: Multiple Conditions ```jsx theme={null} expression: `length( filter(table1, (row) => and( row.status == "active", row.priority > 5, or(row.category == "urgent", row.category == "high") ) ) )` ``` ## Integration with Conditional Logic Formulas and conditional logic work independently: * **Formulas** - Calculate field values automatically * **Conditional Logic** - Control field/page visibility A field can have both formulas and conditional logic: ```jsx theme={null} { _id: 'calculated_field', type: 'number', value: 0, hidden: true, formulas: [{ formula: 'calc', key: 'value' }], // Calculates value logic: { action: 'show', eval: 'and', conditions: [ { file: 'file1', page: 'page1', field: 'trigger_field', condition: '=', value: 'show' } ] } } ``` ## Error Handling Formulas are evaluated automatically by JoyDoc. If a formula has errors: 1. The formula is skipped (doesn't crash the form) 2. Other formulas continue to evaluate 3. The field retains its previous value **Common Errors:** * **Unknown Function** - Function name typo (e.g., `lenght` instead of `length`) * **Undefined Variable** - Variable name doesn't match lambda parameter * **Circular Dependency** - Fields reference each other in a loop * **Type Mismatch** - Formula result type doesn't match field type ## Best Practices 1. **Use Descriptive Formula IDs** - Make formula IDs meaningful (`calculateTotal` vs `f1`) 2. **Add Descriptions** - Use the `desc` field to document what each formula does 3. **Handle Null Values** - Use `empty()` or null checks for optional fields 4. **Test Edge Cases** - Test with empty arrays, null values, and zero values 5. **Keep Expressions Readable** - Break complex formulas into multiple steps when possible 6. **Use Arrow Functions** - Use `=>` or `->` syntax for cleaner array operations 7. **Validate Field Types** - Ensure formula results match target field types 8. **Reference Existing Fields** - Ensure referenced fields exist before using them ## Troubleshooting ### Formula Not Updating 1. **Check Formula Feature** - Ensure `features.formulas: true` is set 2. **Check Formula Application** - Ensure field has `formulas` array with correct `formula` reference 3. **Verify Field References** - Ensure field IDs in expression match actual field IDs 4. **Check Field Type** - Ensure field type supports formula writes ### Formula Errors 1. **Unknown Function** - Check function name spelling 2. **Undefined Variable** - Verify variable names match lambda parameters 3. **Type Mismatch** - Ensure formula result type matches field type 4. **Circular Dependency** - Check for circular references between formulas ### Debugging Tips 1. **Start Simple** - Test with basic arithmetic first 2. **Check onChange** - Verify `onChange` is being called with updated values 3. **Validate Document Structure** - Ensure formulas array exists and is properly formatted 4. **Test Individual Formulas** - Isolate formulas to identify issues ## Complete Integration Example ```jsx theme={null} import { useState } from 'react'; import { JoyDoc } from '@builttocreate/joyfill-components'; function InvoiceForm() { const [document, setDocument] = useState({ _id: 'invoice_doc', identifier: 'invoice', name: 'Invoice Form', fields: [ { _id: 'unit_price', type: 'number', value: 0 }, { _id: 'quantity', type: 'number', value: 0 }, { _id: 'discount_percent', type: 'number', value: 0 }, { _id: 'subtotal', type: 'number', title: 'Subtotal', value: 0, formulas: [{ formula: 'calculateSubtotal', key: 'value' }] }, { _id: 'discount_amount', type: 'number', title: 'Discount', value: 0, formulas: [{ formula: 'calculateDiscount', key: 'value' }] }, { _id: 'total', type: 'number', title: 'Total', value: 0, formulas: [{ formula: 'calculateTotal', key: 'value' }] } ], formulas: [ { _id: 'calculateSubtotal', desc: 'Calculate subtotal before discount', expression: 'unit_price * quantity' }, { _id: 'calculateDiscount', desc: 'Calculate discount amount', expression: 'subtotal * (discount_percent / 100)' }, { _id: 'calculateTotal', desc: 'Calculate final total', expression: 'subtotal - discount_amount' } ], files: [/* ... */] }); return ( { // Formulas are automatically resolved // updatedDoc contains all calculated values console.log('Document updated:', updatedDoc); setDocument(updatedDoc); }} /> ); } ``` In this example: * When `unit_price` or `quantity` changes, `subtotal` is automatically recalculated * When `subtotal` or `discount_percent` changes, `discount_amount` is recalculated * When `subtotal` or `discount_amount` changes, `total` is recalculated * All calculations happen automatically in the correct order # Image Upload Handling Source: https://docs.joyfill.io/web/guides/image-upload-handling ## Overview The JoyDoc SDK provides comprehensive image upload functionality through image fields, supporting both single and multiple image uploads with flexible handling options. **Architecture Overview** When users interact with image fields, the SDK triggers upload events that your application must handle by implementing upload handlers. **Flow:** 1. User selects image field → SDK creates upload event 2. Your upload handler receives the event 3. You handle the image processing and pass the image url back to SDK 4. SDK updates the form with the provided URLs ## Upload Methods JoyDoc provides two methods for handling image uploads: * **`onFileUploadAsync`** (configured in field settings) * **`onUploadAsync`** (configured in JoyDoc component props) ## Basic Implementation Examples **onFieldUploadAsync** You can also configure `onFileUploadAsync` via field settings as documented in the [Joyfill Customize Settings guide](https://docs.joyfill.io/docs/customize-settings#/field--field_type--field_identifier): ```jsx theme={null} const fieldSettings = { field: { onFileUploadAsync: async (params, fileUploads) => { console.log( "onFileUploadAsync via field settings: ", params, fileUploads ); const uploadPromises = fileUploads.map(async (file) => { const dataUri = await getDataUriForFileUpload(file); return uploadFileAsync(params.fieldIdentifier, dataUri); }); const results = await Promise.all(uploadPromises); return results; }, }, }; // Use with JoyDoc component ; ``` ### onUploadAsync Configure upload handling at the document level: ```jsx theme={null} { console.log("onUploadAsync: ", params, fileUploads); const resultPromises = await fileUploads.map(async (fileUpload) => { const dataUri = await getDataUriForFileUpload(fileUpload); return uploadFileAsync(params.fieldIdentifier, dataUri); }); return Promise.all(resultPromises) .then((responses) => { // Normalize response for different file types const finalResponse = Array.isArray(responses[0]) ? responses[0] : responses; return finalResponse; }) .catch((error) => { console.error("Upload error:", error); if (error) return; }); }} // ... other props />; ``` ## Parameters Both methods receive the same parameters: * **params**: Object containing field/document metadata * `params.fileId`: The file ID * `params.pageId`: The page ID * `params.fieldId`: The field ID * `params.fieldPositionId`: The field position ID * `params.fieldIdentifier`: The field identifier * `params.documentId`: The document ID * `params.documentIdentifier`: The document identifier * **fileUploads**: Array of File objects to be uploaded ## Return Value Both methods should return an array of upload results. Each result should contain: * `_id`: Unique identifier for the uploaded file * `url`: URL where the file can be accessed * `fileName`: Original filename * `fileSize`: Size of the file in bytes ## Complete Working Examples ### Example 1: onFileUploadAsync Here's a complete working example using `onFileUploadAsync`: ```jsx theme={null} import React, { useState } from 'react'; import { JoyDoc, getDefaultDocument } from '@joyfill/components'; function OnFileUploadAsyncExample() { const [document, setDocument] = useState(() => { const doc = getDefaultDocument(); const fields = [ { _id: 'profileImage', identifier: 'profileImage', type: 'image', title: 'Profile Image', value: [], file: doc.files[0]._id, multi: false } ]; doc.fields = fields; doc.files[0].pages[0].fieldPositions.push({ _id: 'profileImage-position', field: 'profileImage', x: 0, y: 0, width: 1, height: 1, displayType: 'original' }); return doc; }); const handleChange = (changelogs, updatedDoc) => { setDocument(updatedDoc); }; const fieldSettings = { field: { systemFilePicker: true, onFileUploadAsync: async (params, fileUploads) => { console.log('onFileUploadAsync triggered:', params, fileUploads); const uploadPromises = fileUploads.map(async (file) => { const dataUri = await getDataUriForFileUpload(file); return uploadFileAsync(params.fieldIdentifier, dataUri); }); const results = await Promise.all(uploadPromises); console.log('Upload completed:', results); return results; } } }; return (

onFileUploadAsync Example

); } export default OnFileUploadAsyncExample; ``` ### Example 2: onUploadAsync Here's a complete working example using `onUploadAsync`: ```jsx theme={null} import React, { useState } from "react"; import { JoyDoc, getDefaultDocument } from "@joyfill/components"; function OnUploadAsyncExample() { const [document, setDocument] = useState(() => { const doc = getDefaultDocument(); const fields = [ { _id: "galleryImages", identifier: "galleryImages", type: "image", title: "Image Gallery", value: [], file: doc.files[0]._id, multi: true, }, ]; doc.fields = fields; doc.files[0].pages[0].fieldPositions.push({ _id: "galleryImages-position", field: "galleryImages", x: 0, y: 0, width: 1, height: 1, displayType: "original", }); return doc; }); const handleChange = (changelogs, updatedDoc) => { setDocument(updatedDoc); }; const handleUpload = async (params, fileUploads) => { console.log("onUploadAsync triggered:", params, fileUploads); const resultPromises = await fileUploads.map(async (fileUpload) => { const dataUri = await getDataUriForFileUpload(fileUpload); return uploadFileAsync(params.fieldIdentifier, dataUri); }); return Promise.all(resultPromises) .then((responses) => { // Normalize response for different file types const finalResponse = Array.isArray(responses[0]) ? responses[0] : responses; console.log("Upload completed:", finalResponse); return finalResponse; }) .catch((error) => { console.error("Upload error:", error); if (error) return; }); }; return (

onUploadAsync Example

); } export default OnUploadAsyncExample; ``` ## Setup steps **Step 1: Create Joyfill Account** * To begin working with Joyfill, go to [Joyfill's Platform](https://app-joy.joyfill.io/templates) and create an account (jump to step 2 if you already have an account). * By creating an account you will add yourself as the first user to your newly created Joyfill Organization and be placed inside the Joyfill Manager. **Step 2: Generate Your userAccessToken** * Once you're inside the Joyfill Manager you will want to select from the top navigation bar Settings & Users -> Manager Users -> and click "Access Tokens" button next to your user. * Once the modal appears select "Add Access Token". Copy and securely store your access token for later use. **Step3 Api Url** * The Api url is `http://api-joy.joyfill.io` ## Helper Functions These utility functions are used in both upload methods: ### getDataUriForFileUpload Converts a File object to a data URI string: ```jsx theme={null} const getDataUriForFileUpload = async (fileUpload) => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsDataURL(fileUpload); reader.onloadend = async () => { resolve(reader.result); }; reader.onerror = () => { reject(new Error('Failed to read file')); }; }); }; ``` ### uploadFileAsync Uploads a data URI to the server: ```jsx theme={null} const uploadFileAsync = async (docIdentifier, dataUri) => { const response = await fetch(`${apiUrl}/v1/documents/${docIdentifier}/files/datauri`, { method: 'POST', mode: 'cors', headers: getHeaders(), body: JSON.stringify({ file: dataUri }) }); const data = await response.json(); return data; }; ``` ### getHeaders Returns the necessary headers for API requests: ```jsx theme={null} const getHeaders = () => { return { Authorization: `Bearer ${userAccessToken}`, 'Content-Type': 'application/json' }; }; ``` # Generate PDF Downloads Source: https://docs.joyfill.io/web/guides/joydoc-exporter Render JoyDoc forms as PDF-ready layouts for self-hosted PDF generation # Overview The `JoyDocExporter` component renders a JoyDoc form as a PDF-ready layout. It handles field measurement, formula resolution, and conditional logic — outputting a structure suitable for capturing as a PDF file using a headless browser. This component is the core building block for [self-hosted PDF generation](/web/guides/pdf-generator-self-hosted). It takes your JoyDoc JSON data and renders it in a print-optimized format. # 1. Install ## Using a package manager ```bash bash theme={null} npm install --save @joyfill/components ``` ```bash bash theme={null} yarn add @joyfill/components ``` # 2. Props | Prop | Type | Required | Default | Description | | -------- | -------- | -------- | --------- | ----------------------------------------------------------- | | `doc` | `object` | ✅ | — | JoyDoc JSON data containing `files` and `fields` arrays. | | `theme` | `object` | ✅ | — | Theme configuration. The `fontFamily` property is required. | | `config` | `object` | ❌ | See below | PDF page configuration (height, width, padding). | ### Config Object | Property | Type | Default | Description | | --------------------- | -------- | ------- | ----------------------- | | `config.page.height` | `number` | `1056` | Page height in pixels. | | `config.page.width` | `number` | `816` | Page width in pixels. | | `config.page.padding` | `number` | `0` | Page padding in pixels. | ### Theme Object The `fontFamily` property is **required**. You must also ensure the font is loaded in your application before rendering the exporter. ```javascript theme={null} theme={{ fontFamily: 'Arial, sans-serif', fontColorPrimary: '#333333', fontColorSecondary: '#666666', primaryColor: '#0066FF', backgroundColor: '#FFFFFF', borderColor: '#E0E0E0', borderRadius: 4, field: { titleFontSize: 14, titleFontColor: '#333333', titleFontStyle: 'normal', titleFontWeight: 'bold', titleTextAlign: 'left', fontSize: 13, fontColor: '#333333', margin: 4 } }} ``` **fontFamily is required.** The `JoyDocExporter` will not render correctly without a `fontFamily` set in the theme. Make sure the font is loaded in your application. # 3. Usage ## React ```jsx theme={null} import { JoyDocExporter } from '@joyfill/components'; const doc = { _id: '691f3762738ed0e8e217abff', type: 'template', stage: 'draft', metadata: {}, identifier: 'template_691f3762738ed0e8e217abff', name: 'Starbucks template', createdOn: 1763653482316, files: [ { _id: '691f3762c80bfb0005c57b48', metadata: {}, name: 'Starbucks template', version: 1, styles: { margin: 4 }, pages: [ { name: 'New Page', fieldPositions: [ { _id: '699f166036e3f465e57c6b68', type: 'text', displayType: 'original', x: 0, y: 11, width: 4, height: 8, field: 'text1', }, ], hidden: false, width: 816, height: 1056, cols: 8, rowHeight: 8, layout: 'grid', presentation: 'normal', margin: 0, padding: 24, borderWidth: 0, _id: '691f376206195944e65eef76', metadata: {}, }, ], pageOrder: ['691f376206195944e65eef76'], views: [], }, ], fields: [ { file: '691f3762c80bfb0005c57b48', _id: 'text1', type: 'text', title: 'Text', identifier: 'field_text1', }, ], deleted: false, categories: [], }; function PDFPage() { return ( ); } export default PDFPage; ``` # 4. PDF Capture Ready The `JoyDocExporter` uses a two-phase rendering process: 1. **Measurement Phase** — The component measures field heights for dynamic layouts (tables, multi-select fields, etc.). During this phase, a "Field Measuring In Progress" message is displayed. 2. **Render Phase** — Once measurements are complete, the final PDF layout is rendered and a `#pdf-capture-ready` element is added to the DOM. **Important:** When using a headless browser to capture the PDF, you must wait for the `#pdf-capture-ready` element to appear in the DOM before taking a screenshot or generating the PDF. Capturing before this element exists will result in incomplete or missing content. ### Example: Waiting for PDF Ready (Puppeteer) ```javascript theme={null} await page.goto('http://localhost:3000/pdf-export', { waitUntil: 'networkidle0' }); await page.waitForSelector('#pdf-capture-ready'); await page.pdf({ path: 'output.pdf', format: 'Letter', printBackground: true }); ``` # 5. CSS Requirements Apply the following styles to the HTML file that contains the `JoyDocExporter`. This ensures proper color rendering and spacing when generating a PDF. ```html theme={null} ``` Ensure that no margin, padding, or other external spacing styles are targeting the body or any parent elements that contain `JoyDocExporter`. The component targets measurements and sizing to support standard PDF Letter dimensions. Additional spacing could impact sizing and cause fields to get cut off. # 6. Features The `JoyDocExporter` automatically handles the following when rendering: * **Formula Resolution** — All formulas are calculated and resolved before rendering. * **Conditional Logic** — Fields are shown or hidden based on configured conditional logic rules. * **Dynamic Field Sizing** — Table, collection, and multi-select fields are measured and sized to fit their content. * **Page Breaking** — Content is split across pages based on the configured page dimensions. # Form Modes Source: https://docs.joyfill.io/web/guides/modes ## Overview The Joyfill Components SDK supports three distinct modes that control how users can interact with your forms: **Edit Mode**, **Fill Mode**, and **Read-Only Mode**. ## Edit Mode (Default) **Edit Mode** (`edit`) is the default mode that allows full form design, styling, and configuration capabilities. ```jsx theme={null} ``` **Features:** * Full form design and layout capabilities * Field creation, editing, and deletion * Field positioning and resizing * Form styling and theme customization * Page management (create, duplicate, delete) * Table and collection field management * Formula configuration * Field settings and validation rules * All interactive design elements are functional ## Fill Mode **Fill Mode** (`fill`) allows users to input data into pre-configured forms without design capabilities. ```jsx theme={null} const documentEditor = ( ) ``` **Features:** * Users can input data into all field types * File uploads and camera capture are enabled * Form validation is active * Page navigation is available for multi-page forms * Field interactions (focus, blur, change) are tracked * All data entry functionality is operational * Design elements are hidden or disabled ## Read-Only Mode **Read-Only Mode** (`readonly`) displays forms for viewing only, preventing any user modifications. ```jsx theme={null} const documentEditor = ( ) ``` **Features:** * All fields are disabled for input * File uploads and camera capture are disabled * Form data is displayed but cannot be modified * Page navigation remains functional for multi-page forms * Field interactions are limited to viewing * All design and editing capabilities are disabled * Form validation is not enforced ## Mode Comparison | Feature | Edit Mode | Fill Mode | Read-Only Mode | | --------------------- | --------- | --------- | -------------- | | Field Creation | ✅ | ❌ | ❌ | | Field Editing | ✅ | ❌ | ❌ | | Field Positioning | ✅ | ❌ | ❌ | | Data Input | ✅ | ✅ | ❌ | | File Uploads | ✅ | ✅ | ❌ | | Form Validation | ✅ | ✅ | ❌ | | Page Management | ✅ | ❌ | ❌ | | Formula Configuration | ✅ | ❌ | ❌ | | Theme Customization | ✅ | ❌ | ❌ | | Field Settings | ✅ | ❌ | ❌ | | Page Navigation | ✅ | ✅ | ✅ | | Print/Export | ✅ | ✅ | ✅ | # PDF File Uploads Source: https://docs.joyfill.io/web/guides/pdf-file-uploads How to create fillable PDF forms from your user's local PDF files # PDF File Uploads (JS SDK) How to create fillable PDF forms from your user's local PDF files. ## Overview The Joyfill SDK is built on the JSON standard (see JoyDoc) instead of the PDF binary file standard. This enables better data storage, parsing, querying and overall developer experience. But in order to achieve this transition from PDF to JSON, we must transform the PDF. In this guide we will show you how to: * Convert PDFs to images that can be utilized within JSON. * Generate a Joyfill Fillable PDF JSON Template from the original PDF images. * Handle underlying PDF update scenarios within existing Joyfill Fillable PDFs. ## Step 1: PDF Conversion You can implement your own PDF to Image conversion functionality internally. See recommend functionality below: * Add PDF to Image Conversion functionality. We recommend using [Convert API](https://www.convertapi.com/). We are not affiliated with them but we are big fans. Convert API provide a wonderful API, enterprise compliance, and affordable pricing. * Store those converted images in your own asset service, ie. S3 or something similar. * Generate an array of objects that contain the public urls or signing information from the stored images. This information will be utilized to create the Joyfill JSON object. ## Step 2: JSON Creation In this step we are going to show you how to utilizes the PDF conversion from Step 1 to create a fillable PDF form that can be used within the Joyfill SDK. ## Option 1: Joyfill SDK Workflow This example uses the Joyfill SDK as the starting point for uploading user PDFs. Users can drag and drop or upload PDFs directly inside the of Joyfill SDK using the right panel upload setting. ### Step 1: Load Joyfill SDK In this step you will send the user directly to the application page that renders the Joyfill SDK. User can then upload their PDF file using upload area in the right side panel settings. ### Step 2: Handle upload in onUploadAsync When a user uploads a PDF file through the Joyfill SDK, the [`onUploadAsync`](/web/guides/image-upload-handling#onuploadasync) handler is triggered. Your implementation should: 1. **Detect the file type** - Check if the uploaded file is a PDF or image 2. **Convert PDF to images** - If it's a PDF, convert each page to an image using the conversion functionality from Step 1: PDF Conversion 3. **Return formatted data** - Return a resolved promise containing an array of image URL objects The `onUploadAsync` handler must return a resolved promise with an array of objects, where each object contains the image URL. Learn more about handling uploads and returning properly formatted data in our [PDF, Files, and Image Uploads Guide](/web/guides/image-upload-handling). **Example:** When a user uploads a PDF file like this: Your `onUploadAsync` handler will convert it to images and return the array. Once the SDK receives the properly formatted array of image objects, it will automatically create or update the associated pages in the document: See example usage of [`onUploadAsync`](/web/guides/image-upload-handling#onuploadasync) **Important Note:** The Joyfill SDK upload allows for both PDF and image file types. Always check the file type inside your `onUploadAsync` handler to ensure proper handling - PDFs need conversion to images, while image files can be used directly. ## Option 2: Custom Creation Workflow This approach uses a custom creation flow to generate the JoyDoc JSON structure before sending users to the Joyfill SDK screen. You can implement this using a modal, button, or any other custom workflow in your application. **Using Joyfill Business Solution:** You can generate the JoyDoc JSON structure using the Joyfill Business Solution by following these steps: 1. Navigate to the [Joyfill Business Solution](https://app.joyfill.io) and open the form library section. 2. Click the "Upload PDF" button to open the upload modal. Upload PDF Modal 3. Select and upload your PDF file from your local file system. 4. Once uploaded, the system automatically converts the PDF to images and builds the JoyDoc JSON structure. Click the "Create" button to proceed. 5. You'll be redirected to the Joyfill SDK screen with the JoyDoc JSON already populated. The PDF pages will appear as background images, ready for you to add form fields and customize. ### Step 1: Handle File Upload and Conversion After the user has uploaded a PDF file to your application, you will then need to convert that PDF file into background images using the conversion functionality from Step 1: PDF Conversion. ### Step 2: Generate JoyDoc JSON Template In this step we will utilize the Joyfill helper methods and the images from Step 1: PDF conversion, to generate the JoyDoc JSON that can be used within the Joyfill SDK. ```js theme={null} import { getDefaultTemplate, getDefaultPage } from '@joyfill/components'; const generateTemplateFromPDF = async (file) => { //1. Generate default template const template = getDefaultTemplate(); //Template is a Joyfill JSON Object //2. Convert PDF to images const imageUrls = await convertPDFFromStepOne(file); //imgUrls example: [ { url: String }, {url: String }, ... ]; //3. Generate a JoyDoc Page for each PDF Page const templatePages = []; const templatePageOrder = []; imageUrls.forEach(({ url }) => { const page = getDefaultPage({ displayType: 'pdf', //Important! backgroundImage: url, }); templatePages.push(page); templatePageOrder.push(page._id); }) //4. Assign template pages and page order template.files[x].pages = templatePages; template.files[x].pageOrder = templatePageOrder; //5. Return formatted template for use in the Joyfill SDK return template; }; ``` ### Step 3: Joyfill SDK Now that you have the properly formatted JoyDoc JSON you can pass that directly to the Joyfill SDK to render your form and capture data from your users. **Important Note:** When using a custom creation experience we recommend reviewing the update workflows below. You will need to evaluate enabling/disabling update PDF workflows after the the initial creation experience. The update workflows are usually due to users updating the underlying PDF (text, style, images, etc). The update workflows allow your users to swap out the background PDF for the existing form instead of requiring your users to create an entirely new form. Review the below examples for handling the different PDF scenarios after initial creation. ## Summary Hopefully from the examples above you have a better idea of how to handle PDFs with Joyfill. You can mix and match these strategies to create your ideal workflow based on your application and user workflows. ## Support for signed urls If you are planning on using signed urls, we recommend this approach, then you will want to review our Working With Signed URLs Guide (Coming soon). You can utilize all the above workflows with the signed url approach. # Populating and Extracting Data Source: https://docs.joyfill.io/web/guides/populating-and-extracting-data This guide explains how to populate and extract data from JoyDoc, focusing on text fields and table fields. ## 1. Accessing Fields You can access fields using `doc.fields`. The `fields` property contains an array of all field objects in the document. ```jsx theme={null} // Access all fields const allFields = doc.fields; // Example field structure { "_id": "name-field", "identifier": "name", "type": "text", "title": "Your Name", "value": "", "file": "file1" } ``` ## 2. Finding Fields Fields can be found using their `fieldId` (which is the `_id` property of the field). ```jsx theme={null} // Find a specific field by its ID const findFieldById = (doc, fieldId) => { return doc.fields.find(field => field._id === fieldId); }; // Example usage const nameField = findFieldById(doc, "name-field"); console.log(nameField.title); // "Your Name" ``` ## 3. Extracting Data for Non-Table Fields (Text) At any moment, you can get the latest document from `JoyDoc.onChange` and extract text field values using the fieldId. ```jsx theme={null} // In your JoyDoc component { // Extract text field value const textField = updatedDoc.fields.find( (field) => field._id === "name-field" ); const textValue = textField.value; console.log("Text field value:", textValue); }} />; ``` ## 4. Populating Data You can populate fields by injecting values into `doc.fields[].value`. Here's how to populate text fields: ```jsx theme={null} // Populate text fields const populateTextFields = (doc) => { const updatedDoc = { ...doc }; const updatedFields = [...doc.fields]; // Find and populate specific text fields const nameField = updatedFields.find(field => field._id === "name-field"); if (nameField) { nameField.value = "John Doe"; } const emailField = updatedFields.find(field => field._id === "email-field"); if (emailField) { emailField.value = "john.doe@example.com"; } updatedDoc.fields = updatedFields; return updatedDoc; }; // Example usage const populatedDoc = populateTextFields(doc); ``` ## 5. Extracting Data for Table Fields You can get a table field value by mapping through fields and accessing the table field's `value` using the table fieldId. ```jsx theme={null} // Extract table field data const extractTableData = (doc, tableFieldId) => { const tableField = doc.fields.find((field) => field._id === tableFieldId); if (tableField && tableField.type === "table") { return tableField.value.map((row) => { const rowData = {}; // Extract data from each cell Object.keys(row.cells).forEach((columnId) => { const column = tableField.tableColumns.find( (col) => col._id === columnId ); rowData[column.title] = row.cells[columnId]; }); return rowData; }); } return []; }; // Example usage const tableData = extractTableData(doc, "68ef815e4c716f176ea8d2ba"); console.log("Table data:", tableData); ``` ## 6. Populating Table Fields You can populate table rows by updating the `value.cells` property. Here's how to populate table fields: ```jsx theme={null} // Populate table fields const populateTableFields = (doc, tableFieldId) => { const updatedDoc = { ...doc }; const updatedFields = [...doc.fields]; const tableField = updatedFields.find((field) => field._id === tableFieldId); if (tableField && tableField.type === "table") { // Create cells object with column data const cells = {}; tableField.tableColumns.forEach((column) => { if (column.type === "text") { cells[column._id] = "Sample text data"; } else if (column.type === "dropdown") { cells[column._id] = column.options[0]._id; // Select first option } }); // Create new rows with populated cells const newRows = [ { _id: "row1", deleted: false, cells: { ...cells }, }, { _id: "row2", deleted: false, cells: { ...cells }, }, ]; tableField.value = newRows; tableField.rowOrder = ["row1", "row2"]; } updatedDoc.fields = updatedFields; return updatedDoc; }; // Example usage const populatedTableDoc = populateTableFields(doc, "68ef815e4c716f176ea8d2ba"); ``` ## Complete Example Here's a complete example showing both population and extraction: ```jsx theme={null} import React, { useState } from "react"; import JoyDoc from "./JoyDoc"; const MyForm = () => { const [doc, setDoc] = useState(initialDoc); // Populate data function const populateData = () => { const updatedDoc = { ...doc }; const updatedFields = [...doc.fields]; // Populate text field const nameField = updatedFields.find((field) => field._id === "name-field"); if (nameField) { nameField.value = "Jane Smith"; } // Populate table field const tableField = updatedFields.find( (field) => field._id === "68ef815e4c716f176ea8d2ba" ); if (tableField && tableField.type === "table") { const cells = {}; tableField.tableColumns.forEach((column) => { if (column.type === "text") { cells[column._id] = "Table cell data"; } }); tableField.value = [{ _id: "row1", deleted: false, cells: { ...cells } }]; tableField.rowOrder = ["row1"]; } updatedDoc.fields = updatedFields; setDoc(updatedDoc); }; // Extract data function const extractData = () => { // Extract text field const nameField = doc.fields.find((field) => field._id === "name-field"); console.log("Name:", nameField?.value); // Extract table data const tableField = doc.fields.find( (field) => field._id === "68ef815e4c716f176ea8d2ba" ); if (tableField && tableField.type === "table") { const tableData = tableField.value.map((row) => { const rowData = {}; Object.keys(row.cells).forEach((columnId) => { const column = tableField.tableColumns.find( (col) => col._id === columnId ); rowData[column.title] = row.cells[columnId]; }); return rowData; }); console.log("Table data:", tableData); } }; return (
{ setDoc(updatedDoc); }} />
); }; ``` ## Key Points to Remember * **Field Access**: Use `doc.fields` to access all fields * **Field Finding**: Use `field._id` to find specific fields * **Text Fields**: Access value directly with `field.value` * **Table Fields**: Access rows through `field.value` array, each row has `cells` object * **Population**: Modify `field.value` for text fields, or `field.value[].cells` for table fields * **Extraction**: Use `onChange` callback to get the latest document state * **Immutable Updates**: Always create new objects when updating to avoid mutation issues # Required Field Validation Source: https://docs.joyfill.io/web/guides/required-field-validation ## Overview JoyDoc provides validation for required fields, ensuring users complete necessary information before submitting forms. The validation system respects conditional logic and field visibility. ## How It Works ### Basic Usage ```jsx theme={null} import { validator } from '@joyfill/components'; // Validate a document const validation = validator.validate(doc); ``` ### Validation Logic 1. **Hidden fields** are always valid (respects conditional logic) 2. **Non-required fields** are always valid 3. **Required fields** with empty values are invalid 4. **Required fields** with values are valid ### Empty Value Detection * **Text/Number/Dropdown**: Empty if `""`, `null`, or `undefined` * **Arrays (image, file, multiSelect)**: Empty if not array, empty array, or all items are empty strings * **Table :** Empty if field.value is an empty array ## Configuring Required Fields Set `required: true` on any field: ```jsx theme={null} { "_id": "name-field", "type": "text", "title": "Full Name", "value": "", "required": true, // Makes field required "file": "file1" } ``` ## Validation Results ```jsx theme={null} { status: 'valid' | 'invalid' | null, fieldValidations: [ { field: { /* field object */ }, status: 'valid' | 'invalid' } ] } ``` **Status meanings:** * `'valid'`: All required fields filled * `'invalid'`: One or more required fields empty * `null`: Invalid document structure ## Implementation Example ```jsx theme={null} import React, { useEffect, useState } from "react"; import { JoyDoc, validator } from "@joyfill/components"; function JoyDocPage() { const [doc, setDoc] = useState(null); const [validationResult, setValidationResult] = useState(null); useEffect(() => { const getDocument = async () => { try { const response = await fetch("YOUR_API_ENDPOINT", { method: "GET", headers: { Authorization: "Bearer YOUR_TOKEN", "Content-Type": "text/JSON", }, }); const data = await response.json(); setDoc(data); // Validate the document if (data) { const validation = validator.validate(data); setValidationResult(validation); } } catch (e) { console.log(e); } }; getDocument(); }, []); return (
{/* Validation Status Display */} {validationResult && (

Validation Status:{" "} {validationResult.status?.toUpperCase() || "NULL"}

{validationResult.status === "invalid" && (

Invalid Fields ( { validationResult.fieldValidations.filter( (f) => f.status === "invalid" ).length } ):

    {validationResult.fieldValidations .filter((fv) => fv.status === "invalid") .map((fieldValidation, index) => (
  • {fieldValidation.field?.title || fieldValidation.field?._id} ({fieldValidation.field?.type})
  • ))}
)}
Raw Validation Data
              {JSON.stringify(validationResult, null, 2)}
            
)} { // Re-validate when document changes if (doc) { const validation = validator.validate(doc); setValidationResult(validation); } }} />
); } export default JoyDocPage; ``` ## Key Points * **Hidden fields are always valid** (respects conditional logic) * **Validate on document changes** using the `onChange` callback * **Use visual feedback** to show validation status to users * **Debug with raw validation data** when troubleshooting # Angular Source: https://docs.joyfill.io/web/guides/sample-projects-angular ## Overview This guide shows you how to integrate Joyfill Components into your Angular v17+ application. ## Setup Welcome to Joyfill! It's easy to get started with a Joyfill Developer account and add powerful form and digital PDF capabilities to your product or service. Just follow the steps below and you'll be up and running shortly. ### Setup Steps The steps below are the prerequisites you will need to accomplish before moving on to the rest of this guide.A couple of the steps below will be done in our [Joyfill Manager](https://app-joy.joyfill.io). **Step 1: Create Joyfill Account** * To begin working with Joyfill, go to [Joyfill's Platform](https://app-joy.joyfill.io) and create an account (jump to step 2 if you already have an account). * By creating an account you will add yourself as the first user to your newly created Joyfill Organization and be placed inside the Joyfill Manager. **Step 2: Generate Your userAccessToken** * Once you're inside the Joyfill Manager you will want to select from the top navigation bar Settings & Users -> Manager Users -> and click "Access Tokens" button next to your user. * Once the modal appears select "Add Access Token". Copy and securely store your access token for later use. **Step 3: Create Your First Template** Create your first template within the Joyfill Manager by going to the Template Library tab in the top navigation and click the "Add Template". We recommended following this guide Create Your First Template when creating your first template. This makes it easy to experiment with the different parts of the Joyfill Platform easily. **Step 4: Get your template identifier** Inside the Joyfill Manager you will want to select from the top navigation bar Template Library and under your Templates table list within the column ID copy that for pasting into our example Angular guides. ## Requirements * Angular v17+ * Review SDK README ## 🚀 Quick Start ### 1. Install Install the Joyfill Components package: ```bash theme={null} npm install --save @joyfill/components ``` or ```bash theme={null} yarn add @joyfill/components ``` ### 2. Usage #### Create Joyfill Service Create `src/app/joyfill.service.ts`: ```typescript theme={null} import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class JoyfillService { url = 'https://api-joy.joyfill.io/v1'; headers = { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' } constructor() { } async getTemplate(identifier: string): Promise { const data = await fetch(`${this.url}/templates/${identifier}`, { method: 'GET', headers: this.headers }); return await data.json(); } async updateTemplate(identifier: string, data: object): Promise { const response = await fetch(`${this.url}/templates/${identifier}`, { method: 'POST', headers: this.headers, body: JSON.stringify(data) }); return await response.json(); } } ``` #### Update App Component Update `src/app/app.component.ts`: **❗️Important Note:** ensure you use the full import path `import { JoyDoc } from '@joyfill/components/dist/joyfill.min.js'`. This will ensure proper angular support. ```typescript theme={null} import { Component, OnInit, inject } from '@angular/core'; import { CommonModule } from '@angular/common'; import { JoyDoc } from '@joyfill/components/dist/joyfill.min.js'; import { JoyfillService } from './joyfill.service'; @Component({ selector: 'app-root', standalone: true, imports: [CommonModule], template: `
`, styleUrl: './app.component.css' }) export class AppComponent implements OnInit { joyfillService: JoyfillService = inject(JoyfillService); identifer = ''; pendingTemplate = {}; ngOnInit() { this.initJoyfill(); } async initJoyfill() { const template = await this.joyfillService.getTemplate(this.identifer); JoyDoc( document.getElementById('joyfill-target'), { doc: template, onChange: (changelogs, data) => { /** * changelogs - the individual changes * data - the entire new template with all changes applied */ console.log(changelogs, data) this.pendingTemplate = data; } } ); } async handleSave() { const updatedTemplate = await this.joyfillService.updateTemplate(this.identifer, this.pendingTemplate); console.log('>>>>>>>>>>> updatedTemplate: ', updatedTemplate); } } ``` ### Typescript Configuration We have added the properties below to the "compilerOptions" in the `tsconfig.json` file in order to support the Joyfill JS SDK. ```json theme={null} { "compilerOptions": { "allowJs": true, "noImplicitAny": false } } ``` ## Complete Project Structure ``` my-joyfill-app/ ├── src/ │ ├── app/ │ │ ├── joyfill.service.ts # Joyfill API service │ │ ├── app.component.ts # Main app component │ │ └── app.component.css # App styles │ └── main.ts # Entry point ├── tsconfig.json # TypeScript configuration └── package.json # Dependencies ``` ## Complete Angular Example ### app.component.ts ```typescript theme={null} import { Component, OnInit, inject } from '@angular/core'; import { CommonModule } from '@angular/common'; import { JoyDoc } from '@joyfill/components/dist/joyfill.min.js'; import { JoyfillService } from './joyfill.service'; @Component({ selector: 'app-root', standalone: true, imports: [CommonModule], template: `

🎉 Joyfill Components

Angular v17+ Integration Example

`, styleUrl: './app.component.css' }) export class AppComponent implements OnInit { joyfillService: JoyfillService = inject(JoyfillService); identifer = ''; pendingTemplate = {}; isSaving = false; ngOnInit() { this.initJoyfill(); } async initJoyfill() { try { const template = await this.joyfillService.getTemplate(this.identifer); JoyDoc( document.getElementById('joyfill-target'), { doc: template, onChange: (changelogs, data) => { /** * changelogs - the individual changes * data - the entire new template with all changes applied */ console.log(changelogs, data); this.pendingTemplate = data; }, onError: (error) => { console.error('Joyfill error:', error); } } ); } catch (error) { console.error('Error initializing Joyfill:', error); } } async handleSave() { if (this.isSaving) return; this.isSaving = true; try { const updatedTemplate = await this.joyfillService.updateTemplate( this.identifer, this.pendingTemplate ); console.log('Updated template:', updatedTemplate); alert('Template saved successfully!'); } catch (error) { console.error('Error saving template:', error); alert('Error saving template'); } finally { this.isSaving = false; } } } ``` ### app.component.css ```css theme={null} main { max-width: 1200px; margin: 0 auto; padding: 20px; } header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 20px; color: white; margin-bottom: 20px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center; } header h1 { margin: 0; font-size: 2rem; } header p { margin: 10px 0 0 0; opacity: 0.9; } button { background: white; color: #667eea; border: none; padding: 12px 24px; border-radius: 4px; cursor: pointer; font-size: 16px; font-weight: 600; } button:disabled { opacity: 0.6; cursor: not-allowed; } button:hover:not(:disabled) { background: #f0f0f0; } #joyfill-target { background: white; border-radius: 8px; padding: 30px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } ``` ### joyfill.service.ts ```typescript theme={null} import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class JoyfillService { private url = 'https://api-joy.joyfill.io/v1'; private headers = { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' } constructor() { } async getTemplate(identifier: string): Promise { try { const response = await fetch(`${this.url}/templates/${identifier}`, { method: 'GET', headers: this.headers }); if (!response.ok) { throw new Error(`Failed to fetch template: ${response.statusText}`); } return await response.json(); } catch (error) { console.error('Error fetching template:', error); throw error; } } async updateTemplate(identifier: string, data: object): Promise { try { const response = await fetch(`${this.url}/templates/${identifier}`, { method: 'POST', headers: this.headers, body: JSON.stringify(data) }); if (!response.ok) { throw new Error(`Failed to update template: ${response.statusText}`); } return await response.json(); } catch (error) { console.error('Error updating template:', error); throw error; } } } ``` ### tsconfig.json ```json theme={null} { "compileOnSave": false, "compilerOptions": { "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "allowJs": true, "noImplicitAny": false, "esModuleInterop": true, "sourceMap": true, "declaration": false, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "ES2022", "module": "ES2022", "lib": [ "ES2022", "dom" ] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true } } ``` ## Common Configuration Options The `JoyDoc` function accepts the following configuration options: ```typescript theme={null} JoyDoc( document.getElementById('joyfill-target'), { doc: template, // Your template JSON mode: 'edit', // 'edit' | 'fill' | 'readonly' view: 'desktop', // 'desktop' | 'mobile' | 'tablet' width: 800, // Component width height: 600, // Component height theme: { // Custom theme primaryColor: '#007bff', secondaryColor: '#6c757d' }, features: { // Feature flags formulas: true, readableIds: false, validateSchema: false }, onChange: (changelogs, data) => { // Handle form changes // changelogs - the individual changes // data - the entire new template with all changes applied }, onError: (error) => { // Handle errors }, onFocus: (fieldId, fieldData) => { // Handle field focus }, onBlur: (fieldId, fieldData) => { // Handle field blur }, onCaptureAsync: async (fieldId, data) => { // Handle field capture return data; }, onUploadAsync: async (file) => { // Handle file upload return { url: 'uploaded-file-url' }; } } ); ``` ## Try it yourself If you're looking for a full example project that shows many more of the Joyfill SDK capabilities and workflows then head over to our full example project and try it for yourself. * [Joyfill Angular Example](https://github.com/joyfill/examples/tree/main/angular) * [SDK NPM Package](https://www.npmjs.com/package/@joyfill/components) ## That's it! 🎉 Your Angular v17+ application with Joyfill Components is now ready. The form will render with proper Angular dependency injection and lifecycle management. # JS CDN Source: https://docs.joyfill.io/web/guides/sample-projects-js-cdn ## Overview This guide shows you how to integrate Joyfill Components into your web application using the CDN Approach. ## 🚀 Quick Start 1. Identify the version of joyfill/components you want to test and copy the script url using the following steps 2. Open [https://www.jsdelivr.com/package/npm/@joyfill/components?tab=files](https://www.jsdelivr.com/package/npm/@joyfill/components?tab=files) and pick the version that works for you on the right 3. We are choosing `4.0.0-rc4` in this example 4. You will find the script url under the default tab ## Include the CDN Script Add this script tag to your HTML `` section: ```html theme={null} ``` ## Create a Container Element ```html theme={null}
``` ## Load a Form Here is a sample contact form that you could use: ```html theme={null} ``` ## Complete HTML Example ```html theme={null} Joyfill Contact Form

Contact Us

``` ## That's it! 🎉 Your contact form is now ready. The `onChange` callback will fire whenever the user makes changes to the form. # JS Module Source: https://docs.joyfill.io/web/guides/sample-projects-js-module ## Overview This guide shows you how to integrate Joyfill Components into your JavaScript application using ES modules and a build tool like Parcel. ## 🚀 Quick Start ### Setup Project Create a new project and install dependencies: ```bash theme={null} mkdir my-joyfill-app cd my-joyfill-app npm init -y npm install @joyfill/components@latest npm install --save-dev parcel ``` ### Create HTML File Create `src/index.html`: ```html theme={null} Joyfill Form - ES Module

Contact Form

``` ### Create JavaScript Module Create `src/index.js`: ```javascript theme={null} import Joyfill from "@joyfill/components/dist/joyfill.min.js"; // Complete contact form example const contactForm = { "_id": "68f231604484695c16d65f97", "identifier": "doc_68f231604484695c16d65f97", "name": "New Doc", "files": [ { "_id": "68f231607dee0bbd73994f87", "name": "New File", "pageOrder": [ "68f23160b929e32e60e663a0" ], "pages": [ { "_id": "68f23160b929e32e60e663a0", "name": "New Page", "width": 816, "height": 1056, "rowHeight": 8, "cols": 8, "fieldPositions": [ { "_id": "68f23164c57b7fe946d32b1b", "type": "text", "displayType": "original", "x": 0, "y": 0, "width": 4, "height": 8, "field": "68f23164886cdb084afec912" }, { "_id": "68f231663b6a898390b23fec", "type": "textarea", "displayType": "original", "x": 4, "y": 0, "width": 4, "height": 23, "field": "68f2316612113f6141fac40a" }, { "_id": "68f231673dc5e58c883c8437", "type": "number", "displayType": "original", "x": 0, "y": 8, "width": 4, "height": 8, "field": "68f23167998040ea25a54fa8" }, { "_id": "68f231796f5ea38ae37f951b", "type": "text", "displayType": "original", "x": 0, "y": 16, "width": 4, "height": 8, "field": "68f23179479739cf0883d27e" } ], "layout": "grid", "presentation": "normal", "padding": 24 } ], "styles": { "margin": 4 } } ], "fields": [ { "file": "68f231607dee0bbd73994f87", "_id": "68f23164886cdb084afec912", "type": "text", "title": "Name", "identifier": "field_68f23164886cdb084afec912" }, { "file": "68f231607dee0bbd73994f87", "_id": "68f2316612113f6141fac40a", "type": "textarea", "title": "Comments", "identifier": "field_68f2316612113f6141fac40a", "value": "" }, { "file": "68f231607dee0bbd73994f87", "_id": "68f23167998040ea25a54fa8", "type": "number", "title": "Phone number", "identifier": "field_68f23167998040ea25a54fa8" }, { "file": "68f231607dee0bbd73994f87", "_id": "68f23179479739cf0883d27e", "type": "text", "title": "Email", "identifier": "field_68f23179479739cf0883d27e" } ], "type": "document" }; // Initialize the form Joyfill.JoyDoc( document.getElementById('joyfill-container'), { doc: contactForm, mode: 'edit', onChange: (changelogs, doc) => { console.log('Form updated:', doc); } } ); ``` ### Add Package.json Scripts Update your `package.json`: ```json theme={null} { "name": "my-joyfill-app", "version": "1.0.0", "scripts": { "start": "parcel src/index.html", "build": "parcel build src/index.html" }, "dependencies": { "@joyfill/components": "latest" }, "devDependencies": { "parcel": "^2.8.2" } } ``` ### Start Development Server ```bash theme={null} npm start ``` Open `http://localhost:1234` in your browser. ## Complete Project Structure ``` my-joyfill-app/ ├── src/ │ ├── index.html # Main HTML file │ └── index.js # JavaScript module ├── package.json # Project configuration └── dist/ # Built files (auto-generated) ``` ## Complete HTML Example ```html theme={null} Joyfill Form - ES Module

Contact Us

``` ## Complete JavaScript Example ```javascript theme={null} import Joyfill from "@joyfill/components/dist/joyfill.min.js"; // Complete contact form with validation const contactForm = { "_id": "68f231604484695c16d65f97", "identifier": "doc_68f231604484695c16d65f97", "name": "New Doc", "files": [ { "_id": "68f231607dee0bbd73994f87", "name": "New File", "pageOrder": [ "68f23160b929e32e60e663a0" ], "pages": [ { "_id": "68f23160b929e32e60e663a0", "name": "New Page", "width": 816, "height": 1056, "rowHeight": 8, "cols": 8, "fieldPositions": [ { "_id": "68f23164c57b7fe946d32b1b", "type": "text", "displayType": "original", "x": 0, "y": 0, "width": 4, "height": 8, "field": "68f23164886cdb084afec912" }, { "_id": "68f231663b6a898390b23fec", "type": "textarea", "displayType": "original", "x": 4, "y": 0, "width": 4, "height": 23, "field": "68f2316612113f6141fac40a" }, { "_id": "68f231673dc5e58c883c8437", "type": "number", "displayType": "original", "x": 0, "y": 8, "width": 4, "height": 8, "field": "68f23167998040ea25a54fa8" }, { "_id": "68f231796f5ea38ae37f951b", "type": "text", "displayType": "original", "x": 0, "y": 16, "width": 4, "height": 8, "field": "68f23179479739cf0883d27e" } ], "layout": "grid", "presentation": "normal", "padding": 24 } ], "styles": { "margin": 4 } } ], "fields": [ { "file": "68f231607dee0bbd73994f87", "_id": "68f23164886cdb084afec912", "type": "text", "title": "Name", "identifier": "field_68f23164886cdb084afec912" }, { "file": "68f231607dee0bbd73994f87", "_id": "68f2316612113f6141fac40a", "type": "textarea", "title": "Comments", "identifier": "field_68f2316612113f6141fac40a", "value": "" }, { "file": "68f231607dee0bbd73994f87", "_id": "68f23167998040ea25a54fa8", "type": "number", "title": "Phone number", "identifier": "field_68f23167998040ea25a54fa8" }, { "file": "68f231607dee0bbd73994f87", "_id": "68f23179479739cf0883d27e", "type": "text", "title": "Email", "identifier": "field_68f23179479739cf0883d27e" } ], "type": "document" }; // Initialize the form const joyDocInstance = Joyfill.JoyDoc( document.getElementById('joyfill-container'), { doc: contactForm, mode: 'edit', onChange: (changelogs, doc) => { console.log('Form updated:', doc); console.log('Changes:', changelogs); // Save form data saveFormData(doc); }, onError: (error) => { console.error('Form error:', error); } } ); // Helper function to save form data async function saveFormData(doc) { try { const response = await fetch('/api/contact', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(doc) }); if (response.ok) { console.log('Form saved successfully'); } else { console.error('Failed to save form'); } } catch (error) { console.error('Error saving form:', error); } } ``` ## Build for Production ```bash theme={null} npm run build ``` This creates optimized files in the `dist/` folder ready for deployment. ## Common Configuration Options ```javascript theme={null} Joyfill.JoyDoc(container, { doc: yourFormData, // Your form JSON mode: 'edit', // 'edit' | 'view' | 'preview' view: 'desktop', // 'desktop' | 'mobile' | 'tablet' width: 800, // Component width height: 600, // Component height onChange: (changes, doc) => { // Handle form changes }, onError: (error) => { // Handle errors }, onFocus: (fieldId, fieldData) => { // Handle field focus }, onBlur: (fieldId, fieldData) => { // Handle field blur } }); ``` ## That's it! 🎉 Your form is now ready with ES modules. The build process will bundle everything for production deployment. # React Source: https://docs.joyfill.io/web/guides/sample-projects-react ## Overview This guide shows you how to integrate Joyfill Components into your React v18 application. ## 🚀 Quick Start ### Setup React Project Create a new React project: ```bash theme={null} npx create-react-app my-joyfill-app cd my-joyfill-app npm install @joyfill/components ``` ### Create JoyDoc Component Create `src/components/JoyDocForm.js`: ```jsx theme={null} import React, { useState } from 'react'; import { JoyDoc } from '@joyfill/components'; const JoyDocForm = () => { const [doc, setDoc] = useState(null); // Complete contact form example const contactForm = { "_id": "68f231604484695c16d65f97", "identifier": "doc_68f231604484695c16d65f97", "name": "New Doc", "files": [ { "_id": "68f231607dee0bbd73994f87", "name": "New File", "pageOrder": [ "68f23160b929e32e60e663a0" ], "pages": [ { "_id": "68f23160b929e32e60e663a0", "name": "New Page", "width": 816, "height": 1056, "rowHeight": 8, "cols": 8, "fieldPositions": [ { "_id": "68f23164c57b7fe946d32b1b", "type": "text", "displayType": "original", "x": 0, "y": 0, "width": 4, "height": 8, "field": "68f23164886cdb084afec912" }, { "_id": "68f231663b6a898390b23fec", "type": "textarea", "displayType": "original", "x": 4, "y": 0, "width": 4, "height": 23, "field": "68f2316612113f6141fac40a" }, { "_id": "68f231673dc5e58c883c8437", "type": "number", "displayType": "original", "x": 0, "y": 8, "width": 4, "height": 8, "field": "68f23167998040ea25a54fa8" }, { "_id": "68f231796f5ea38ae37f951b", "type": "text", "displayType": "original", "x": 0, "y": 16, "width": 4, "height": 8, "field": "68f23179479739cf0883d27e" } ], "layout": "grid", "presentation": "normal", "padding": 24 } ], "styles": { "margin": 4 } } ], "fields": [ { "file": "68f231607dee0bbd73994f87", "_id": "68f23164886cdb084afec912", "type": "text", "title": "Name", "identifier": "field_68f23164886cdb084afec912" }, { "file": "68f231607dee0bbd73994f87", "_id": "68f2316612113f6141fac40a", "type": "textarea", "title": "Comments", "identifier": "field_68f2316612113f6141fac40a", "value": "" }, { "file": "68f231607dee0bbd73994f87", "_id": "68f23167998040ea25a54fa8", "type": "number", "title": "Phone number", "identifier": "field_68f23167998040ea25a54fa8" }, { "file": "68f231607dee0bbd73994f87", "_id": "68f23179479739cf0883d27e", "type": "text", "title": "Email", "identifier": "field_68f23179479739cf0883d27e" } ], "type": "document" }; const handleChange = (changelogs, updatedDoc) => { console.log('Form updated:', updatedDoc); console.log('Changes:', changelogs); setDoc(updatedDoc); }; const handleError = (error) => { console.error('Form error:', error); }; return (

Contact Form

); }; export default JoyDocForm; ``` ### Update App Component Update `src/App.js`: ```jsx theme={null} import React from 'react'; import JoyDocForm from './components/JoyDocForm'; import './App.css'; function App() { return (
); } export default App; ``` ### Start Development Server ```bash theme={null} npm start ``` Open `http://localhost:3000` in your browser. ## Complete Project Structure ``` my-joyfill-app/ ├── src/ │ ├── components/ │ │ └── JoyDocForm.js # JoyDoc component │ ├── App.js # Main app component │ ├── App.css # App styles │ └── index.js # Entry point ├── public/ │ └── index.html # HTML template └── package.json # Dependencies ``` ## Complete React Example ### App.js ```jsx theme={null} import React from 'react'; import JoyDocForm from './components/JoyDocForm'; import './App.css'; function App() { return (

🎉 Joyfill Components

React v18 Integration Example

); } export default App; ``` ### App.css ```css theme={null} .App { text-align: center; min-height: 100vh; background-color: #f5f5f5; } .App-header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 20px; color: white; margin-bottom: 20px; } .App-header h1 { margin: 0; font-size: 2rem; } .App-header p { margin: 10px 0 0 0; opacity: 0.9; } main { max-width: 1200px; margin: 0 auto; padding: 0 20px; } ``` ### JoyDocForm.js ```jsx theme={null} import React, { useState, useCallback } from 'react'; import { JoyDoc } from '@joyfill/components'; const JoyDocForm = () => { const [doc, setDoc] = useState(null); const [isLoading, setIsLoading] = useState(false); // Complete contact form example const contactForm = { "_id": "68f231604484695c16d65f97", "identifier": "doc_68f231604484695c16d65f97", "name": "New Doc", "files": [ { "_id": "68f231607dee0bbd73994f87", "name": "New File", "pageOrder": [ "68f23160b929e32e60e663a0" ], "pages": [ { "_id": "68f23160b929e32e60e663a0", "name": "New Page", "width": 816, "height": 1056, "rowHeight": 8, "cols": 8, "fieldPositions": [ { "_id": "68f23164c57b7fe946d32b1b", "type": "text", "displayType": "original", "x": 0, "y": 0, "width": 4, "height": 8, "field": "68f23164886cdb084afec912" }, { "_id": "68f231663b6a898390b23fec", "type": "textarea", "displayType": "original", "x": 4, "y": 0, "width": 4, "height": 23, "field": "68f2316612113f6141fac40a" }, { "_id": "68f231673dc5e58c883c8437", "type": "number", "displayType": "original", "x": 0, "y": 8, "width": 4, "height": 8, "field": "68f23167998040ea25a54fa8" }, { "_id": "68f231796f5ea38ae37f951b", "type": "text", "displayType": "original", "x": 0, "y": 16, "width": 4, "height": 8, "field": "68f23179479739cf0883d27e" } ], "layout": "grid", "presentation": "normal", "padding": 24 } ], "styles": { "margin": 4 } } ], "fields": [ { "file": "68f231607dee0bbd73994f87", "_id": "68f23164886cdb084afec912", "type": "text", "title": "Name", "identifier": "field_68f23164886cdb084afec912" }, { "file": "68f231607dee0bbd73994f87", "_id": "68f2316612113f6141fac40a", "type": "textarea", "title": "Comments", "identifier": "field_68f2316612113f6141fac40a", "value": "" }, { "file": "68f231607dee0bbd73994f87", "_id": "68f23167998040ea25a54fa8", "type": "number", "title": "Phone number", "identifier": "field_68f23167998040ea25a54fa8" }, { "file": "68f231607dee0bbd73994f87", "_id": "68f23179479739cf0883d27e", "type": "text", "title": "Email", "identifier": "field_68f23179479739cf0883d27e" } ], "type": "document" }; const handleChange = useCallback((changelogs, updatedDoc) => { console.log('Form updated:', updatedDoc); console.log('Changes:', changelogs); setDoc(updatedDoc); }, []); const handleError = useCallback((error) => { console.error('Form error:', error); }, []); const handleFocus = useCallback((fieldId, fieldData) => { console.log('Field focused:', fieldId); }, []); const handleBlur = useCallback((fieldId, fieldData) => { console.log('Field blurred:', fieldId); }, []); const saveForm = async () => { if (!doc) return; setIsLoading(true); try { const response = await fetch('/api/contact', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(doc) }); if (response.ok) { console.log('Form saved successfully'); alert('Form saved successfully!'); } else { console.error('Failed to save form'); alert('Failed to save form'); } } catch (error) { console.error('Error saving form:', error); alert('Error saving form'); } finally { setIsLoading(false); } }; return (

Contact Us

); }; export default JoyDocForm; ``` ## Advanced React Integration ### Custom Hook for Form Management Create `src/hooks/useJoyDocForm.js`: ```jsx theme={null} import { useState, useCallback } from 'react'; export const useJoyDocForm = (initialDoc) => { const [doc, setDoc] = useState(initialDoc); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const handleChange = useCallback((changelogs, updatedDoc) => { setDoc(updatedDoc); setError(null); }, []); const handleError = useCallback((error) => { setError(error); }, []); const saveForm = useCallback(async (saveUrl) => { if (!doc) return; setIsLoading(true); setError(null); try { const response = await fetch(saveUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(doc) }); if (!response.ok) { throw new Error('Failed to save form'); } return await response.json(); } catch (error) { setError(error); throw error; } finally { setIsLoading(false); } }, [doc]); return { doc, isLoading, error, handleChange, handleError, saveForm }; }; ``` ### Using the Custom Hook ```jsx theme={null} import React from 'react'; import { JoyDoc } from '@joyfill/components'; import { useJoyDocForm } from '../hooks/useJoyDocForm'; const ContactForm = () => { const contactForm = { // ... your form data }; const { doc, isLoading, error, handleChange, handleError, saveForm } = useJoyDocForm(contactForm); const handleSave = async () => { try { await saveForm('/api/contact'); alert('Form saved successfully!'); } catch (error) { alert('Error saving form'); } }; return (
{error && (
Error: {error.message}
)}
); }; export default ContactForm; ``` ## Common Configuration Options ```jsx theme={null} { // Handle form changes }} onError={(error) => { // Handle errors }} onFocus={(fieldId, fieldData) => { // Handle field focus }} onBlur={(fieldId, fieldData) => { // Handle field blur }} onCaptureAsync={async (fieldId, data) => { // Handle field capture return data; }} onUploadAsync={async (file) => { // Handle file upload return { url: 'uploaded-file-url' }; }} /> ``` ## That's it! 🎉 Your React v18 application with Joyfill Components is now ready. The form will render with proper React state management and event handling. # Schema Validation Source: https://docs.joyfill.io/web/guides/schema-validation Schema validation in JoyDoc ensures that documents conform to the expected structure and version compatibility. It validates the document's JSON schema against the official JoyDoc schema definition and checks version compatibility. ### Manual Schema Validation You can also validate schemas manually: ```jsx theme={null} import { validateSchema } from '@joyfill/components'; const schemaError = validateSchema(doc); if (schemaError) { console.log('Schema validation failed:', schemaError); } else { console.log('Document schema is valid'); } ``` ## Enabling/Disabling Schema Validation ### Default Behavior Schema validation is **enabled by default** in the JoyDoc component. ### Enabling Schema Validation Schema validation is enabled by default, but you can explicitly enable it: ```jsx theme={null} ``` ### Disabling Schema Validation To disable schema validation, set `features.validateSchema` to `false`: ```jsx theme={null} ``` ## How Schema Validation Works ### Basic Usage Schema validation is automatically performed by the JoyDoc component when enabled: ```jsx theme={null} import { JoyDoc } from '@joyfill/components'; // Schema validation is enabled by default { if (error.code === 'ERROR_SCHEMA_VALIDATION') { console.log('Schema validation failed:', error); } }} /> ``` ## Schema Validation Process The validation process follows these steps: 1. **Version Compatibility Check**: Verifies the document's schema version is compatible with the current SDK 2. **JSON Schema Validation**: Validates the document structure against the official JoyDoc schema 3. **Error Reporting**: Returns detailed error information if validation fails ## Validation Results ### Success Case When validation passes, the function returns `undefined`: ```jsx theme={null} const result = validateSchema(validDoc); console.log(result); // undefined (no errors) ``` ### Error Cases When validation fails, an error object is returned with the following structure: ```jsx theme={null} { code: 'ERROR_SCHEMA_VALIDATION' | 'ERROR_SCHEMA_VERSION', message: 'Error description', details: { schemaVersion: '2.0.0', sdkVersion: '3.1.4' }, errors?: [...] // Only present for schema validation errors } ``` ## Error Types ### 1. Schema Version Error **Code**: `ERROR_SCHEMA_VERSION` Occurs when the document's schema version is incompatible with the current SDK version. ```jsx theme={null} { code: 'ERROR_SCHEMA_VERSION', message: 'Error detected with targeted schema version.', error: 'The targeted version of 1.0.0 is not supported. Version 2.0.0 must be used.', details: { schemaVersion: '2.0.0', sdkVersion: '3.1.4' } } ``` **Common causes:** * Using an outdated document format * Document created with an older SDK version * Manual document editing with incorrect version ### 2. Schema Validation Error **Code**: `ERROR_SCHEMA_VALIDATION` Occurs when the document structure doesn't match the expected schema. ```jsx theme={null} { code: 'ERROR_SCHEMA_VALIDATION', message: 'Error detected during schema validation', details: { schemaVersion: '2.0.0', sdkVersion: '3.1.4' }, errors: [ { instancePath: '/fields/0', schemaPath: '#/properties/fields/items/required', keyword: 'required', params: { missingProperty: 'type' }, message: "must have required property 'type'" } ] } ``` **Common causes:** * Missing required properties * Invalid field types * Malformed document structure * Corrupted document data ## Error Handling ### In JoyDoc Component Handle schema validation errors using the `onError` callback: ```jsx theme={null} { switch (error.code) { case 'ERROR_SCHEMA_VERSION': console.error('Schema version incompatible:', error.error); // Handle version incompatibility break; case 'ERROR_SCHEMA_VALIDATION': console.error('Schema validation failed:', error.errors); // Handle schema validation errors break; default: console.error('Unknown error:', error); } }} /> ``` ## Implementation Example ```jsx theme={null} import React, { useState, useEffect } from "react"; import { JoyDoc, validateSchema } from "@joyfill/components"; function DocumentEditor() { const [doc, setDoc] = useState(null); const [schemaError, setSchemaError] = useState(null); useEffect(() => { if (doc) { const error = validateSchema(doc); setSchemaError(error); } }, [doc]); const handleError = (error) => { if (error.code === "ERROR_SCHEMA_VALIDATION") { setSchemaError(error); } }; return (
{/* Schema Error Display */} {schemaError && (

Schema Validation Error

{schemaError.message}

{schemaError.code === "ERROR_SCHEMA_VERSION" && (

Error: {schemaError.error}

)}

Schema Version: {schemaError.details.schemaVersion}

SDK Version: {schemaError.details.sdkVersion}

{schemaError.errors && (
Validation Details
                {JSON.stringify(schemaError.errors, null, 2)}
              
)}
)}
); } export default DocumentEditor; ``` ## Key points: * **Schema validation is enabled by default** in JoyDoc components * **Version compatibility** is checked using major version numbers * **Two types of errors**: Schema version errors and schema validation errors * **Always handle errors** using the `onError` callback * **Validate documents** before processing to prevent runtime errors * **Use manual validation** for programmatic document validation