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 Setup

Server-Side Integration: Phoenix integrates with SignSure on the client-side using JavaScript hooks and traditional templates.
PDF Worker Required: SignSure requires both signsure.min.js AND pdf.worker.min.js files to function. The PDF worker handles PDF parsing and rendering operations and must be placed in the same directory as the main library file.
Complete Step-by-Step Guide: This section provides comprehensive instructions for integrating SignSure into Phoenix applications using direct file inclusion.

Quick File Overview

For SignSure to work correctly, you need these files in your Phoenix project:

# All files go in assets/vendor/signsure/ directory:
signsure.min.js       # Main SignSure library (265 KB)
pdf.worker.min.js     # PDF.js worker - REQUIRED for PDF operations (1.8 MB)
signsure.css          # Base styles (35 KB)

# Optional theme files:
themes/dark.css       # Dark theme
themes/minimal.css    # Minimal theme

File Setup

No NPM Package: SignSure is currently distributed as standalone files only. Download the complete package and include files directly in your Phoenix project using the steps below.

Step 1: Download SignSure Package

Download the complete SignSure package from the downloads page:

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

Step 2: Extract Required Files

From the extracted package, you need these files from the dist/lib/ folder:

# Required files:
dist/lib/signsure.min.js       # Main library (265 KB)
dist/lib/pdf.worker.min.js     # PDF worker - REQUIRED (1.8 MB)
dist/lib/signsure.css          # Base styles (35 KB)

# Optional theme files:
dist/lib/themes/dark.css       # Dark theme (24 KB)
dist/lib/themes/minimal.css    # Minimal theme (25 KB)

Step 3: Place Files in Phoenix Project

Extract the following files from the SignSure download to your Phoenix project:

# Create SignSure directory and copy all files:
mkdir -p assets/vendor/signsure/themes/
cp dist/lib/signsure.min.js assets/vendor/signsure/
cp dist/lib/pdf.worker.min.js assets/vendor/signsure/
cp dist/lib/signsure.css assets/vendor/signsure/

# Optional: Copy theme files
cp dist/lib/themes/dark.css assets/vendor/signsure/themes/
cp dist/lib/themes/minimal.css assets/vendor/signsure/themes/
Your Phoenix project structure:
assets/
├── css/
│   └── app.css                # ← Your app styles
├── js/
│   ├── app.js
│   └── hooks/
│       └── signsure_hook.js   # ← We'll create this
└── vendor/
    └── signsure/              # ← All SignSure files together
        ├── signsure.min.js    # ← Main SignSure library
        ├── pdf.worker.min.js  # ← Required PDF worker
        ├── signsure.css       # ← SignSure base styles
        └── themes/
            ├── dark.css       # ← Optional: Dark theme
            └── minimal.css    # ← Optional: Minimal theme

Step 4: Include Files in Layout Template

Add the CSS and JavaScript files to your Phoenix layout:

<!-- In lib/my_app_web/templates/layout/root.html.heex -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <meta name="csrf-token" content={get_csrf_token()}/>

    <!-- Your existing CSS -->
    <link phx-track-static rel="stylesheet" href={~p"/assets/app.css"}/>

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

    <!-- Optional: Theme CSS (choose one) -->
    <link phx-track-static rel="stylesheet" href={~p"/assets/signsure/themes/dark.css"}/>

    <title>My App</title>
  </head>
  <body>
    <%= @inner_content %>

    <!-- SignSure JavaScript - MUST load before app.js -->
    <script defer phx-track-static src={~p"/assets/signsure/signsure.min.js"}></script>

    <!-- Your app JavaScript -->
    <script defer phx-track-static src={~p"/assets/app.js"}></script>
  </body>
</html>
Auto-Detection: SignSure automatically detects the PDF worker when both files are in the same directory. No additional configuration needed!

Step 5: Create JavaScript Hook

Create a LiveView hook to handle SignSure integration:

// assets/js/hooks/signsure_hook.js
// SignSure is loaded globally from the script tag in 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: 'DEMO-1234-5678-9ABC-DEF123456789', // Replace with your license
      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)
  }
}

Step 6: Register Hook in App.js

Register your SignSure hook with Phoenix LiveView:

// assets/js/app.js
// Phoenix LiveView setup
import "phoenix_html"
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import topbar from "../vendor/topbar"

// Import your SignSure hook
import { SignSureEditor } from "./hooks/signsure_hook"

// Configure LiveView 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
})

// 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 PDF Editor LiveView

Create a LiveView component that uses your SignSure hook:

# 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") # Replace with your 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

  @impl true
  def render(assigns) do
    ~H"""
    <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>
    """
  end
end

Step 8: Add Route

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, {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
  end
end

Step 9: 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
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
  • Verify script tag in layout template
  • Check file path: /assets/signsure/signsure.min.js

❌ "PDF Worker not found"

  • Ensure pdf.worker.min.js is in same directory as signsure.min.js
  • Both should be in assets/vendor/signsure/
  • Check file permissions

❌ "Hook not working"

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

❌ "CSS not loading"

  • Check signsure.css path in layout template
  • Verify file exists in assets/vendor/signsure/signsure.css
  • Run mix assets.deploy to rebuild assets

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>