Framework Integrations

SignSure is designed to work seamlessly with all modern JavaScript frameworks. This guide provides detailed integration instructions and examples for the most popular frameworks and build tools.

Framework Agnostic: SignSure is built as a vanilla JavaScript library, making it compatible with any framework or no framework at all.

React

Perfect integration with React hooks and lifecycle methods. Includes TypeScript definitions and custom hooks.

Vue.js

Works with both Options API and Composition API. Supports Vue 2 and Vue 3 with reactive data binding.

Angular

Full Angular service integration with dependency injection and RxJS observables support.

Svelte

Lightweight integration with Svelte's reactive system and stores for state management.

Phoenix

Full integration with Phoenix LiveView and traditional controller/view patterns for Elixir applications.

React Integration

React Setup

Installation

npm install @signsure/core

Basic Component

import React, { useEffect, useRef, useState } from 'react';
import SignSure from '@signsure/core';

function PDFEditor({ pdfUrl, onFieldsChange }) {
    const containerRef = useRef(null);
    const signSureRef = useRef(null);
    const [isLoaded, setIsLoaded] = useState(false);
    const [fields, setFields] = useState([]);

    useEffect(() => {
        // Initialize SignSure
        signSureRef.current = new SignSure({
            container: containerRef.current,
            licenseKey: process.env.REACT_APP_SIGNSURE_LICENSE,
            theme: 'default',
            enableNavigation: true,
            enableZoom: true
        });

        // Set up event listeners
        const signSure = signSureRef.current;

        signSure.on('pdf:loaded', () => {
            setIsLoaded(true);
        });

        signSure.on('field:added', (field) => {
            setFields(prev => [...prev, field]);
            onFieldsChange?.(field);
        });

        signSure.on('field:removed', (fieldId) => {
            setFields(prev => prev.filter(f => f.id !== fieldId));
        });

        signSure.on('field:updated', (field) => {
            setFields(prev => prev.map(f => f.id === field.id ? field : f));
        });

        // Cleanup
        return () => {
            signSure.destroy();
        };
    }, [onFieldsChange]);

    useEffect(() => {
        if (pdfUrl && signSureRef.current) {
            signSureRef.current.loadPDF(pdfUrl)
                .catch(error => console.error('Failed to load PDF:', error));
        }
    }, [pdfUrl]);

    const addSignatureField = async () => {
        if (signSureRef.current && isLoaded) {
            try {
                await signSureRef.current.addField({
                    type: 'signature',
                    page: 1,
                    x: 100,
                    y: 200,
                    width: 200,
                    height: 80,
                    required: true,
                    label: 'Signature'
                });
            } catch (error) {
                console.error('Failed to add field:', error);
            }
        }
    };

    return (
        
Fields: {fields.length}
); } export default PDFEditor;

Custom Hook

useSignSure Hook
import { useEffect, useRef, useState, useCallback } from 'react';
import SignSure from '@signsure/core';

export function useSignSure(options = {}) {
    const signSureRef = useRef(null);
    const [isInitialized, setIsInitialized] = useState(false);
    const [isDocumentLoaded, setIsDocumentLoaded] = useState(false);
    const [fields, setFields] = useState([]);
    const [error, setError] = useState(null);

    const initialize = useCallback((container) => {
        if (signSureRef.current) return;

        try {
            signSureRef.current = new SignSure({
                container,
                licenseKey: process.env.REACT_APP_SIGNSURE_LICENSE,
                ...options
            });

            const signSure = signSureRef.current;

            signSure.on('pdf:loaded', () => setIsDocumentLoaded(true));
            signSure.on('pdf:error', (err) => setError(err));
            signSure.on('field:added', (field) => {
                setFields(prev => [...prev, field]);
            });
            signSure.on('field:removed', (fieldId) => {
                setFields(prev => prev.filter(f => f.id !== fieldId));
            });

            setIsInitialized(true);
        } catch (err) {
            setError(err);
        }
    }, [options]);

    const loadDocument = useCallback(async (source) => {
        if (!signSureRef.current) throw new Error('SignSure not initialized');

        try {
            setError(null);
            await signSureRef.current.loadPDF(source);
        } catch (err) {
            setError(err);
            throw err;
        }
    }, []);

    const addField = useCallback(async (fieldConfig) => {
        if (!signSureRef.current) throw new Error('SignSure not initialized');

        try {
            return await signSureRef.current.addField(fieldConfig);
        } catch (err) {
            setError(err);
            throw err;
        }
    }, []);

    const exportConfiguration = useCallback(() => {
        if (!signSureRef.current) return null;
        return signSureRef.current.exportConfiguration();
    }, []);

    useEffect(() => {
        return () => {
            if (signSureRef.current) {
                signSureRef.current.destroy();
                signSureRef.current = null;
            }
        };
    }, []);

    return {
        initialize,
        loadDocument,
        addField,
        exportConfiguration,
        isInitialized,
        isDocumentLoaded,
        fields,
        error,
        instance: signSureRef.current
    };
}

Angular Integration

Angular Setup

Installation

npm install @signsure/core

Service Setup

// signsure.service.ts
import { Injectable, NgZone } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import SignSure from '@signsure/core';

export interface SignSureField {
  id: string;
  type: string;
  page: number;
  x: number;
  y: number;
  width: number;
  height: number;
  required?: boolean;
  label?: string;
  value?: any;
}

@Injectable({
  providedIn: 'root'
})
export class SignSureService {
  private signSure: SignSure | null = null;
  private fieldsSubject = new BehaviorSubject([]);
  private loadedSubject = new BehaviorSubject(false);
  private errorSubject = new BehaviorSubject(null);

  public fields$ = this.fieldsSubject.asObservable();
  public loaded$ = this.loadedSubject.asObservable();
  public error$ = this.errorSubject.asObservable();

  constructor(private ngZone: NgZone) {}

  initialize(container: HTMLElement, options: any = {}): Promise {
    return new Promise((resolve, reject) => {
      try {
        this.signSure = new SignSure({
          container,
          licenseKey: options.licenseKey || 'your-license-key',
          theme: options.theme || 'default',
          ...options
        });

        this.signSure.on('pdf:loaded', () => {
          this.ngZone.run(() => {
            this.loadedSubject.next(true);
            resolve();
          });
        });

        this.signSure.on('pdf:error', (error: Error) => {
          this.ngZone.run(() => {
            this.errorSubject.next(error);
            reject(error);
          });
        });

        this.signSure.on('field:added', (field: SignSureField) => {
          this.ngZone.run(() => {
            const currentFields = this.fieldsSubject.value;
            this.fieldsSubject.next([...currentFields, field]);
          });
        });

        this.signSure.on('field:removed', (fieldId: string) => {
          this.ngZone.run(() => {
            const currentFields = this.fieldsSubject.value;
            this.fieldsSubject.next(currentFields.filter(f => f.id !== fieldId));
          });
        });

      } catch (error) {
        reject(error as Error);
      }
    });
  }

  async loadPDF(source: string | Uint8Array): Promise {
    if (!this.signSure) {
      throw new Error('SignSure not initialized');
    }

    try {
      await this.signSure.loadPDF(source);
    } catch (error) {
      this.errorSubject.next(error as Error);
      throw error;
    }
  }

  async addField(fieldConfig: Partial): Promise {
    if (!this.signSure) {
      throw new Error('SignSure not initialized');
    }

    return await this.signSure.addField(fieldConfig);
  }

  exportConfiguration(): any {
    if (!this.signSure) return null;
    return this.signSure.exportConfiguration();
  }

  destroy(): void {
    if (this.signSure) {
      this.signSure.destroy();
      this.signSure = null;
      this.fieldsSubject.next([]);
      this.loadedSubject.next(false);
      this.errorSubject.next(null);
    }
  }
}

Component Example

// pdf-editor.component.ts
import { Component, ElementRef, OnInit, OnDestroy, ViewChild, Input } from '@angular/core';
import { SignSureService, SignSureField } from './signsure.service';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-pdf-editor',
  template: `
    
Fields: {{ (fields$ | async)?.length || 0 }}
Error: {{ error.message }}
`, styles: [` .pdf-container { height: 600px; border: 1px solid #ccc; margin-top: 1rem; } .toolbar { display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem; } .error { color: red; margin-top: 1rem; } `] }) export class PdfEditorComponent implements OnInit, OnDestroy { @ViewChild('pdfContainer', { static: true }) pdfContainer!: ElementRef; @Input() pdfUrl?: string; fields$: Observable; loaded$: Observable; error$: Observable; constructor(private signSureService: SignSureService) { this.fields$ = this.signSureService.fields$; this.loaded$ = this.signSureService.loaded$; this.error$ = this.signSureService.error$; } async ngOnInit() { try { await this.signSureService.initialize( this.pdfContainer.nativeElement, { licenseKey: 'your-license-key', theme: 'default' } ); if (this.pdfUrl) { await this.signSureService.loadPDF(this.pdfUrl); } } catch (error) { console.error('Failed to initialize SignSure:', error); } } async addSignatureField() { try { await this.signSureService.addField({ type: 'signature', page: 1, x: 100, y: 200, width: 200, height: 80, required: true, label: 'Signature' }); } catch (error) { console.error('Failed to add field:', error); } } ngOnDestroy() { this.signSureService.destroy(); } }

