Friday, October 4, 2019

Capture Signature using HTML Canvas inside Salesforce Lightning Web Components

Motivation behind this


I am exploring Lightning Web Components and thought of preparing an use case on signature capturing functionality, but want to leverage native HTML Canvas element without using any 3rd party libraries. Earlier I have worked on this type of functionality using Visualforce page and didn't find any post on Lightning Web Components which motivates me to do this proof-of-concept. Though there are couple of solutions available using Lightning Aura Components.

There are few challenges I have faced during this development which I want to share to community members.

Let's get started.

Use Case


Business has a requirement to capture a signature of the Customer for finalizing a deal. 


Developer wants to build using Lightning Web Components without 3rd party libraries and want to leverage native HTML functionalities.

Possible End Results


After building the use case, it will perform the functionality as following video:


Solution Approach


We will create captureSignature component for this functionality.

Flow Diagram


Create a flow diagram like this way to understand the functionalities, flow of control and finalize design approach.


Before going through this below section, it will be better to go through my post on Salesforce Lightning Web Components Cheat Sheet to have overall idea for all types of properties, methods, event propagation etc.

captureSignature component:

captureSignature.html will prepare a screen like this:


Code is as follows:

HTML canvas element is used.


<template>
    <lightning-card class="slds-align_absolute-center" 
                title="Signature Capturing using HTML Canvas and Lightning Web Components" 
                icon-name="custom:custom30">
    <div class="c-container"> 
        <div style="text-align: center; "> 
            <h2 class="slds-text-heading_medium slds-m-bottom_medium">
                Sign Here
            </h2>            
            <p class="slds-m-bottom_small"> 
                <canvas name="canvasItem" style="border:2px solid rgb(136, 135, 135);
                background: transparent;"></canvas>
            </p>
            <p class="slds-m-bottom_small"> 
            <lightning-button variant="brand" label="Save" title="Save" 
                onclick={handleSaveClick} class="slds-m-left_x-small"></lightning-button>
            
            <lightning-button variant="brand" label="Clear" title="Clear" 
                onclick={handleClearClick} class="slds-m-left_x-small"></lightning-button>
            </p>
        </div>       
    </div>
    </lightning-card>
</template>

captureSignature.js

This is most important for implementing this functionality and if we follow the flow diagram it is easy to understand which methods are performing which responsibilities. Some important points to be highlighted:
  • import LightningElement, saveSign and ShowToastEvent which will be used in this class.
  • add event listeners for different mouse activities in constructor.
  • Required calculations have performed at setupCoordinate, redraw and drawDot methods.
  • handleSaveClick method, following below lines have been added otherwise canvas background will show black after generating image:
ctx.globalCompositeOperation = "destination-over";
ctx.fillStyle = "#FFF"; //white
ctx.fillRect(0,0,canvasElement.width, canvasElement.height);

  • handleSaveClick method, convert canvas drawing element to base64 encoded URL and then imperative way saveSign method is called.
  • We have used Javascript promise to show the results and errors.
saveSign({strSignElement: convertedDataURI})
 .then(result => {
  //parse result
 })
 .catch(error => {
  //show error message
  
 });

Entire js file as below:


  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
/* eslint-disable no-console */
import { LightningElement } from 'lwc';
import saveSign from '@salesforce/apex/SignatureHelper.saveSign';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';

//declaration of variables for calculations
let isDownFlag, 
    isDotFlag = false,
    prevX = 0,
    currX = 0,
    prevY = 0,
    currY = 0;            
       
let x = "#0000A0"; //blue color
let y = 1.5; //weight of line width and dot.       

let canvasElement, ctx; //storing canvas context
let attachment; //holds attachment information after saving the sigture on canvas
let dataURL,convertedDataURI; //holds image data

export default class CapturequestedEventignature extends LightningElement {
    
    //event listeners added for drawing the signature within shadow boundary
    constructor() {
        super();
        this.template.addEventListener('mousemove', this.handleMouseMove.bind(this));
        this.template.addEventListener('mousedown', this.handleMouseDown.bind(this));
        this.template.addEventListener('mouseup', this.handleMouseUp.bind(this));
        this.template.addEventListener('mouseout', this.handleMouseOut.bind(this));
    }

    //retrieve canvase and context
    renderedCallback(){
        canvasElement = this.template.querySelector('canvas');
        ctx = canvasElement.getContext("2d");
    }
    
    //handler for mouse move operation
    handleMouseMove(event){
        this.searchCoordinatesForEvent('move', event);      
    }
    
