Installation
No additional dependencies are required beyond Angular. Ensure you have Angular CLI installed:npm install -g @angular/cli
Documenso Embed Component
Component TypeScript
// documenso-embed.component.ts
import {
Component,
Input,
Output,
EventEmitter,
OnInit,
OnDestroy,
ChangeDetectionStrategy
} from '@angular/core';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
export interface DocumentCompletedData {
token: string;
documentId: number;
envelopeId: string;
recipientId: number;
}
export interface DocumentRejectedData extends DocumentCompletedData {
reason?: string;
}
export interface FieldSignedData {
fieldId?: number;
value?: string;
isBase64?: boolean;
}
@Component({
selector: 'app-documenso-embed',
templateUrl: './documenso-embed.component.html',
styleUrls: ['./documenso-embed.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DocumensoEmbedComponent implements OnInit, OnDestroy {
@Input() token!: string;
@Input() name?: string;
@Input() lockName = false;
@Input() email?: string;
@Input() lockEmail = false;
@Input() allowDocumentRejection = false;
@Input() showOtherRecipientsCompletedFields = false;
@Input() darkModeDisabled = false;
@Input() css?: string;
@Input() cssVars?: Record<string, string>;
@Input() baseUrl = 'https://app.documenso.com';
@Output() ready = new EventEmitter<void>();
@Output() complete = new EventEmitter<DocumentCompletedData>();
@Output() reject = new EventEmitter<DocumentRejectedData>();
@Output() error = new EventEmitter<void>();
@Output() fieldSigned = new EventEmitter<FieldSignedData>();
@Output() fieldUnsigned = new EventEmitter<{ fieldId?: number }>();
embedUrl!: SafeResourceUrl;
private messageListener?: (event: MessageEvent) => void;
constructor(private sanitizer: DomSanitizer) {}
ngOnInit(): void {
this.buildEmbedUrl();
this.setupMessageListener();
}
ngOnDestroy(): void {
if (this.messageListener) {
window.removeEventListener('message', this.messageListener);
}
}
private buildEmbedUrl(): void {
const embedData: Record<string, any> = {
darkModeDisabled: this.darkModeDisabled
};
if (this.name) embedData.name = this.name;
if (this.lockName) embedData.lockName = this.lockName;
if (this.email) embedData.email = this.email;
if (this.lockEmail) embedData.lockEmail = this.lockEmail;
if (this.allowDocumentRejection) {
embedData.allowDocumentRejection = this.allowDocumentRejection;
}
if (this.showOtherRecipientsCompletedFields) {
embedData.showOtherRecipientsCompletedFields = this.showOtherRecipientsCompletedFields;
}
if (this.css) embedData.css = this.css;
if (this.cssVars) embedData.cssVars = this.cssVars;
const hash = btoa(encodeURIComponent(JSON.stringify(embedData)));
const url = `${this.baseUrl}/embed/sign/${this.token}#${hash}`;
this.embedUrl = this.sanitizer.bypassSecurityTrustResourceUrl(url);
}
private setupMessageListener(): void {
this.messageListener = (event: MessageEvent) => {
const allowedOrigin = new URL(this.baseUrl).origin;
if (event.origin !== allowedOrigin) {
return;
}
switch (event.data.action) {
case 'document-ready':
this.ready.emit();
break;
case 'document-completed':
this.complete.emit(event.data.data);
break;
case 'document-rejected':
this.reject.emit(event.data.data);
break;
case 'document-error':
this.error.emit();
break;
case 'field-signed':
this.fieldSigned.emit(event.data.data);
break;
case 'field-unsigned':
this.fieldUnsigned.emit(event.data.data);
break;
}
};
window.addEventListener('message', this.messageListener);
}
}
Component Template
<!-- documenso-embed.component.html -->
<iframe
[src]="embedUrl"
class="documenso-embed-iframe"
allow="clipboard-write"
title="Documenso Document Signing"
></iframe>
Component Styles
/* documenso-embed.component.css */
:host {
display: block;
width: 100%;
height: 100%;
}
.documenso-embed-iframe {
width: 100%;
height: 100%;
border: none;
}
Module Setup
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { DocumensoEmbedComponent } from './components/documenso-embed/documenso-embed.component';
@NgModule({
declarations: [
DocumensoEmbedComponent
],
imports: [
BrowserModule
],
exports: [
DocumensoEmbedComponent
]
})
export class DocumensoModule { }
Usage Examples
Basic Usage
// signing-page.component.ts
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { DocumentCompletedData } from './documenso-embed/documenso-embed.component';
@Component({
selector: 'app-signing-page',
template: `
<div class="container">
<h1>Sign Your Contract</h1>
<app-documenso-embed
[token]="recipientToken"
(complete)="onComplete($event)"
(error)="onError()"
></app-documenso-embed>
</div>
`,
styles: [`
.container {
height: 100vh;
display: flex;
flex-direction: column;
}
app-documenso-embed {
flex: 1;
}
`]
})
export class SigningPageComponent {
recipientToken = 'token-xyz-123';
constructor(private router: Router) {}
onComplete(data: DocumentCompletedData): void {
console.log('Document signed!', data);
this.router.navigate(['/success']);
}
onError(): void {
console.error('Error signing document');
this.router.navigate(['/error']);
}
}
With Loading State
// signing-page-with-loader.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-signing-page-with-loader',
template: `
<div class="container">
<div *ngIf="isLoading" class="loader-overlay">
<div class="spinner"></div>
<p>Loading document...</p>
</div>
<div *ngIf="errorMessage" class="error-overlay">
<h2>Error</h2>
<p>{{ errorMessage }}</p>
<button (click)="retry()">Retry</button>
</div>
<app-documenso-embed
[token]="token"
(ready)="onReady()"
(complete)="onComplete($event)"
(error)="onError()"
></app-documenso-embed>
</div>
`,
styles: [`
.container {
position: relative;
height: 100vh;
}
.loader-overlay,
.error-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.95);
z-index: 10;
}
.spinner {
width: 48px;
height: 48px;
border: 3px solid #f3f3f3;
border-top: 3px solid #3b82f6;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`]
})
export class SigningPageWithLoaderComponent {
token = 'token-xyz-123';
isLoading = true;
errorMessage: string | null = null;
onReady(): void {
this.isLoading = false;
}
onComplete(data: any): void {
console.log('Completed:', data);
}
onError(): void {
this.isLoading = false;
this.errorMessage = 'Failed to load document. Please try again.';
}
retry(): void {
window.location.reload();
}
}
Pre-filled Form
// customer-signing.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
@Component({
selector: 'app-customer-signing',
template: `
<app-documenso-embed
[token]="token"
[email]="customerEmail"
[lockEmail]="true"
[name]="customerName"
[lockName]="false"
[allowDocumentRejection]="true"
(complete)="onComplete($event)"
(reject)="onReject($event)"
></app-documenso-embed>
`
})
export class CustomerSigningComponent implements OnInit {
token!: string;
customerEmail = 'customer@example.com';
customerName = 'John Doe';
constructor(
private route: ActivatedRoute,
private router: Router
) {}
ngOnInit(): void {
this.token = this.route.snapshot.params['token'];
}
onComplete(data: any): void {
// Save to backend
console.log('Document completed:', data);
this.router.navigate(['/success']);
}
onReject(data: any): void {
console.log('Document rejected:', data);
this.router.navigate(['/rejected']);
}
}
Direct Template Component
// direct-template-embed.component.ts
import {
Component,
Input,
Output,
EventEmitter,
OnInit,
OnDestroy
} from '@angular/core';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
@Component({
selector: 'app-direct-template-embed',
template: `
<iframe
[src]="embedUrl"
class="direct-template-iframe"
allow="clipboard-write"
title="Sign Document"
></iframe>
`,
styles: [`
:host {
display: block;
width: 100%;
height: 100%;
}
.direct-template-iframe {
width: 100%;
height: 100%;
border: none;
}
`]
})
export class DirectTemplateEmbedComponent implements OnInit, OnDestroy {
@Input() directToken!: string;
@Input() email?: string;
@Input() lockEmail = false;
@Input() name?: string;
@Input() lockName = false;
@Input() externalId?: string;
@Input() cssVars?: Record<string, string>;
@Input() css?: string;
@Input() baseUrl = 'https://app.documenso.com';
@Output() ready = new EventEmitter<void>();
@Output() complete = new EventEmitter<any>();
@Output() error = new EventEmitter<string>();
embedUrl!: SafeResourceUrl;
private messageListener?: (event: MessageEvent) => void;
constructor(private sanitizer: DomSanitizer) {}
ngOnInit(): void {
this.buildEmbedUrl();
this.setupMessageListener();
}
ngOnDestroy(): void {
if (this.messageListener) {
window.removeEventListener('message', this.messageListener);
}
}
private buildEmbedUrl(): void {
const embedData: Record<string, any> = {};
if (this.email) embedData.email = this.email;
if (this.lockEmail) embedData.lockEmail = this.lockEmail;
if (this.name) embedData.name = this.name;
if (this.lockName) embedData.lockName = this.lockName;
if (this.css) embedData.css = this.css;
if (this.cssVars) embedData.cssVars = this.cssVars;
const hash = btoa(encodeURIComponent(JSON.stringify(embedData)));
const queryParams = this.externalId
? `?externalId=${encodeURIComponent(this.externalId)}`
: '';
const url = `${this.baseUrl}/d/${this.directToken}${queryParams}#${hash}`;
this.embedUrl = this.sanitizer.bypassSecurityTrustResourceUrl(url);
}
private setupMessageListener(): void {
this.messageListener = (event: MessageEvent) => {
const allowedOrigin = new URL(this.baseUrl).origin;
if (event.origin !== allowedOrigin) return;
switch (event.data.action) {
case 'document-ready':
this.ready.emit();
break;
case 'document-completed':
this.complete.emit(event.data.data);
break;
case 'document-error':
this.error.emit(event.data.data);
break;
}
};
window.addEventListener('message', this.messageListener);
}
}
Service for Embed Management
// documenso-embed.service.ts
import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';
export interface EmbedEvent {
action: string;
data: any;
}
@Injectable({
providedIn: 'root'
})
export class DocumensoEmbedService {
private eventSubject = new Subject<EmbedEvent>();
public events$: Observable<EmbedEvent> = this.eventSubject.asObservable();
constructor() {
this.setupGlobalListener();
}
private setupGlobalListener(): void {
window.addEventListener('message', (event: MessageEvent) => {
// Validate origin (add your allowed origins)
const allowedOrigins = ['https://app.documenso.com'];
if (!allowedOrigins.includes(event.origin)) {
return;
}
this.eventSubject.next({
action: event.data.action,
data: event.data.data
});
});
}
createEmbedUrl(
token: string,
options: {
name?: string;
email?: string;
lockName?: boolean;
lockEmail?: boolean;
cssVars?: Record<string, string>;
baseUrl?: string;
} = {}
): string {
const baseUrl = options.baseUrl || 'https://app.documenso.com';
const embedData: Record<string, any> = {};
if (options.name) embedData.name = options.name;
if (options.lockName) embedData.lockName = options.lockName;
if (options.email) embedData.email = options.email;
if (options.lockEmail) embedData.lockEmail = options.lockEmail;
if (options.cssVars) embedData.cssVars = options.cssVars;
const hash = btoa(encodeURIComponent(JSON.stringify(embedData)));
return `${baseUrl}/embed/sign/${token}#${hash}`;
}
}
Using the Service
import { Component, OnInit } from '@angular/core';
import { DocumensoEmbedService } from './services/documenso-embed.service';
import { filter } from 'rxjs/operators';
@Component({
selector: 'app-signing-with-service',
template: `
<app-documenso-embed [token]="token"></app-documenso-embed>
`
})
export class SigningWithServiceComponent implements OnInit {
token = 'token-xyz';
constructor(private embedService: DocumensoEmbedService) {}
ngOnInit(): void {
// Listen to all embed events globally
this.embedService.events$
.pipe(filter(event => event.action === 'document-completed'))
.subscribe(event => {
console.log('Document completed:', event.data);
});
}
}
Routing Setup
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { SigningPageComponent } from './components/signing-page/signing-page.component';
const routes: Routes = [
{
path: 'sign/:token',
component: SigningPageComponent
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
Testing
// documenso-embed.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DocumensoEmbedComponent } from './documenso-embed.component';
describe('DocumensoEmbedComponent', () => {
let component: DocumensoEmbedComponent;
let fixture: ComponentFixture<DocumensoEmbedComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ DocumensoEmbedComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(DocumensoEmbedComponent);
component = fixture.componentInstance;
component.token = 'test-token';
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should emit ready event when document is ready', (done) => {
component.ready.subscribe(() => {
expect(true).toBeTruthy();
done();
});
// Simulate postMessage
window.postMessage(
{ action: 'document-ready', data: null },
window.location.origin
);
});
});
Best Practices
- Always use DomSanitizer to safely handle iframe URLs
- Clean up event listeners in ngOnDestroy
- Validate message origins for security
- Use ChangeDetectionStrategy.OnPush for performance
- Provide TypeScript interfaces for all data structures
- Handle loading and error states gracefully
- Use services for shared embed logic
Next Steps
Vue SDK
Build Vue wrapper components
Direct Links
Learn about direct signing links