Svelte Integration

Svelte Setup

Installation

npm install @signsure/core

Basic Component

<script>
  import { onMount, onDestroy } from 'svelte';
  import { writable } from 'svelte/store';
  import SignSure from '@signsure/core';

  export let pdfUrl = '';

  let container;
  let signSure = null;

  const fields = writable([]);
  const isLoaded = writable(false);
  const error = writable(null);

  onMount(async () => {
    try {
      signSure = new SignSure({
        container,
        licenseKey: 'your-license-key',
        theme: 'default',
        enableNavigation: true,
        enableZoom: true
      });

      signSure.on('pdf:loaded', () => {
        isLoaded.set(true);
      });

      signSure.on('pdf:error', (err) => {
        error.set(err);
      });

      signSure.on('field:added', (field) => {
        fields.update(f => [...f, field]);
      });

      signSure.on('field:removed', (fieldId) => {
        fields.update(f => f.filter(field => field.id !== fieldId));
      });

      signSure.on('field:updated', (field) => {
        fields.update(f => f.map(existing =>
          existing.id === field.id ? field : existing
        ));
      });

      if (pdfUrl) {
        await signSure.loadPDF(pdfUrl);
      }
    } catch (err) {
      error.set(err);
    }
  });

  onDestroy(() => {
    if (signSure) {
      signSure.destroy();
    }
  });

  async function addSignatureField() {
    if (!signSure || !$isLoaded) return;

    try {
      await signSure.addField({
        type: 'signature',
        page: 1,
        x: 100,
        y: 200,
        width: 200,
        height: 80,
        required: true,
        label: 'Signature'
      });
    } catch (err) {
      error.set(err);
    }
  }

  function exportConfiguration() {
    return signSure?.exportConfiguration();
  }

  // Reactive statement for URL changes
  $: if (signSure && pdfUrl) {
    signSure.loadPDF(pdfUrl).catch(err => error.set(err));
  }
</script>

<div class="pdf-editor">
  <div class="toolbar">
    <button
      on:click={addSignatureField}
      disabled={!$isLoaded}
      class="btn btn-primary"
    >
      Add Signature Field
    </button>
    <span class="field-count">
      Fields: {$fields.length}
    </span>
  </div>

  <div
    bind:this={container}
    class="pdf-container"
  ></div>

  {#if $error}
    <div class="error">
      Error: {$error.message}
    </div>
  {/if}
</div>

<style>
  .pdf-editor {
    width: 100%;
  }

  .toolbar {
    display: flex;
    align-items: center;
    gap: 1rem;
    margin-bottom: 1rem;
  }

  .pdf-container {
    height: 600px;
    border: 1px solid #ccc;
    border-radius: 4px;
  }

  .btn {
    padding: 0.5rem 1rem;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-weight: 500;
  }

  .btn-primary {
    background: #3b82f6;
    color: white;
  }

  .btn-primary:disabled {
    background: #9ca3af;
    cursor: not-allowed;
  }

  .field-count {
    color: #6b7280;
    font-size: 0.875rem;
  }

  .error {
    color: #ef4444;
    margin-top: 1rem;
    padding: 1rem;
    background: #fef2f2;
    border: 1px solid #fecaca;
    border-radius: 4px;
  }
</style>

Store Integration

Svelte Store for SignSure
// stores/signSure.js
import { writable, derived } from 'svelte/store';
import SignSure from '@signsure/core';

function createSignSureStore() {
  const { subscribe, set, update } = writable({
    instance: null,
    isInitialized: false,
    isLoaded: false,
    fields: [],
    error: null
  });

  return {
    subscribe,

    async initialize(container, options = {}) {
      try {
        const signSure = new SignSure({
          container,
          licenseKey: options.licenseKey || 'your-license-key',
          ...options
        });

        signSure.on('pdf:loaded', () => {
          update(state => ({ ...state, isLoaded: true }));
        });

        signSure.on('field:added', (field) => {
          update(state => ({
            ...state,
            fields: [...state.fields, field]
          }));
        });

        signSure.on('field:removed', (fieldId) => {
          update(state => ({
            ...state,
            fields: state.fields.filter(f => f.id !== fieldId)
          }));
        });

        set({
          instance: signSure,
          isInitialized: true,
          isLoaded: false,
          fields: [],
          error: null
        });

        return signSure;
      } catch (error) {
        update(state => ({ ...state, error }));
        throw error;
      }
    },

    async loadPDF(source) {
      update(state => {
        if (state.instance) {
          state.instance.loadPDF(source).catch(error => {
            update(s => ({ ...s, error }));
          });
        }
        return state;
      });
    },

    destroy() {
      update(state => {
        if (state.instance) {
          state.instance.destroy();
        }
        return {
          instance: null,
          isInitialized: false,
          isLoaded: false,
          fields: [],
          error: null
        };
      });
    }
  };
}

export const signSureStore = createSignSureStore();
Complete Phoenix Integration Example
# lib/my_app_web/live/pdf_editor_live.ex
defmodule MyAppWeb.PdfEditorLive do
  use MyAppWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    {:ok,
     socket
     |> assign(:pdf_url, "/assets/sample.pdf")
     |> assign(:fields, [])
     |> assign(:is_loaded, false)}
  end

  @impl true
  def handle_event("pdf_loaded", _params, socket) do
    {:noreply, assign(socket, :is_loaded, true)}
  end

  def handle_event("field_added", %{"field" => field}, socket) do
    fields = [field | socket.assigns.fields]
    {:noreply, assign(socket, :fields, fields)}
  end

  @impl true
  def render(assigns) do
    ~H"""
    <div class="container mx-auto p-4">
      <h1 class="text-2xl font-bold mb-4">PDF Editor</h1>

      <div class="pdf-editor" phx-hook="SignSureEditor" id="pdf-editor">
        <div class="toolbar mb-4">
          <button phx-click="add_signature_field"
                  disabled={!@is_loaded}
                  class="px-4 py-2 bg-blue-500 text-white rounded disabled:opacity-50">
            Add Signature Field
          </button>
          <span class="ml-4">Fields: <%= length(@fields) %></span>
        </div>

        <div id="pdf-container"
             class="border border-gray-300 h-96"
             data-pdf-url={@pdf_url}
             phx-update="ignore">
        </div>
      </div>
    </div>
    """
  end
end
Setup Complete! Your Phoenix application is now ready to use SignSure with direct file inclusion. The PDF worker will automatically handle PDF rendering and field operations.

Webpack Configuration

Webpack Setup

Basic Configuration

// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
    clean: true
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      },
      {
        test: /\.(png|jpg|jpeg|gif|svg)$/,
        type: 'asset/resource'
      },
      {
        test: /\.pdf$/,
        type: 'asset/resource'
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html'
    })
  ],
  resolve: {
    fallback: {
      "fs": false,
      "path": require.resolve("path-browserify"),
      "stream": require.resolve("stream-browserify"),
      "buffer": require.resolve("buffer")
    }
  },
  devServer: {
    static: {
      directory: path.join(__dirname, 'dist')
    },
    port: 3000,
    open: true,
    hot: true
  }
};

Entry Point Example

// src/index.js
import SignSure from '@signsure/core';
import './styles.css';

class PDFEditor {
  constructor(container) {
    this.container = container;
    this.signSure = null;
    this.init();
  }

  async init() {
    try {
      this.signSure = new SignSure({
        container: this.container,
        licenseKey: process.env.SIGNSURE_LICENSE_KEY,
        theme: 'default'
      });

      this.setupEventListeners();
      this.createToolbar();
    } catch (error) {
      console.error('Failed to initialize SignSure:', error);
    }
  }

  setupEventListeners() {
    this.signSure.on('pdf:loaded', () => {
      console.log('PDF loaded successfully');
    });

    this.signSure.on('field:added', (field) => {
      console.log('Field added:', field);
    });
  }

  createToolbar() {
    const toolbar = document.createElement('div');
    toolbar.className = 'toolbar';

    const addSignBtn = document.createElement('button');
    addSignBtn.textContent = 'Add Signature';
    addSignBtn.onclick = () => this.addSignatureField();

    toolbar.appendChild(addSignBtn);
    this.container.parentNode.insertBefore(toolbar, this.container);
  }