    //handler for mouse down operation
    handleMouseDown(event){
        this.searchCoordinatesForEvent('down', event);         
    }
    
    //handler for mouse up operation
    handleMouseUp(event){
        this.searchCoordinatesForEvent('up', event);       
    }

    //handler for mouse out operation
    handleMouseOut(event){
        this.searchCoordinatesForEvent('out', event);         
    }
    
    /*
        handler to perform save operation.
        save signature as attachment.
        after saving shows success or failure message as toast
    */
    handleSaveClick(){    
        //set to draw behind current content
        ctx.globalCompositeOperation = "destination-over";
        ctx.fillStyle = "#FFF"; //white
        ctx.fillRect(0,0,canvasElement.width, canvasElement.height); 

        //convert to png image as dataURL
        dataURL = canvasElement.toDataURL("image/png");
        //convert that as base64 encoding
        convertedDataURI = dataURL.replace(/^data:image\/(png|jpg);base64,/, "");
        
        //call Apex method imperatively and use promise for handling sucess & failure
        saveSign({strSignElement: convertedDataURI})
            .then(result => {
                this.attachment = result;
                console.log('attachment id=' + this.attachment.Id);
                //show success message
                this.dispatchEvent(
                    new ShowToastEvent({
                        title: 'Success',
                        message: 'Attachment created with Signature',
                        variant: 'success',
                    }),
                );
            })
            .catch(error => {
                //show error message
                this.dispatchEvent(
                    new ShowToastEvent({
                        title: 'Error creating Attachment record',
                        message: error.body.message,
                        variant: 'error',
                    }),
                );
            });
            
    }

    //clear the signature from canvas
    handleClearClick(){
        ctx.clearRect(0, 0, canvasElement.width, canvasElement.height);          
    }

    searchCoordinatesForEvent(requestedEvent, event){
        event.preventDefault();
        if (requestedEvent === 'down') {
            this.setupCoordinate(event);           
            isDownFlag = true;
            isDotFlag = true;
            if (isDotFlag) {
                this.drawDot();
                isDotFlag = false;
            }
        }
        if (requestedEvent === 'up' || requestedEvent === "out") {
            isDownFlag = false;
        }
        if (requestedEvent === 'move') {
            if (isDownFlag) {
                this.setupCoordinate(event);
                this.redraw();
            }
        }
    }

    //This method is primary called from mouse down & move to setup cordinates.
    setupCoordinate(eventParam){
        //get size of an element and its position relative to the viewport 
        //using getBoundingClientRect which returns left, top, right, bottom, x, y, width, height.
        const clientRect = canvasElement.getBoundingClientRect();
        prevX = currX;
        prevY = currY;
        currX = eventParam.clientX -  clientRect.left;
        currY = eventParam.clientY - clientRect.top;
    }

    //For every mouse move based on the coordinates line to redrawn
    redraw() {
        ctx.beginPath();
        ctx.moveTo(prevX, prevY);
        ctx.lineTo(currX, currY);
        ctx.strokeStyle = x; //sets the color, gradient and pattern of stroke
        ctx.lineWidth = y;        
        ctx.closePath(); //create a path from current point to starting point
        ctx.stroke(); //draws the path
    }
    
    //this draws the dot
    drawDot(){
        ctx.beginPath();
        ctx.fillStyle = x; //blue color
        ctx.fillRect(currX, currY, y, y); //fill rectrangle with coordinates
        ctx.closePath();
    }
}

SignatureHelper.cls

saveSign method takes String parameter which has been used as Attachment body after decoding. Here we have saved the Attachment under a Dummy Contact record.



 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public with sharing class SignatureHelper {
    
    @AuraEnabled
    public static Attachment saveSign(String strSignElement){
            Contact con = new Contact(Id='0032v00002ypAntAAE');
            Attachment objAttachment = new Attachment();
            objAttachment.Name = 'Demo-Signature.png';
            objAttachment.ParentId = con.Id;
            objAttachment.ContentType = 'image/png';
            objAttachment.Body = EncodingUtil.base64Decode(strSignElement);        
            insert objAttachment;
            return objAttachment;       
    }
}

When we initially try to save attachment, we could face this below error if (cacheable=true) is used with @AuraEnabled

Too many DML statements: 1 out of 0 Error

which means that component is readonly and it doesn't allow to perform DML operation.

That's why cacheable=true is omitted. 

Nice learning!

meta files should include where these components are available.

Create a Lightning App Builder page with one region and place this component and run the application, it will display the screen as above.

Finally, we are done and thanks for reading.


References


Further Reading



3 comments: