Skip to main content
While Documenso doesn’t provide an official Angular SDK, you can easily create reusable Angular components to wrap the embedding functionality. This guide shows you how to build production-ready Angular components for document signing.

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

  1. Always use DomSanitizer to safely handle iframe URLs
  2. Clean up event listeners in ngOnDestroy
  3. Validate message origins for security
  4. Use ChangeDetectionStrategy.OnPush for performance
  5. Provide TypeScript interfaces for all data structures
  6. Handle loading and error states gracefully
  7. Use services for shared embed logic

Next Steps

Vue SDK

Build Vue wrapper components

Direct Links

Learn about direct signing links