  async addSignatureField() {
    await this.signSure.addField({
      type: 'signature',
      page: 1,
      x: 100,
      y: 200,
      width: 200,
      height: 80
    });
  }

  async loadPDF(source) {
    await this.signSure.loadPDF(source);
  }
}

// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
  const container = document.getElementById('pdf-container');
  const editor = new PDFEditor(container);

  // Load a sample PDF
  editor.loadPDF('/sample.pdf');
});

Vite Configuration

Vite Setup

Configuration

// vite.config.js
import { defineConfig } from 'vite';
import { resolve } from 'path';

export default defineConfig({
  define: {
    global: 'globalThis'
  },
  optimizeDeps: {
    include: ['@signsure/core']
  },
  server: {
    port: 3000,
    open: true
  },
  build: {
    rollupOptions: {
      input: {
        main: resolve(__dirname, 'index.html')
      }
    }
  },
  assetsInclude: ['**/*.pdf']
});

TypeScript Support

// vite.config.ts (for TypeScript projects)
import { defineConfig } from 'vite';
import { resolve } from 'path';

export default defineConfig({
  define: {
    global: 'globalThis'
  },
  optimizeDeps: {
    include: ['@signsure/core']
  },
  server: {
    port: 3000,
    open: true
  },
  build: {
    rollupOptions: {
      input: {
        main: resolve(__dirname, 'index.html')
      }
    }
  },
  assetsInclude: ['**/*.pdf'],
  esbuild: {
    target: 'es2020'
  }
});

Usage Example

// src/main.ts
import SignSure from '@signsure/core';
import './style.css';

class VitePDFEditor {
  private signSure: SignSure | null = null;
  private container: HTMLElement;

  constructor(container: HTMLElement) {
    this.container = container;
    this.initialize();
  }

  private async initialize() {
    try {
      this.signSure = new SignSure({
        container: this.container,
        licenseKey: import.meta.env.VITE_SIGNSURE_LICENSE,
        theme: 'default'
      });

      this.signSure.on('pdf:loaded', () => {
        console.log('PDF loaded in Vite environment');
      });

    } catch (error) {
      console.error('SignSure initialization failed:', error);
    }
  }

  async loadPDF(source: string | Uint8Array) {
    if (!this.signSure) throw new Error('SignSure not initialized');
    await this.signSure.loadPDF(source);
  }

  async addField(config: any) {
    if (!this.signSure) throw new Error('SignSure not initialized');
    return await this.signSure.addField(config);
  }
}

// Initialize
const container = document.querySelector('#app')!;
const editor = new VitePDFEditor(container);

Create React App

Create React App Setup

Note: Create React App works out of the box with SignSure. No additional webpack configuration needed.

Installation

npx create-react-app my-signsure-app
cd my-signsure-app
npm install @signsure/core

Environment Variables

# .env
REACT_APP_SIGNSURE_LICENSE=your-license-key-here

App Component

// src/App.js
import React, { useState } from 'react';
import PDFEditor from './components/PDFEditor';
import './App.css';

function App() {
  const [pdfFile, setPdfFile] = useState(null);

  const handleFileUpload = (event) => {
    const file = event.target.files[0];
    if (file && file.type === 'application/pdf') {
      setPdfFile(URL.createObjectURL(file));
    }
  };

  return (
    

SignSure with Create React App

{pdfFile && (
)}
); } export default App;

PDF Editor Component

// src/components/PDFEditor.js
import React, { useEffect, useRef, useState } from 'react';
import SignSure from '@signsure/core';

const PDFEditor = ({ pdfUrl }) => {
  const containerRef = useRef(null);
  const signSureRef = useRef(null);
  const [isReady, setIsReady] = useState(false);
  const [fields, setFields] = useState([]);

  useEffect(() => {
    const initializeSignSure = async () => {
      try {
        signSureRef.current = new SignSure({
          container: containerRef.current,
          licenseKey: process.env.REACT_APP_SIGNSURE_LICENSE,
          theme: 'default'
        });

        signSureRef.current.on('pdf:loaded', () => {
          setIsReady(true);
        });

        signSureRef.current.on('field:added', (field) => {
          setFields(prev => [...prev, field]);
        });

        if (pdfUrl) {
          await signSureRef.current.loadPDF(pdfUrl);
        }
      } catch (error) {
        console.error('Failed to initialize SignSure:', error);
      }
    };

    initializeSignSure();

    return () => {
      if (signSureRef.current) {
        signSureRef.current.destroy();
      }
    };
  }, [pdfUrl]);

  const addSignature = async () => {
    if (signSureRef.current && isReady) {
      await signSureRef.current.addField({
        type: 'signature',
        page: 1,
        x: 100,
        y: 200,
        width: 200,
        height: 80
      });
    }
  };

  return (
    
Fields: {fields.length}
); }; export default PDFEditor;

Next.js Integration

Next.js Setup

Configuration

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    appDir: true,
  },
  webpack: (config, { isServer }) => {
    if (!isServer) {
      config.resolve.fallback = {
        ...config.resolve.fallback,
        fs: false,
        path: false,
        stream: false,
      };
    }
    return config;
  },
  transpilePackages: ['@signsure/core'],
};

module.exports = nextConfig;

Dynamic Import (Recommended)

// components/PDFEditor.jsx
'use client';

import { useEffect, useRef, useState } from 'react';
import dynamic from 'next/dynamic';

const PDFEditor = ({ pdfUrl }) => {
  const containerRef = useRef(null);
  const signSureRef = useRef(null);
  const [isReady, setIsReady] = useState(false);
  const [isClient, setIsClient] = useState(false);

  useEffect(() => {
    setIsClient(true);
  }, []);

  useEffect(() => {
    if (!isClient) return;

    const initializeSignSure = async () => {
      try {
        const SignSure = (await import('@signsure/core')).default;

        signSureRef.current = new SignSure({
          container: containerRef.current,
          licenseKey: process.env.NEXT_PUBLIC_SIGNSURE_LICENSE,
          theme: 'default'
        });

        signSureRef.current.on('pdf:loaded', () => {
          setIsReady(true);
        });

        if (pdfUrl) {
          await signSureRef.current.loadPDF(pdfUrl);
        }
      } catch (error) {
        console.error('Failed to initialize SignSure:', error);
      }
    };

    initializeSignSure();

    return () => {
      if (signSureRef.current) {
        signSureRef.current.destroy();
      }
    };
  }, [isClient, pdfUrl]);

  const addSignature = async () => {
    if (signSureRef.current && isReady) {
      await signSureRef.current.addField({
        type: 'signature',
        page: 1,
        x: 100,
        y: 200,
        width: 200,
        height: 80
      });
    }
  };

  if (!isClient) {
    return 
Loading...
; } return (
); }; export default PDFEditor;

Server-Side Rendering Support

// pages/pdf-editor.js
import dynamic from 'next/dynamic';

const PDFEditor = dynamic(() => import('../components/PDFEditor'), {
  ssr: false,
  loading: () => 

Loading PDF Editor...

}); export default function PDFEditorPage() { return (

PDF Editor

); }

TypeScript Integration

TypeScript Support

Type Definitions

// types/signsure.d.ts
export interface SignSureField {
  id: string;
  type: 'signature' | 'text' | 'date' | 'checkbox' | 'initial';
  page: number;
  x: number;
  y: number;
  width: number;
  height: number;
  required?: boolean;
  label?: string;
  value?: any;
  placeholder?: string;
  readonly?: boolean;
}

export interface SignSureConfig {
  container: HTMLElement;
  licenseKey: string;
  theme?: 'default' | 'dark' | 'minimal';
  enableNavigation?: boolean;
  enableZoom?: boolean;
  allowedFields?: string[];
  maxFileSize?: number;
  onFieldAdded?: (field: SignSureField) => void;
  onFieldRemoved?: (fieldId: string) => void;
  onFieldUpdated?: (field: SignSureField) => void;
  onDocumentLoaded?: () => void;
  onError?: (error: Error) => void;
}

export interface SignSureExportOptions {
  format?: 'json' | 'xml';
  includeData?: boolean;
  flattenFields?: boolean;
}

export default class SignSure {
  constructor(config: SignSureConfig);

  loadPDF(source: string | Uint8Array | File): Promise;
  addField(config: Partial): Promise;
  removeField(fieldId: string): Promise;
  updateField(fieldId: string, updates: Partial): Promise;
  getField(fieldId: string): SignSureField | null;
  getAllFields(): SignSureField[];
  exportConfiguration(options?: SignSureExportOptions): any;
  importConfiguration(config: any): Promise;
  generatePDF(): Promise;
  destroy(): void;

  on(event: string, callback: Function): void;
  off(event: string, callback: Function): void;
}

React Hook with TypeScript

// hooks/useSignSure.ts
import { useEffect, useRef, useState, useCallback } from 'react';
import SignSure, { SignSureField, SignSureConfig } from '@signsure/core';

