Skip to main content

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:
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 (
    <JoyDoc
      doc={document}
      onChange={handleChange}
      mode="fill"
    />
  );
}

Changelog Structure

Each changelog contains detailed information about the change:
{
  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

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

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

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

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

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

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

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:
NameTypeDescription
vNumberChangelog version number
sdkStringSpecifies the name of the SDK that generated the changelog object
targetStringSpecifies the target change that was made
_idStringSpecifies the target document _id for the change
identifierStringSpecifies the target document identifier for the change
fileIdStringSpecifies the target file _id for the change
pageIdStringSpecifies the target page _id for the change
fieldIdStringSpecifies the target field _id for the change
fieldIdentifierStringSpecifies the target field identifier for the change
fieldPositionIdStringSpecifies the target field position _id for the change
changeObjectObject containing the properties and values that should be applied for the change
createdOnNumberMillisecond 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:
{
  sdk: 'js',
  type: 'fieldPosition.focus',
  v: 1,
  _id: documentId,
  identifier: documentIdentifier,
  fileId: fileId,
  pageId: pageId,
  fieldId: fieldId,
  fieldPositionId: fieldPositionId,
  fieldIdentifier: fieldIdentifier
}

Usage Example:
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 (
    <JoyDoc
      doc={document}
      onFocus={handleFocus}
    />
  );
}

onBlur

Triggered when a field loses focus. Event Parameters:
  • params (Object): Blur context information
  • event (Event): The DOM blur event
Changelog Structure:
{
  sdk: 'js',
  v: 1,
  target: 'fieldPosition.blur',
  _id: documentId,
  identifier: documentIdentifier,
  fileId: fileId,
  pageId: pageId,
  fieldId: fieldId,
  fieldPositionId: fieldPositionId,
  fieldIdentifier: fieldIdentifier,
}

Usage Example:
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 (
    <JoyDoc
      doc={document}
      onBlur={handleBlur}
    />
  );
}

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:
{
  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:
{
  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:
{
  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:
{
  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:
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 (
    <JoyDoc
      doc={document}
      onChange={handleChange}
    />
  );
}

Capture Events

onCaptureAsync - Barcode Capture

Triggered when barcode capture is initiated (e.g., table barcode cell, barcode field). Function Signature:
onCaptureAsync() => Promise<string>

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:
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 (
    <JoyDoc
      doc={document}
      onCaptureAsync={handleCaptureAsync}
    />
  );
}

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:
onUploadAsync(params, files) => Promise<Array<ImageObject>>

Parameters:
  • params (Object): Upload context information
  • files (Array<File>): Array of File objects selected by the user
Params Structure: Normal Field Upload:
{
  "target": "field.update",
  "_id": "68ef74d29e5f80ece781b8ed",
  "identifier": "my-document",
  "fileId": "file1",
  "pageId": "page1",
  "fieldId": "68ef7569f4642997bd016fec",
  "fieldIdentifier": "field_68ef7569f4642997bd016fec",
  "fieldPositionId": "68ef75696b31bf638f74c074",
  "multi": true
}

Table Field Upload:
{
  "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:
[
  {
    "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:
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 (
    <JoyDoc
      doc={document}
      onChange={handleChange}
      onUploadAsync={handleUploadAsync}
    />
  );
}

// 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:

		onFileUploadAsync(params, files) => Promise<Array<ImageObject>>

Parameters:
  • params (Object): Upload context information
  • files (Array<File>): Array of File objects selected by the user
Usage Example:
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 (
    <JoyDoc
      doc={document}
      onChange={handleChange}
			fieldSettings={fieldSettings}
    />
  );
}

File Interaction Events

onFileClick

Triggered when a file is clicked or selected in an image field. Function Signature:
onFileClick(params, urlObject) => Promise<void>

Parameters:
  • params (Object): Context information about the file click
  • urlObject (Object): Information about the clicked file
Params Object:
{
  "_id": "68ef74d29e5f80ece781b8ed",
  "identifier": "my-document",
  "fileId": "file1",
  "pageId": "page1",
  "fieldId": "68ef81699ee8e13ed8dcd8e3",
  "fieldIdentifier": "field_68ef81699ee8e13ed8dcd8e3",
  "fieldPositionId": "68ef8169fb605e9648b562f1"
}

URL Object:
{
  "_id": "68ef8fb9874dbbcfd1189f88",
  "url": "<https://placehold.co/600x400>",
  "fileName": "68ef8fb38f7d1d3e8348f894-1760530355509.jpg",
  "filePath": "68a47e5d32dddce3ee2c31a5/documents/my-document"
}

Usage Example:
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 <JoyDoc doc={document} onFileClick={handleFileClick} />;
}

onFileDelete

Triggered when a file is deleted from an image field. Function Signature:
onFileDelete(params, urlObject) => Promise<void>

Parameters:
  • params (Object): Context information about the file deletion
  • urlObject (Object): Information about the file being deleted
Usage Example:
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 <JoyDoc doc={document} onFileDelete={handleFileDelete} />;
}

Advanced Change Processing

Filtering Changes by Type

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

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 (
    <div>
      <JoyDoc
        doc={document}
        features={{
          validateSchema: true,
        }}
        onChange={handleChange}
      />
      {Object.keys(errors).length > 0 && (
        <div className="errors">
          {Object.entries(errors).map(([fieldId, message]) => (
            <div key={fieldId} className="error">
              {message}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

Auto-Save

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 (
    <div>
      <JoyDoc doc={document} onChange={handleChange} />
      <div className="save-status">
        {isSaving && <span>Saving...</span>}
        {lastSaved && !isSaving && (
          <span>Last saved: {lastSaved.toLocaleTimeString()}</span>
        )}
      </div>
    </div>
  );
}

export default MyForm;

Analytics Tracking

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 (
    <JoyDoc
      doc={document}
      onChange={handleChange}
    />
  );
}

Undo/Redo Functionality

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 (
    <div>
      <div className="controls">
        <button onClick={undo} disabled={historyIndex <= 0}>
          Undo
        </button>
        <button onClick={redo} disabled={historyIndex >= history.length - 1}>
          Redo
        </button>
      </div>
      <JoyDoc
        doc={document}
        onChange={handleChange}
      />
    </div>
  );
}

Best Practices

1. Efficient Change Processing

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

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:
<JoyDoc
  doc={document}
  onChange={handleChange} // Make sure this is provided
/>

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:
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); },
	}

}

<JoyDoc
  doc={document}
  onFocus={handleFocus}
  onBlur={handleBlur}
  fieldSettings={fieldSettings}
/>

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.