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 LiveView Integration
Complete File Setup
Place SignSure files directly in your Phoenix assets directory for streamlined integration:
# Your Phoenix project structure:
assets/
├── css/
│ ├── app.css # ← Import SignSure CSS here
│ └── signsure.css # ← SignSure base styles
├── js/
│ ├── app.js # ← Configure hooks here
│ ├── hooks/
│ │ └── signsure_hook.js # ← SignSure LiveView hook
│ ├── signsure.min.js # ← Main SignSure library
│ └── pdf.min.js # ← PDF.js library
└── vendor/
└── topbar.js
Step 1: Download and Extract SignSure Files
Extract these files from the SignSure package to your Phoenix project:
# Download the complete package
wget https://signsure.xyz/downloads/signsure-complete.zip
unzip signsure-complete.zip
# Copy files to Phoenix assets directory
cp dist/lib/signsure.min.js assets/js/
cp dist/lib/pdf.min.js assets/js/
cp dist/lib/signsure.css assets/css/
Step 2: Import SignSure CSS
Add SignSure styles to your main CSS file:
/* assets/css/app.css */
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
@import "./signsure.css";
/* Your application CSS */
Step 3: Update Layout Template
Include SignSure JavaScript in your root layout:
<!-- lib/my_app_web/components/layouts/root.html.heex -->
<!DOCTYPE html>
<html lang="en" class="[scrollbar-gutter:stable]">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="csrf-token" content={get_csrf_token()} />
<.live_title default="My App" suffix=" · Phoenix Framework">
{assigns[:page_title]}
</.live_title>
<!-- CSS bundle includes SignSure styles -->
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
<!-- JavaScript files - order matters! -->
<script defer phx-track-static type="text/javascript" src={~p"/assets/pdf.min.js"}></script>
<script defer phx-track-static type="text/javascript" src={~p"/assets/signsure.min.js"}></script>
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}></script>
</head>
<body class="bg-white">
{@inner_content}
</body>
</html>
Step 4: Configure ESBuild
Update your Phoenix config to build the SignSure files:
# config/config.exs
# Configure esbuild (the version is required)
config :esbuild,
version: "0.17.11",
pdfx: [
args: ~w(
js/app.js
js/pdf.min.js
js/signsure.min.js
--bundle
--target=es2017
--outdir=../priv/static/assets
--external:/fonts/*
--external:/images/*
),
cd: Path.expand("../assets", __DIR__),
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
]
Step 5: Create SignSure LiveView Hook
Create a simplified hook based on the working pdfx implementation:
// assets/js/hooks/signsure_hook.js
export const SignSureEditor = {
mounted() {
this.initializeSignSure();
this.setupEventListeners();
},
updated() {
const pdfUrl = this.el.querySelector("#pdf-container").dataset.pdfUrl;
if (pdfUrl && pdfUrl !== this.currentPdfUrl) {
this.loadPDF(pdfUrl);
this.currentPdfUrl = pdfUrl;
}
},
destroyed() {
this.signSure?.destroy();
},
initializeSignSure() {
const container = this.el.querySelector("#pdf-container");
this.signSure = new window.SignSure.SignSure({
container: container,
licenseKey: "YOUR-LICENSE-KEY-HERE",
theme: "default",
enableNavigation: true,
enableZoom: true,
debug: false,
});
this.signSure.on("pdf:loaded", () => {
this.pushEvent("pdf_loaded", {});
});
this.signSure.on("field:created", (field) => {
this.pushEvent("field_added", { field: field });
});
this.signSure.on("field:removed", (fieldId) => {
this.pushEvent("field_removed", { field_id: fieldId });
});
this.signSure.on("error", (error) => {
this.pushEvent("pdf_error", { error: error.message });
});
this.loadInitialPDF();
},
setupEventListeners() {
this.handleEvent("add_signature_field", () => {
this.addSignatureField();
});
this.handleEvent("export_configuration", () => {
this.exportConfiguration();
});
},
loadInitialPDF() {
const pdfUrl = this.el.querySelector("#pdf-container").dataset.pdfUrl;
if (pdfUrl) {
this.loadPDF(pdfUrl);
this.currentPdfUrl = pdfUrl;
}
},
loadPDF(pdfUrl) {
this.signSure.loadPDF(pdfUrl);
},
addSignatureField() {
this.signSure.addField({
type: "signature",
page: 1,
x: 100,
y: 200,
width: 200,
height: 80,
required: true,
label: "Signature",
});
},
exportConfiguration() {
const config = this.signSure.exportConfiguration();
const blob = new Blob([JSON.stringify(config, null, 2)], {
type: "application/json",
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "signsure-config.json";
a.click();
URL.revokeObjectURL(url);
},
};
Step 6: Configure App.js with Hooks
Register the SignSure hook in your main app.js file:
// assets/js/app.js
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html"
// Establish Phoenix Socket and LiveView configuration.
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import topbar from "../vendor/topbar"
import { SignSureEditor } from "./hooks/signsure_hook"
let Hooks = {
SignSureEditor: SignSureEditor
}
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500,
params: {_csrf_token: csrfToken},
hooks: Hooks
})
// Show progress bar on live navigation and form submits
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
Step 7: Create LiveView Module
Create a LiveView that handles SignSure events and state management:
# lib/my_app_web/live/pdf_editor_live.ex
defmodule MyAppWeb.PdfEditorLive do
use MyAppWeb, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:pdf_url, "https://ontheline.trincoll.edu/images/bookdown/sample-local-pdf.pdf")
|> assign(:fields, [])
|> assign(:is_loaded, false)}
end
@impl true
def handle_event("pdf_loaded", _params, socket) do
{:noreply, assign(socket, :is_loaded, true)}
end
def handle_event("field_added", %{"field" => field}, socket) do
fields = [field | socket.assigns.fields]
{:noreply, assign(socket, :fields, fields)}
end
def handle_event("field_removed", %{"field_id" => field_id}, socket) do
fields = Enum.reject(socket.assigns.fields, &(&1["id"] == field_id))
{:noreply, assign(socket, :fields, fields)}
end
def handle_event("add_signature_field", _params, socket) do
if socket.assigns.is_loaded do
{:noreply, push_event(socket, "add_signature_field", %{})}
else
{:noreply, put_flash(socket, :error, "Please load a PDF first")}
end
end
def handle_event("export_configuration", _params, socket) do
{:noreply, push_event(socket, "export_configuration", %{})}
end
def handle_event("pdf_error", %{"error" => error}, socket) do
{:noreply, put_flash(socket, :error, "PDF Error: #{error}")}
end
end
Step 7: Create LiveView Template
Create a template that uses the SignSure hook:
<!-- lib/my_app_web/live/pdf_editor_live.html.heex -->
<div class="container mx-auto p-4">
<h1 class="text-3xl font-bold mb-6 text-gray-900">PDF Editor</h1>
<div class="pdf-editor" phx-hook="SignSureEditor" id="pdf-editor">
<div class="toolbar mb-6 flex flex-wrap gap-4 items-center">
<button
phx-click="add_signature_field"
disabled={!@is_loaded}
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
>
<i class="fas fa-signature mr-2"></i>
Add Signature Field
</button>
<button
phx-click="export_configuration"
disabled={length(@fields) == 0}
class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50"
>
<i class="fas fa-download mr-2"></i>
Export Configuration
</button>
<div class="flex items-center text-gray-600">
<i class="fas fa-layer-group mr-2"></i>
Fields: <span class="font-semibold ml-1"><%= length(@fields) %></span>
</div>
<div class="flex items-center text-gray-600">
<i class="fas fa-circle mr-2 text-green-500" :if={@is_loaded}></i>
<i class="fas fa-circle mr-2 text-yellow-500" :if={!@is_loaded}></i>
Status: <span class="font-semibold ml-1"><%= if @is_loaded, do: "Ready", else: "Loading..." %></span>
</div>
</div>
<div
id="pdf-container"
class="border-2 border-gray-300 rounded-lg"
style="height: 600px;"
data-pdf-url={@pdf_url}
phx-update="ignore"
>
<!-- SignSure will render the PDF here -->
</div>
</div>
</div>
Step 8: Add Router Configuration
Add a route to access your PDF editor:
# lib/my_app_web/router.ex
defmodule MyAppWeb.Router do
use MyAppWeb, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, html: {MyAppWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
scope "/", MyAppWeb do
pipe_through :browser
get "/", PageController, :home
live "/pdf-editor", PdfEditorLive, :index
end
end
Test Your Setup
Start your Phoenix server and test the integration:
# Start your Phoenix server
mix phx.server
# Navigate to: http://localhost:4000/pdf-editor
# You should see:
# ✅ PDF loads automatically
# ✅ Add Signature Field button works
# ✅ Export Configuration downloads JSON
# ✅ Real-time field count updates
# ✅ No console errors
Common Troubleshooting
# Check browser console for errors
# Check browser console for errors
# Verify PDF URL is accessible
# Ensure both signsure.min.js and pdf.min.js are loaded
# Check that SignSure is available globally:
console.log(window.SignSure)
# Should output: SignSure library object
# Check LiveView hook registration in app.js
# Verify phx-hook="SignSureEditor" attribute
# Check that SignSure instance is initialized
# Look for JavaScript errors in browser console
- ✓ 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 in root.html.heex
- Verify script tag path:
/assets/signsure.min.js - Check that ESBuild is properly configured in config.exs
❌ "PDF library not found"
- Ensure pdf.min.js is in
assets/js/directory - Check that pdf.min.js is included in ESBuild args
- Verify the script tag is in root.html.heex
❌ "Hook not working"
- Verify hook registration in app.js:
SignSureEditor: SignSureEditor - Check phx-hook attribute matches exactly:
SignSureEditor - Ensure LiveView is properly mounted and container exists
❌ "CSS not loading"
- Check CSS import in
assets/css/app.css:@import "./signsure.css"; - Verify file exists in
assets/css/signsure.css - Run
mix assets.deployto rebuild bundled assets - Check that Tailwind watcher is running in development
- ✓ Direct file placement in assets directory
- ✓ ESBuild bundling for optimized delivery
- ✓ LiveView hooks for real-time interaction
- ✓ Simplified hook implementation
- ✓ Proper CSS integration via app.css import
Phoenix LiveView Integration
# lib/my_app_web/live/pdf_editor_live.ex
defmodule MyAppWeb.PdfEditorLive do
use MyAppWeb, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:pdf_url, nil)
|> assign(:fields, [])
|> assign(:is_loaded, false)}
end
@impl true
def handle_params(%{"pdf_url" => pdf_url}, _url, socket) do
{:noreply, assign(socket, :pdf_url, pdf_url)}
end
def handle_params(_params, _url, socket) do
{:noreply, socket}
end
@impl true
def handle_event("pdf_loaded", _params, socket) do
{:noreply, assign(socket, :is_loaded, true)}
end
def handle_event("field_added", %{"field" => field}, socket) do
fields = [field | socket.assigns.fields]
{:noreply, assign(socket, :fields, fields)}
end
def handle_event("field_removed", %{"field_id" => field_id}, socket) do
fields = Enum.reject(socket.assigns.fields, &(&1["id"] == field_id))
{:noreply, assign(socket, :fields, fields)}
end
def handle_event("add_signature_field", _params, socket) do
if socket.assigns.is_loaded do
# Signal to the client to add a signature field
{:noreply, push_event(socket, "add_signature_field", %{})}
else
{:noreply, put_flash(socket, :error, "Please load a PDF first")}
end
end
def handle_event("export_configuration", _params, socket) do
# Signal to the client to export configuration
{:noreply, push_event(socket, "export_configuration", %{})}
end
@impl true
def render(assigns) do
~H"""
<%= 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