interface UseSignSureOptions extends Omit {
  autoInitialize?: boolean;
}

interface UseSignSureReturn {
  initialize: (container: HTMLElement) => Promise;
  loadDocument: (source: string | Uint8Array | File) => Promise;
  addField: (config: Partial) => Promise;
  removeField: (fieldId: string) => Promise;
  updateField: (fieldId: string, updates: Partial) => Promise;
  exportConfiguration: () => any;
  generatePDF: () => Promise;
  isInitialized: boolean;
  isDocumentLoaded: boolean;
  fields: SignSureField[];
  error: Error | null;
  instance: SignSure | null;
}

export function useSignSure(options: UseSignSureOptions = {}): UseSignSureReturn {
  const signSureRef = useRef(null);
  const [isInitialized, setIsInitialized] = useState(false);
  const [isDocumentLoaded, setIsDocumentLoaded] = useState(false);
  const [fields, setFields] = useState([]);
  const [error, setError] = useState(null);

  const initialize = useCallback(async (container: HTMLElement) => {
    if (signSureRef.current) return;

    try {
      signSureRef.current = new SignSure({
        container,
        licenseKey: options.licenseKey || process.env.REACT_APP_SIGNSURE_LICENSE || '',
        ...options
      });

      const signSure = signSureRef.current;

      signSure.on('pdf:loaded', () => setIsDocumentLoaded(true));
      signSure.on('pdf:error', (err: Error) => setError(err));
      signSure.on('field:added', (field: SignSureField) => {
        setFields(prev => [...prev, field]);
      });
      signSure.on('field:removed', (fieldId: string) => {
        setFields(prev => prev.filter(f => f.id !== fieldId));
      });
      signSure.on('field:updated', (field: SignSureField) => {
        setFields(prev => prev.map(f => f.id === field.id ? field : f));
      });

      setIsInitialized(true);
    } catch (err) {
      setError(err as Error);
      throw err;
    }
  }, [options]);

  const loadDocument = useCallback(async (source: string | Uint8Array | File) => {
    if (!signSureRef.current) throw new Error('SignSure not initialized');

    try {
      setError(null);
      await signSureRef.current.loadPDF(source);
    } catch (err) {
      setError(err as Error);
      throw err;
    }
  }, []);

  const addField = useCallback(async (fieldConfig: Partial) => {
    if (!signSureRef.current) throw new Error('SignSure not initialized');

    try {
      return await signSureRef.current.addField(fieldConfig);
    } catch (err) {
      setError(err as Error);
      throw err;
    }
  }, []);

  const removeField = useCallback(async (fieldId: string) => {
    if (!signSureRef.current) throw new Error('SignSure not initialized');

    try {
      await signSureRef.current.removeField(fieldId);
    } catch (err) {
      setError(err as Error);
      throw err;
    }
  }, []);

  const updateField = useCallback(async (fieldId: string, updates: Partial) => {
    if (!signSureRef.current) throw new Error('SignSure not initialized');

    try {
      return await signSureRef.current.updateField(fieldId, updates);
    } catch (err) {
      setError(err as Error);
      throw err;
    }
  }, []);

  const exportConfiguration = useCallback(() => {
    if (!signSureRef.current) return null;
    return signSureRef.current.exportConfiguration();
  }, []);

  const generatePDF = useCallback(async () => {
    if (!signSureRef.current) throw new Error('SignSure not initialized');
    return await signSureRef.current.generatePDF();
  }, []);

  useEffect(() => {
    return () => {
      if (signSureRef.current) {
        signSureRef.current.destroy();
        signSureRef.current = null;
      }
    };
  }, []);

  return {
    initialize,
    loadDocument,
    addField,
    removeField,
    updateField,
    exportConfiguration,
    generatePDF,
    isInitialized,
    isDocumentLoaded,
    fields,
    error,
    instance: signSureRef.current
  };
}

Custom Hooks

Custom React Hooks

useSignSureFields Hook

import { useState, useCallback } from 'react';

export function useSignSureFields(signSureInstance) {
  const [fields, setFields] = useState([]);

  const addTextField = useCallback(async (position) => {
    if (!signSureInstance) return;

    const field = await signSureInstance.addField({
      type: 'text',
      page: position.page || 1,
      x: position.x || 100,
      y: position.y || 100,
      width: position.width || 200,
      height: position.height || 30,
      placeholder: 'Enter text here'
    });

    setFields(prev => [...prev, field]);
    return field;
  }, [signSureInstance]);

  const addSignatureField = useCallback(async (position) => {
    if (!signSureInstance) return;

    const field = await signSureInstance.addField({
      type: 'signature',
      page: position.page || 1,
      x: position.x || 100,
      y: position.y || 200,
      width: position.width || 200,
      height: position.height || 80,
      required: true
    });

    setFields(prev => [...prev, field]);
    return field;
  }, [signSureInstance]);

  const addDateField = useCallback(async (position) => {
    if (!signSureInstance) return;

    const field = await signSureInstance.addField({
      type: 'date',
      page: position.page || 1,
      x: position.x || 100,
      y: position.y || 300,
      width: position.width || 150,
      height: position.height || 30,
      value: new Date().toISOString().split('T')[0]
    });

    setFields(prev => [...prev, field]);
    return field;
  }, [signSureInstance]);

  const removeField = useCallback(async (fieldId) => {
    if (!signSureInstance) return;

    await signSureInstance.removeField(fieldId);
    setFields(prev => prev.filter(f => f.id !== fieldId));
  }, [signSureInstance]);

  const clearAllFields = useCallback(async () => {
    if (!signSureInstance) return;

    for (const field of fields) {
      await signSureInstance.removeField(field.id);
    }
    setFields([]);
  }, [signSureInstance, fields]);

  return {
    fields,
    addTextField,
    addSignatureField,
    addDateField,
    removeField,
    clearAllFields
  };
}

useSignSurePDF Hook

import { useState, useCallback } from 'react';

export function useSignSurePDF(signSureInstance) {
  const [isLoading, setIsLoading] = useState(false);
  const [currentPage, setCurrentPage] = useState(1);
  const [totalPages, setTotalPages] = useState(0);
  const [zoomLevel, setZoomLevel] = useState(1);

  const loadPDF = useCallback(async (source) => {
    if (!signSureInstance) return;

    setIsLoading(true);
    try {
      await signSureInstance.loadPDF(source);
      // Assume we can get page info from the instance
      const pageInfo = signSureInstance.getPageInfo?.() || { total: 1 };
      setTotalPages(pageInfo.total);
      setCurrentPage(1);
    } catch (error) {
      console.error('Failed to load PDF:', error);
      throw error;
    } finally {
      setIsLoading(false);
    }
  }, [signSureInstance]);

  const goToPage = useCallback(async (pageNumber) => {
    if (!signSureInstance || pageNumber < 1 || pageNumber > totalPages) return;

    try {
      await signSureInstance.goToPage?.(pageNumber);
      setCurrentPage(pageNumber);
    } catch (error) {
      console.error('Failed to navigate to page:', error);
    }
  }, [signSureInstance, totalPages]);

  const nextPage = useCallback(() => {
    if (currentPage < totalPages) {
      goToPage(currentPage + 1);
    }
  }, [currentPage, totalPages, goToPage]);

  const previousPage = useCallback(() => {
    if (currentPage > 1) {
      goToPage(currentPage - 1);
    }
  }, [currentPage, goToPage]);

  const zoomIn = useCallback(() => {
    const newZoom = Math.min(zoomLevel * 1.25, 3);
    setZoomLevel(newZoom);
    signSureInstance.setZoom?.(newZoom);
  }, [signSureInstance, zoomLevel]);

  const zoomOut = useCallback(() => {
    const newZoom = Math.max(zoomLevel * 0.8, 0.25);
    setZoomLevel(newZoom);
    signSureInstance.setZoom?.(newZoom);
  }, [signSureInstance, zoomLevel]);

  const resetZoom = useCallback(() => {
    setZoomLevel(1);
    signSureInstance.setZoom?.(1);
  }, [signSureInstance]);

  return {
    isLoading,
    currentPage,
    totalPages,
    zoomLevel,
    loadPDF,
    goToPage,
    nextPage,
    previousPage,
    zoomIn,
    zoomOut,
    resetZoom
  };
}

Composition API Examples

Vue 3 Composition API

Composable Function

// composables/useSignSure.js
import { ref, onMounted, onUnmounted } from 'vue';
import SignSure from '@signsure/core';

