Skip to content

Instantly share code, notes, and snippets.

@calebergh
Created February 23, 2020 18:59
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save calebergh/65b7f0f8b5d7e5d1e6cb977a4cb907f4 to your computer and use it in GitHub Desktop.
Save calebergh/65b7f0f8b5d7e5d1e6cb977a4cb907f4 to your computer and use it in GitHub Desktop.
Electronic Signature Pad for Angular 8
import { Directive, ElementRef, EventEmitter, HostListener, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
@Directive({
// tslint:disable-next-line:directive-selector
selector: '[signature]',
})
export class SignatureDirective implements OnInit, OnChanges {
@Output() imgSrc = new EventEmitter<string>();
@Input('clear') clearMe;
@Input('options') options;
private context;
private density: number;
private isDrawing = false;
private sigPadElement;
ngOnInit() {
this.sigPadElement = this.signaturePad.nativeElement;
this.context = this.sigPadElement.getContext('2d');
this.context.strokeStyle = this.options.color || '#3742fa';
this.context.lineWidth = this.options.width || 2;
if ( this.options.blur) {
this.context.shadowBlur = this.options.blur || 2;
this.context.shadowColor = this.options.blurColor || this.context.strokeStyle;
}
this.context.lineCap = 'round';
this.context.lineJoin = 'round';
this.density = window.devicePixelRatio || 1;
}
ngOnChanges(changes: SimpleChanges) {
if (!this.context) {
return;
}
if (changes.clearMe) {
this.clear();
this.save();
}
}
constructor(private signaturePad: ElementRef) {
}
@HostListener('touchstart', ['$event'])
handleTouchStart(e) {
e.preventDefault();
this.isDrawing = true;
this.start(e, 'layer');
}
@HostListener('touchmove', ['$event'])
handleTouchMove(e) {
if (!this.isDrawing) {
return;
}
e.preventDefault();
this.draw(e, 'layer');
}
@HostListener('document:touchend', ['$event'])
@HostListener('document:mouseup', ['$event'])
onMouseUp() {
if ( this.isDrawing) {
this.save();
}
this.isDrawing = false;
}
@HostListener('mousedown', ['$event'])
onMouseDown(e) {
this.isDrawing = true;
this.start(e);
}
@HostListener('mousemove', ['$event'])
onMouseMove(e) {
if (!this.isDrawing) {
return;
}
this.draw(e);
}
private start(e, type: 'client' | 'layer' = 'client') {
const coords = this.relativeCoords(e, type);
this.context.moveTo(coords.x, coords.y);
}
private draw(e, type: 'client' | 'layer' = 'client') {
const coords = this.relativeCoords(e, type);
this.context.lineTo(coords.x, coords.y);
this.context.stroke();
}
private relativeCoords(event, type: 'client' | 'layer') {
const bounds = this.sigPadElement.getBoundingClientRect();
let x = event[ type + 'X' ];
let y = event[ type + 'Y' ];
if ('layer' !== type) {
x -= bounds.left;
y -= bounds.top;
}
return { x, y };
}
clear() {
this.context.clearRect(0, 0, this.sigPadElement.width, this.sigPadElement.height);
this.context.beginPath();
}
save() {
this.imgSrc.emit(this.sigPadElement.toDataURL('image/png'));
}
}
@calebergh
Copy link
Author

calebergh commented Feb 23, 2020

This is a minimal electronic "signature" pad, compatible with desktop and touchscreen devices.

Why?

No libraries or demos I found worked properly with Angular 8+ and touch devices. This aims to be a super-simple, within-your-control, easy to use directive.

This started out as a component, but in order to get proper scoping on the @HostListeners (to just the target canvas), it made more sense as a directive.

I've tested on limited machines / browsers, so if you see something that doesn't work, that's not surprising. (Mac in Chrome and Firefox, iPhone 8+)

Example usage:

Be sure to include this directive in your app.module.ts file, in the declarations.

In the component template file:

<canvas *ngIf="signing" signature="signature" width="350" height="120" [clear]="clearMe" [options]="{color: '#009', width: 1, blur: 2}" (imgSrc)="srcChange($event)"></canvas>
<div *ngIf="!signing"><img class="signature" src="{{src}}" />
    <p class="form-text">Signed<a (click)="resign()">Re-sign</a></p>
</div>
<p *ngIf="signing"><a (click)="clear()">Clear</a><a (click)="save()">Save</a></p>

In the component controller:

import { Component } from '@angular/core';

@Component({
  templateUrl: './my.component.html'
})
export class MyComponent {
  public src: string;
  public clearMe = false;
  public signing = true;

  constructor() {
  }

  srcChange(src) {
    this.src = src;
  }

  clear() {
    this.clearMe = !this.clearMe;
  }

  save() {
    this.signing = false;
  }

  resign() {
    this.clear();
    this.signing = true;
  }
}

WARNING:

Attempting to adjust the width / height via CSS has undesirable results. For this to work properly, it appears the width and height HTML attributes need to be set.

@jotiheranbnach
Copy link

My honest Kudos!

@ahmedfarid-dev
Copy link

first of all ,thanks a lot for this great directive
I have only one problem ,when toggling device toolbar on google chrome ,to test the code in responsive and in different screens ,it doesn't draw and events in the directive are not fired ,any idea why is that ?

@calebergh
Copy link
Author

first of all ,thanks a lot for this great directive I have only one problem ,when toggling device toolbar on google chrome ,to test the code in responsive and in different screens ,it doesn't draw and events in the directive are not fired ,any idea why is that ?

The directive does not watch for window resize events, but certainly could be made to do so. Adding a HostListener for document resize, and triggering some of the "init" code would likely make it work in a responsive manner.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment