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.
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
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: `
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
// 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();
# 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
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
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
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.
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
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/
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>
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
- ✓ Check browser dev tools for JavaScript errors
- ✓ Verify PDF container loads without errors
- ✓ Test adding a signature field
- ✓ Test exporting configuration
Troubleshooting Common Issues
❌ "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"""
<%= 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)
<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>