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:
captureSignature.html will prepare a screen like this:
Code is as follows:
HTML canvas element is used.
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 });
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.