export function useSignSure(options = {}) {
  const signSure = ref(null);
  const isInitialized = ref(false);
  const isDocumentLoaded = ref(false);
  const fields = ref([]);
  const error = ref(null);

  const initialize = async (container) => {
    try {
      signSure.value = new SignSure({
        container,
        licenseKey: options.licenseKey || import.meta.env.VITE_SIGNSURE_LICENSE,
        theme: options.theme || 'default',
        ...options
      });

      signSure.value.on('pdf:loaded', () => {
        isDocumentLoaded.value = true;
      });

      signSure.value.on('field:added', (field) => {
        fields.value.push(field);
      });

      signSure.value.on('field:removed', (fieldId) => {
        fields.value = fields.value.filter(f => f.id !== fieldId);
      });

      signSure.value.on('pdf:error', (err) => {
        error.value = err;
      });

      isInitialized.value = true;
    } catch (err) {
      error.value = err;
      throw err;
    }
  };

  const loadDocument = async (source) => {
    if (!signSure.value) throw new Error('SignSure not initialized');

    try {
      error.value = null;
      await signSure.value.loadPDF(source);
    } catch (err) {
      error.value = err;
      throw err;
    }
  };

  const addField = async (config) => {
    if (!signSure.value) throw new Error('SignSure not initialized');

    try {
      return await signSure.value.addField(config);
    } catch (err) {
      error.value = err;
      throw err;
    }
  };

  const exportConfiguration = () => {
    if (!signSure.value) return null;
    return signSure.value.exportConfiguration();
  };

  onUnmounted(() => {
    if (signSure.value) {
      signSure.value.destroy();
    }
  });

  return {
    signSure: signSure.value,
    isInitialized,
    isDocumentLoaded,
    fields,
    error,
    initialize,
    loadDocument,
    addField,
    exportConfiguration
  };
}

Complete Component Example

<template>
  <div class="pdf-editor">
    <div class="toolbar">
      <div class="toolbar-section">
        <button
          @click="addSignatureField"
          :disabled="!isDocumentLoaded"
          class="btn btn-primary"
        >
          <i class="fas fa-signature"></i>
          Add Signature
        </button>
        <button
          @click="addTextField"
          :disabled="!isDocumentLoaded"
          class="btn btn-secondary"
        >
          <i class="fas fa-font"></i>
          Add Text
        </button>
        <button
          @click="addDateField"
          :disabled="!isDocumentLoaded"
          class="btn btn-secondary"
        >
          <i class="fas fa-calendar"></i>
          Add Date
        </button>
      </div>
      <div class="toolbar-section">
        <span class="field-count">Fields: {{ fields.length }}</span>
        <button
          @click="exportConfig"
          :disabled="fields.length === 0"
          class="btn btn-success"
        >
          <i class="fas fa-download"></i>
          Export
        </button>
      </div>
    </div>

    <div ref="containerRef" class="pdf-container" />

    <div v-if="error" class="error">
      <i class="fas fa-exclamation-triangle"></i>
      {{ error.message }}
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, watch } from 'vue';
import { useSignSure } from '@/composables/useSignSure';

const props = defineProps({
  pdfUrl: String,
  licenseKey: String
});

const emit = defineEmits(['field-added', 'config-exported', 'error']);

const containerRef = ref(null);

const {
  isInitialized,
  isDocumentLoaded,
  fields,
  error,
  initialize,
  loadDocument,
  addField,
  exportConfiguration
} = useSignSure({
  licenseKey: props.licenseKey
});

onMounted(async () => {
  if (containerRef.value) {
    try {
      await initialize(containerRef.value);
      if (props.pdfUrl) {
        await loadDocument(props.pdfUrl);
      }
    } catch (err) {
      emit('error', err);
    }
  }
});

watch(() => props.pdfUrl, async (newUrl) => {
  if (newUrl && isInitialized.value) {
    try {
      await loadDocument(newUrl);
    } catch (err) {
      emit('error', err);
    }
  }
});

const addSignatureField = async () => {
  try {
    const field = await addField({
      type: 'signature',
      page: 1,
      x: 100,
      y: 200,
      width: 200,
      height: 80,
      required: true,
      label: 'Signature'
    });
    emit('field-added', field);
  } catch (err) {
    emit('error', err);
  }
};

const addTextField = async () => {
  try {
    const field = await addField({
      type: 'text',
      page: 1,
      x: 100,
      y: 300,
      width: 200,
      height: 30,
      placeholder: 'Enter text here'
    });
    emit('field-added', field);
  } catch (err) {
    emit('error', err);
  }
};

const addDateField = async () => {
  try {
    const field = await addField({
      type: 'date',
      page: 1,
      x: 100,
      y: 400,
      width: 150,
      height: 30,
      value: new Date().toISOString().split('T')[0]
    });
    emit('field-added', field);
  } catch (err) {
    emit('error', err);
  }
};

const exportConfig = () => {
  const config = exportConfiguration();
  emit('config-exported', config);

  // Download as JSON
  const blob = new Blob([JSON.stringify(config, null, 2)], {
    type: 'application/json'
  });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = 'signsure-config.json';
  a.click();
  URL.revokeObjectURL(url);
};
</script>

<style scoped>
.pdf-editor {
  display: flex;
  flex-direction: column;
  height: 100%;
}

.toolbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem;
  background: #f8f9fa;
  border-bottom: 1px solid #dee2e6;
}

.toolbar-section {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 1rem;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-weight: 500;
  transition: all 0.2s ease;
}

.btn:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.btn-primary {
  background: #007bff;
  color: white;
}

.btn-primary:hover:not(:disabled) {
  background: #0056b3;
}

.btn-secondary {
  background: #6c757d;
  color: white;
}

.btn-secondary:hover:not(:disabled) {
  background: #545b62;
}

.btn-success {
  background: #28a745;
  color: white;
}

.btn-success:hover:not(:disabled) {
  background: #1e7e34;
}

.field-count {
  color: #6c757d;
  font-size: 0.875rem;
  font-weight: 500;
}

.pdf-container {
  flex: 1;
  border: 1px solid #dee2e6;
  background: white;
}

.error {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 1rem;
  background: #f8d7da;
  color: #721c24;
  border: 1px solid #f5c6cb;
  border-radius: 4px;
  margin: 1rem;
}
</style>

Phoenix Integration

Phoenix LiveView Integration

LiveView Integration: Phoenix integrates with SignSure using JavaScript hooks and LiveView for real-time server communication. This guide shows the complete working implementation.

Complete File Setup

Place SignSure files directly in your Phoenix assets directory for streamlined integration:

# Your Phoenix project structure:
assets/
├── css/
│   ├── app.css                # ← Import SignSure CSS here
│   └── signsure.css           # ← SignSure base styles
├── js/
│   ├── app.js                 # ← Configure hooks here
│   ├── hooks/
│   │   └── signsure_hook.js   # ← SignSure LiveView hook
│   ├── signsure.min.js        # ← Main SignSure library
│   └── pdf.min.js             # ← PDF.js library
└── vendor/
    └── topbar.js

Step 1: Download and Extract SignSure Files

Extract these files from the SignSure package to your Phoenix project:

# Download the complete package
wget https://signsure.xyz/downloads/signsure-complete.zip
unzip signsure-complete.zip

# Copy files to Phoenix assets directory
cp dist/lib/signsure.min.js assets/js/
cp dist/lib/pdf.min.js assets/js/
cp dist/lib/signsure.css assets/css/

Step 2: Import SignSure CSS

Add SignSure styles to your main CSS file:

/* assets/css/app.css */
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
@import "./signsure.css";

/* Your application CSS */

Step 3: Update Layout Template

Include SignSure JavaScript in your root layout:

<!-- lib/my_app_web/components/layouts/root.html.heex -->
<!DOCTYPE html>
<html lang="en" class="[scrollbar-gutter:stable]">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="csrf-token" content={get_csrf_token()} />
    <.live_title default="My App" suffix=" · Phoenix Framework">
      {assigns[:page_title]}
    </.live_title>

    <!-- CSS bundle includes SignSure styles -->
    <link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />

    <!-- JavaScript files - order matters! -->
    <script defer phx-track-static type="text/javascript" src={~p"/assets/pdf.min.js"}></script>
    <script defer phx-track-static type="text/javascript" src={~p"/assets/signsure.min.js"}></script>
    <script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}></script>
  </head>
  <body class="bg-white">
    {@inner_content}
  </body>
</html>

Step 4: Configure ESBuild

Update your Phoenix config to build the SignSure files:

# config/config.exs
# Configure esbuild (the version is required)
config :esbuild,
  version: "0.17.11",
  pdfx: [
    args: ~w(
      js/app.js
      js/pdf.min.js
      js/signsure.min.js
      --bundle
      --target=es2017
      --outdir=../priv/static/assets
      --external:/fonts/*
      --external:/images/*
      ),
    cd: Path.expand("../assets", __DIR__),
    env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
  ]

Step 5: Create SignSure LiveView Hook

Create a simplified hook based on the working pdfx implementation:

// assets/js/hooks/signsure_hook.js
export const SignSureEditor = {
  mounted() {
    this.initializeSignSure();
    this.setupEventListeners();
  },

  updated() {
    const pdfUrl = this.el.querySelector("#pdf-container").dataset.pdfUrl;
    if (pdfUrl && pdfUrl !== this.currentPdfUrl) {
      this.loadPDF(pdfUrl);
      this.currentPdfUrl = pdfUrl;
    }
  },

  destroyed() {
    this.signSure?.destroy();
  },

  initializeSignSure() {
    const container = this.el.querySelector("#pdf-container");

    this.signSure = new window.SignSure.SignSure({
      container: container,
      licenseKey: "YOUR-LICENSE-KEY-HERE",
      theme: "default",
      enableNavigation: true,
      enableZoom: true,
      debug: false,
    });

    this.signSure.on("pdf:loaded", () => {
      this.pushEvent("pdf_loaded", {});
    });

    this.signSure.on("field:created", (field) => {
      this.pushEvent("field_added", { field: field });
    });

    this.signSure.on("field:removed", (fieldId) => {
      this.pushEvent("field_removed", { field_id: fieldId });
    });

    this.signSure.on("error", (error) => {
      this.pushEvent("pdf_error", { error: error.message });
    });

    this.loadInitialPDF();
  },

  setupEventListeners() {
    this.handleEvent("add_signature_field", () => {
      this.addSignatureField();
    });

    this.handleEvent("export_configuration", () => {
      this.exportConfiguration();
    });
  },

  loadInitialPDF() {
    const pdfUrl = this.el.querySelector("#pdf-container").dataset.pdfUrl;
    if (pdfUrl) {
      this.loadPDF(pdfUrl);
      this.currentPdfUrl = pdfUrl;
    }
  },

  loadPDF(pdfUrl) {
    this.signSure.loadPDF(pdfUrl);
  },

  addSignatureField() {
    this.signSure.addField({
      type: "signature",
      page: 1,
      x: 100,
      y: 200,
      width: 200,
      height: 80,
      required: true,
      label: "Signature",
    });
  },

  exportConfiguration() {
    const config = this.signSure.exportConfiguration();

    const blob = new Blob([JSON.stringify(config, null, 2)], {
      type: "application/json",
    });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = "signsure-config.json";
    a.click();
    URL.revokeObjectURL(url);
  },
};

Step 6: Configure App.js with Hooks

Register the SignSure hook in your main app.js file:

// assets/js/app.js
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html"
// Establish Phoenix Socket and LiveView configuration.
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import topbar from "../vendor/topbar"

import { SignSureEditor } from "./hooks/signsure_hook"

let Hooks = {
  SignSureEditor: SignSureEditor
}

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {
  longPollFallbackMs: 2500,
  params: {_csrf_token: csrfToken},
  hooks: Hooks
})

// Show progress bar on live navigation and form submits
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
window.addEventListener("phx:page-loading-start", _info => topbar.show(300)) window.addEventListener("phx:page-loading-stop", _info => topbar.hide()) // connect if there are any LiveViews on the page liveSocket.connect() // expose liveSocket on window for web console debug logs and latency simulation: window.liveSocket = liveSocket

Step 7: Create LiveView Module

Create a LiveView that handles SignSure events and state management:

# lib/my_app_web/live/pdf_editor_live.ex
defmodule MyAppWeb.PdfEditorLive do
  use MyAppWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    {:ok,
     socket
     |> assign(:pdf_url, "https://ontheline.trincoll.edu/images/bookdown/sample-local-pdf.pdf")
     |> assign(:fields, [])
     |> assign(:is_loaded, false)}
  end

  @impl true
  def handle_event("pdf_loaded", _params, socket) do
    {:noreply, assign(socket, :is_loaded, true)}
  end

  def handle_event("field_added", %{"field" => field}, socket) do
    fields = [field | socket.assigns.fields]
    {:noreply, assign(socket, :fields, fields)}
  end

  def handle_event("field_removed", %{"field_id" => field_id}, socket) do
    fields = Enum.reject(socket.assigns.fields, &(&1["id"] == field_id))
    {:noreply, assign(socket, :fields, fields)}
  end

  def handle_event("add_signature_field", _params, socket) do
    if socket.assigns.is_loaded do
      {:noreply, push_event(socket, "add_signature_field", %{})}
    else
      {:noreply, put_flash(socket, :error, "Please load a PDF first")}
    end
  end

  def handle_event("export_configuration", _params, socket) do
    {:noreply, push_event(socket, "export_configuration", %{})}
  end

  def handle_event("pdf_error", %{"error" => error}, socket) do
    {:noreply, put_flash(socket, :error, "PDF Error: #{error}")}
  end
end

Step 7: Create LiveView Template

Create a template that uses the SignSure hook:

<!-- lib/my_app_web/live/pdf_editor_live.html.heex -->
<div class="container mx-auto p-4">
  <h1 class="text-3xl font-bold mb-6 text-gray-900">PDF Editor</h1>

  <div class="pdf-editor" phx-hook="SignSureEditor" id="pdf-editor">
    <div class="toolbar mb-6 flex flex-wrap gap-4 items-center">
      <button
        phx-click="add_signature_field"
        disabled={!@is_loaded}
        class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
      >
        <i class="fas fa-signature mr-2"></i>
        Add Signature Field
      </button>

      <button
        phx-click="export_configuration"
        disabled={length(@fields) == 0}
        class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50"
      >
        <i class="fas fa-download mr-2"></i>
        Export Configuration
      </button>

      <div class="flex items-center text-gray-600">
        <i class="fas fa-layer-group mr-2"></i>
        Fields: <span class="font-semibold ml-1"><%= length(@fields) %></span>
      </div>

      <div class="flex items-center text-gray-600">
        <i class="fas fa-circle mr-2 text-green-500" :if={@is_loaded}></i>
        <i class="fas fa-circle mr-2 text-yellow-500" :if={!@is_loaded}></i>
        Status: <span class="font-semibold ml-1"><%= if @is_loaded, do: "Ready", else: "Loading..." %></span>
      </div>
    </div>

    <div
      id="pdf-container"
      class="border-2 border-gray-300 rounded-lg"
      style="height: 600px;"
      data-pdf-url={@pdf_url}
      phx-update="ignore"
    >
      <!-- SignSure will render the PDF here -->
    </div>
  </div>
</div>

Step 8: Add Router Configuration

Add a route to access your PDF editor:

# lib/my_app_web/router.ex
defmodule MyAppWeb.Router do
  use MyAppWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_live_flash
    plug :put_root_layout, html: {MyAppWeb.Layouts, :root}
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  scope "/", MyAppWeb do
    pipe_through :browser

    get "/", PageController, :home
    live "/pdf-editor", PdfEditorLive, :index
  end
end
Integration Summary: This integration places SignSure files directly in Phoenix assets, uses ESBuild to bundle them, and provides a clean LiveView hook for real-time PDF editing.
Production Notes: Replace the demo license key with your production license. Consider implementing proper PDF upload/storage and validating PDF URLs server-side.

Test Your Setup

Start your Phoenix server and test the integration:

# Start your Phoenix server
mix phx.server

# Navigate to: http://localhost:4000/pdf-editor

# You should see:
# ✅ PDF loads automatically
# ✅ Add Signature Field button works
# ✅ Export Configuration downloads JSON
# ✅ Real-time field count updates
# ✅ No console errors

Common Troubleshooting

If PDF doesn't load:
# Check browser console for errors
# Check browser console for errors
# Verify PDF URL is accessible
# Ensure both signsure.min.js and pdf.min.js are loaded

# Check that SignSure is available globally:
console.log(window.SignSure)
# Should output: SignSure library object
If fields don't add:
# Check LiveView hook registration in app.js
# Verify phx-hook="SignSureEditor" attribute
# Check that SignSure instance is initialized
# Look for JavaScript errors in browser console
Testing Checklist:
  • ✓ Check browser dev tools for JavaScript errors
  • ✓ Verify PDF container loads without errors
  • ✓ Test adding a signature field
  • ✓ Test exporting configuration

Troubleshooting Common Issues

Common Problems & Solutions

❌ "SignSure is not defined"

  • Check that signsure.min.js loads before app.js in root.html.heex
  • Verify script tag path: /assets/signsure.min.js
  • Check that ESBuild is properly configured in config.exs

❌ "PDF library not found"

  • Ensure pdf.min.js is in assets/js/ directory
  • Check that pdf.min.js is included in ESBuild args
  • Verify the script tag is in root.html.heex

❌ "Hook not working"

  • Verify hook registration in app.js: SignSureEditor: SignSureEditor
  • Check phx-hook attribute matches exactly: SignSureEditor
  • Ensure LiveView is properly mounted and container exists

❌ "CSS not loading"

  • Check CSS import in assets/css/app.css: @import "./signsure.css";
  • Verify file exists in assets/css/signsure.css
  • Run mix assets.deploy to rebuild bundled assets
  • Check that Tailwind watcher is running in development
Setup Complete! Your Phoenix application now has SignSure integrated with:
  • ✓ Direct file placement in assets directory
  • ✓ ESBuild bundling for optimized delivery
  • ✓ LiveView hooks for real-time interaction
  • ✓ Simplified hook implementation
  • ✓ Proper CSS integration via app.css import

Phoenix LiveView Integration

# lib/my_app_web/live/pdf_editor_live.ex
defmodule MyAppWeb.PdfEditorLive do
  use MyAppWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    {:ok,
     socket
     |> assign(:pdf_url, nil)
     |> assign(:fields, [])
     |> assign(:is_loaded, false)}
  end

  @impl true
  def handle_params(%{"pdf_url" => pdf_url}, _url, socket) do
    {:noreply, assign(socket, :pdf_url, pdf_url)}
  end

  def handle_params(_params, _url, socket) do
    {:noreply, socket}
  end

  @impl true
  def handle_event("pdf_loaded", _params, socket) do
    {:noreply, assign(socket, :is_loaded, true)}
  end

  def handle_event("field_added", %{"field" => field}, socket) do
    fields = [field | socket.assigns.fields]
    {:noreply, assign(socket, :fields, fields)}
  end

  def handle_event("field_removed", %{"field_id" => field_id}, socket) do
    fields = Enum.reject(socket.assigns.fields, &(&1["id"] == field_id))
    {:noreply, assign(socket, :fields, fields)}
  end

  def handle_event("add_signature_field", _params, socket) do
    if socket.assigns.is_loaded do
      # Signal to the client to add a signature field
      {:noreply, push_event(socket, "add_signature_field", %{})}
    else
      {:noreply, put_flash(socket, :error, "Please load a PDF first")}
    end
  end

  def handle_event("export_configuration", _params, socket) do
    # Signal to the client to export configuration
    {:noreply, push_event(socket, "export_configuration", %{})}
  end

  @impl true
  def render(assigns) do
    ~H"""
    
Fields: <%= length(@fields) %>
<%= if @pdf_url do %>

PDF URL: <%= @pdf_url %>

Status: <%= if @is_loaded, do: "Loaded", else: "Loading..." %>

<% end %>
""" end end

LiveView JavaScript Hook

// assets/js/hooks/signsure_hook.js
// assets/js/hooks/signsure_hook.js
// SignSure is loaded globally from the script tag in the layout
// No import needed - it's available as window.SignSure

export const SignSureEditor = {
  mounted() {
    this.initializeSignSure()
    this.setupEventListeners()
  },

  updated() {
    const pdfUrl = this.el.querySelector('#pdf-container').dataset.pdfUrl
    if (pdfUrl && this.signSure && pdfUrl !== this.currentPdfUrl) {
      this.loadPDF(pdfUrl)
      this.currentPdfUrl = pdfUrl
    }
  },

  destroyed() {
    if (this.signSure) {
      this.signSure.destroy()
    }
  },

  initializeSignSure() {
    const container = this.el.querySelector('#pdf-container')

    // Use globally available SignSure from window object
    this.signSure = new window.SignSure({
      container: container,
      licenseKey: window.signSureLicense || 'your-license-key',
      theme: 'default',
      enableNavigation: true,
      enableZoom: true
    })

    // SignSure event listeners
    this.signSure.on('pdf:loaded', () => {
      this.pushEvent('pdf_loaded', {})
    })

    this.signSure.on('field:added', (field) => {
      this.pushEvent('field_added', { field: field })
    })

    this.signSure.on('field:removed', (fieldId) => {
      this.pushEvent('field_removed', { field_id: fieldId })
    })

    this.signSure.on('pdf:error', (error) => {
      console.error('SignSure error:', error)
      this.pushEvent('pdf_error', { error: error.message })
    })
  },

  setupEventListeners() {
    // Listen for Phoenix events
    this.handleEvent('add_signature_field', () => {
      this.addSignatureField()
    })

    this.handleEvent('export_configuration', () => {
      this.exportConfiguration()
    })
  },

  async loadPDF(pdfUrl) {
    if (!this.signSure) return

    try {
      await this.signSure.loadPDF(pdfUrl)
    } catch (error) {
      console.error('Failed to load PDF:', error)
    }
  },

  async addSignatureField() {
    if (!this.signSure) return

    try {
      await this.signSure.addField({
        type: 'signature',
        page: 1,
        x: 100,
        y: 200,
        width: 200,
        height: 80,
        required: true,
        label: 'Signature'
      })
    } catch (error) {
      console.error('Failed to add signature field:', error)
    }
  },

  exportConfiguration() {
    if (!this.signSure) return

    const config = this.signSure.exportConfiguration()

    // Create download
    const blob = new Blob([JSON.stringify(config, null, 2)], {
      type: 'application/json'
    })
    const url = URL.createObjectURL(blob)
    const a = document.createElement('a')
    a.href = url
    a.download = 'signsure-config.json'
    a.click()
    URL.revokeObjectURL(url)
  }
}

Hook Registration

// assets/js/app.js
import { LiveSocket } from "phoenix_live_view"
import { SignSureEditor } from "./hooks/signsure_hook"

// Configure LiveView socket with hooks
let Hooks = {
  SignSureEditor: SignSureEditor
}

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {
  params: { _csrf_token: csrfToken },
  hooks: Hooks
})

// Connect the socket
liveSocket.connect()

Router Configuration

# lib/my_app_web/router.ex
defmodule MyAppWeb.Router do
  use MyAppWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_live_flash
    plug :put_root_layout, {MyAppWeb.LayoutView, :root}
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  scope "/", MyAppWeb do
    pipe_through :browser

    get "/", PageController, :index
    live "/pdf-editor", PdfEditorLive
    live "/pdf-editor/:pdf_url", PdfEditorLive
  end
end

Legacy Controller/View Integration

# lib/my_app_web/controllers/pdf_controller.ex
defmodule MyAppWeb.PdfController do
  use MyAppWeb, :controller

  def index(conn, _params) do
    render(conn, "index.html", %{
      page_title: "PDF Editor",
      pdf_url: nil
    })
  end

  def show(conn, %{"id" => pdf_id}) do
    # Fetch PDF URL from your storage/database
    pdf_url = get_pdf_url(pdf_id)

    render(conn, "show.html", %{
      page_title: "Edit PDF",
      pdf_url: pdf_url,
      pdf_id: pdf_id
    })
  end

  def create_field(conn, %{"pdf_id" => pdf_id, "field" => field_params}) do
    # Save field configuration to database
    case save_field_configuration(pdf_id, field_params) do
      {:ok, field} ->
        conn
        |> put_flash(:info, "Field added successfully")
        |> json(%{status: "success", field: field})

      {:error, changeset} ->
        conn
        |> put_status(:unprocessable_entity)
        |> json(%{status: "error", errors: changeset.errors})
    end
  end

  def export_config(conn, %{"pdf_id" => pdf_id}) do
    config = get_pdf_configuration(pdf_id)

    conn
    |> put_resp_content_type("application/json")
    |> put_resp_header("content-disposition", "attachment; filename=\"config-#{pdf_id}.json\"")
    |> send_resp(200, Jason.encode!(config))
  end

  # Private functions
  defp get_pdf_url(pdf_id) do
    # Implementation depends on your storage solution
    "/uploads/pdfs/#{pdf_id}.pdf"
  end

  defp save_field_configuration(pdf_id, field_params) do
    # Save to database - implementation depends on your schema
    {:ok, %{id: UUID.uuid4(), pdf_id: pdf_id, config: field_params}}
  end

  defp get_pdf_configuration(pdf_id) do
    # Fetch from database - implementation depends on your schema
    %{pdf_id: pdf_id, fields: []}
  end
end

Traditional Template

<!-- lib/my_app_web/templates/pdf/show.html.heex -->
<div class="pdf-editor-page">
  <h1>PDF Editor</h1>

  <div class="toolbar">
    <button id="add-signature-btn" class="btn btn-primary">
      Add Signature Field
    </button>
    <button id="add-text-btn" class="btn btn-secondary">
      Add Text Field
    </button>
    <button id="export-btn" class="btn btn-success">
      Export Configuration
    </button>
    <span id="field-count" class="field-count">Fields: 0</span>
  </div>

  <div id="pdf-container"
       class="pdf-container"
       data-pdf-url="<%= @pdf_url %>"
       data-pdf-id="<%= @pdf_id %>">
  </div>
</div>

<script>
  // Pass Rails CSRF token to JavaScript
  window.csrfToken = '<%= Plug.CSRFProtection.get_csrf_token() %>';
  window.pdfId = '<%= @pdf_id %>';
</script>

Traditional JavaScript Integration

// assets/js/pdf_editor.js
import SignSure from '@signsure/core'

class PhoenixPdfEditor {
  constructor() {
    this.signSure = null
    this.fields = []
    this.pdfId = window.pdfId
    this.init()
  }

  async init() {
    const container = document.getElementById('pdf-container')
    const pdfUrl = container.dataset.pdfUrl

    if (!container) return

    try {
      this.signSure = new window.SignSure({
        container: container,
        licenseKey: window.signSureLicense || 'your-license-key',
        theme: 'default'
      })

      this.setupEventListeners()
      this.setupSignSureEvents()

      if (pdfUrl) {
        await this.signSure.loadPDF(pdfUrl)
      }
    } catch (error) {
      console.error('Failed to initialize SignSure:', error)
    }
  }

  setupEventListeners() {
    document.getElementById('add-signature-btn')?.addEventListener('click', () => {
      this.addSignatureField()
    })

    document.getElementById('add-text-btn')?.addEventListener('click', () => {
      this.addTextField()
    })

    document.getElementById('export-btn')?.addEventListener('click', () => {
      this.exportConfiguration()
    })
  }

  setupSignSureEvents() {
    this.signSure.on('field:added', (field) => {
      this.fields.push(field)
      this.updateFieldCount()
      this.saveFieldToServer(field)
    })

    this.signSure.on('field:removed', (fieldId) => {
      this.fields = this.fields.filter(f => f.id !== fieldId)
      this.updateFieldCount()
      this.removeFieldFromServer(fieldId)
    })
  }

  async addSignatureField() {
    if (!this.signSure) return

    try {
      await this.signSure.addField({
        type: 'signature',
        page: 1,
        x: 100,
        y: 200,
        width: 200,
        height: 80,
        required: true,
        label: 'Signature'
      })
    } catch (error) {
      console.error('Failed to add signature field:', error)
    }
  }

  async addTextField() {
    if (!this.signSure) return

    try {
      await this.signSure.addField({
        type: 'text',
        page: 1,
        x: 100,
        y: 300,
        width: 200,
        height: 30,
        placeholder: 'Enter text here'
      })
    } catch (error) {
      console.error('Failed to add text field:', error)
    }
  }

  async saveFieldToServer(field) {
    try {
      const response = await fetch(`/api/pdfs/${this.pdfId}/fields`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-CSRF-Token': window.csrfToken
        },
        body: JSON.stringify({ field: field })
      })

      if (!response.ok) {
        throw new Error('Failed to save field')
      }
    } catch (error) {
      console.error('Failed to save field to server:', error)
    }
  }

  async removeFieldFromServer(fieldId) {
    try {
      const response = await fetch(`/api/pdfs/${this.pdfId}/fields/${fieldId}`, {
        method: 'DELETE',
        headers: {
          'X-CSRF-Token': window.csrfToken
        }
      })

      if (!response.ok) {
        throw new Error('Failed to remove field')
      }
    } catch (error) {
      console.error('Failed to remove field from server:', error)
    }
  }

  exportConfiguration() {
    if (!this.signSure) return

    const config = this.signSure.exportConfiguration()

    // Download the configuration
    const blob = new Blob([JSON.stringify(config, null, 2)], {
      type: 'application/json'
    })
    const url = URL.createObjectURL(blob)
    const a = document.createElement('a')
    a.href = url
    a.download = `pdf-config-${this.pdfId}.json`
    a.click()
    URL.revokeObjectURL(url)
  }

  updateFieldCount() {
    const countElement = document.getElementById('field-count')
    if (countElement) {
      countElement.textContent = `Fields: ${this.fields.length}`
    }
  }
}

// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
  if (document.getElementById('pdf-container')) {
    new PhoenixPdfEditor()
  }
})

API Routes for Legacy Integration

# lib/my_app_web/router.ex - Add API routes
scope "/api", MyAppWeb.Api, as: :api do
  pipe_through :api

  resources "/pdfs", PdfController, only: [:show] do
    resources "/fields", FieldController, only: [:create, :delete]
    get "/export", PdfController, :export_config
  end
end

Vue.js Integration

Vue.js Setup

Installation

npm install @signsure/core

Options API (Vue 2 & 3)

<template>
  <div class="pdf-editor">
    <div class="toolbar">
      <button
        @click="addSignatureField"
        :disabled="!isDocumentLoaded"
        class="btn btn-primary"
      >
        Add Signature Field
      </button>
      <span class="field-count">
        Fields: {{ fields.length }}
      </span>
    </div>
    <div
      ref="pdfContainer"
      class="pdf-container"
      style="height: 600px; border: 1px solid #ccc;"
    ></div>
  </div>
</template>

<script>
import SignSure from '@signsure/core';

export default {
  name: 'PDFEditor',
  props: {
    pdfUrl: {
      type: String,
      required: true
    }
  },
  data() {
    return {
      signSure: null,
      isDocumentLoaded: false,
      fields: []
    };
  },
  async mounted() {
    try {
      this.signSure = new SignSure({
        container: this.$refs.pdfContainer,
        licenseKey: process.env.VUE_APP_SIGNSURE_LICENSE,
        theme: 'default'
      });

      // Event listeners
      this.signSure.on('pdf:loaded', () => {
        this.isDocumentLoaded = true;
      });

      this.signSure.on('field:added', (field) => {
        this.fields.push(field);
        this.$emit('field-added', field);
      });

      this.signSure.on('field:removed', (fieldId) => {
        this.fields = this.fields.filter(f => f.id !== fieldId);
        this.$emit('field-removed', fieldId);
      });

      if (this.pdfUrl) {
        await this.signSure.loadPDF(this.pdfUrl);
      }
    } catch (error) {
      console.error('Failed to initialize SignSure:', error);
      this.$emit('error', error);
    }
  },
  methods: {
    async addSignatureField() {
      if (!this.signSure || !this.isDocumentLoaded) return;

      try {
        await this.signSure.addField({
          type: 'signature',
          page: 1,
          x: 100,
          y: 200,
          width: 200,
          height: 80,
          required: true,
          label: 'Signature'
        });
      } catch (error) {
        console.error('Failed to add field:', error);
        this.$emit('error', error);
      }
    },
    exportConfiguration() {
      if (!this.signSure) return null;
      return this.signSure.exportConfiguration();
    }
  },
  watch: {
    async pdfUrl(newUrl) {
      if (this.signSure && newUrl) {
        try {
          await this.signSure.loadPDF(newUrl);
        } catch (error) {
          console.error('Failed to load PDF:', error);
          this.$emit('error', error);
        }
      }
    }
  },
  beforeUnmount() {
    if (this.signSure) {
      this.signSure.destroy();
    }
  }
};
</script>

Composition API (Vue 3)

Composition API Example
<template>
  <div class="pdf-editor">
    <div class="toolbar">
      <button
        @click="addSignatureField"
        :disabled="!isDocumentLoaded"
        class="btn btn-primary"
      >
        Add Signature Field
      </button>
      <span>Fields: {{ fields.length }}</span>
    </div>
    <div ref="containerRef" class="pdf-container" />
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue';
import SignSure from '@signsure/core';

const props = defineProps({
  pdfUrl: String
});

const emit = defineEmits(['field-added', 'field-removed', 'error']);

const containerRef = ref(null);
const signSure = ref(null);
const isDocumentLoaded = ref(false);
const fields = ref([]);

onMounted(async () => {
  try {
    signSure.value = new SignSure({
      container: containerRef.value,
      licenseKey: import.meta.env.VITE_SIGNSURE_LICENSE,
      theme: 'default'
    });

    signSure.value.on('pdf:loaded', () => {
      isDocumentLoaded.value = true;
    });

    signSure.value.on('field:added', (field) => {
      fields.value.push(field);
      emit('field-added', field);
    });

    if (props.pdfUrl) {
      await signSure.value.loadPDF(props.pdfUrl);
    }
  } catch (error) {
    console.error('Failed to initialize SignSure:', error);
    emit('error', error);
  }
});

const addSignatureField = async () => {
  if (!signSure.value || !isDocumentLoaded.value) return;

  try {
    await signSure.value.addField({
      type: 'signature',
      page: 1,
      x: 100,
      y: 200,
      width: 200,
      height: 80,
      required: true,
      label: 'Signature'
    });
  } catch (error) {
    console.error('Failed to add field:', error);
    emit('error', error);
  }
};

watch(() => props.pdfUrl, async (newUrl) => {
  if (signSure.value && newUrl) {
    try {
      await signSure.value.loadPDF(newUrl);
    } catch (error) {
      console.error('Failed to load PDF:', error);
      emit('error', error);
    }
  }
});

onUnmounted(() => {
  if (signSure.value) {
    signSure.value.destroy();
  }
});
</script>