Signature Pad with Alpine.js
Signature Pads on websites
Signature pads are a standard feature nowadays when there's a need for authenticity by means of a signature. It is convenient and safe for the user to type/draw the signature rather than having a single image on their storage and upload where it is necessary. Signature pads enable us to record signatures in forms such as `png` or `data URL`. Let's use one of the Javascript libraries called Signature Pad for this tutorial.
The tutorial will be done over the Laravel framework terminology. But the same HTML can be used anywhere when you use Alpine.js with it. Also, note that I have used some Tailwind CSS classes to style the HTML.
Let's get started
First of all, let's create an anonymous blade component named signature-pad.blade.php
in the resources/views/components
directory. I'm using laravel's blade components for better usage. We can use includes too.
Also, Let's create a stack in our layout file resources/views/layouts/app.blade.php
to push the required Javascript from the component. This helps to avoid including the Javascript only when the component is on the page.
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
......
<!-- Tailwind Magic (dev) 😎 -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Include Alpine.js (V3) -->
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" </script>
</head>
<body class="grid h-screen place-items-center place-content-center">
...
@stack('scripts')
</body>
</html>
Also, let us assume we use the x-signature-pad
component in one of the views we render through the normal route/livewire component. We will mount our blade component in some.blade.php
<x-signature-pad/>
Integrate Signature Pad with Alpine.js
It is very easy to initialize the Signature Pad. What it expects is just a canvas to draw the signature. First of all, let's pull the Signature Pad library through CDN and initialize it with a canvas.
<div
x-data="{
signaturePad: null, // used to track the SignaturePad Instance
init() {
this.signaturePad = new SignaturePad(this.$refs.canvas);// initialize the canvas with Signature pad
}
}"
>
<canvas x-ref="canvas" class="w-full h-full border-2 border-gray-300 border-dashed rounded-md"></canvas>
</div>
<!-- Push to the stack we created in app.blade.php -->
@pushonce('scripts')
<script src="https://cdn.jsdelivr.net/npm/signature_pad@4.0.0/dist/signature_pad.umd.min.js"></script>
@endpushonce
Now you should see the signature pad on the screen.
It can be seen that I have used some of the Alpine.js features such as x-ref
to reference the canvas element without using any query selectors. Also, I have used the init()
function which will initialize the Signature Pad. Also, note pushonce
the directive to avoid pushing it multiple times to the stack.
Make it more fun
Once we have the signature pad ready, we can now introduce save()
and clear()
functionalities to the component. Let's use the PNG data URL as the format to save (it can directly be saved in a DB column and used again). There are various formats to save the input too. Please refer to the signature pad docs here for more details.
<div x-data="{
signaturePadId: $id('signature'), // track the pad ID when showing notification
signaturePad: null,
signature: null, // variable to save the signature
init() {
this.signaturePad = new SignaturePad($refs.canvas);
// load if the signature is not null (usefull to show the saved signature in db)
if (this.signature) {
this.signaturePad.fromDataURL(this.signature);
}
},
save() {
this.signature = this.signaturePad.toDataURL(); // save as data:image/png;base64,...
this.$dispatch('signature-saved', this.signaturePadId); // notify saved
},
clear() {
this.signaturePad.clear(); // clear the signature pad
this.signature = null;
}
}">
<canvas x-ref="canvas" class="w-full h-full border-2 border-gray-300 border-dashed rounded-md "></canvas>
<div class="flex mt-2 space-x-2">
<a href="#" x-on:click.prevent="clear()" class="text-sm font-medium text-gray-700 underline">
Clear
</a>
<a href="#" x-on:click.prevent="save()" class="text-sm font-medium text-gray-700 underline">
Save
</a>

<!-- A notification component to indicate if it is saved -->
<span x-data="{
open: false,
saved(e) {
// can have multiple in the same page. Only show it's parent event
if (e.detail != this.signaturePadId) {
return;
}
this.open = true;
setTimeout(() => { this.open = false }, 900);
}
}" x-show="open" @signature-saved.window="saved" x-transition
class="text-sm font-medium text-green-700" style="display:none">
Saved !
</span>
</div>
</div>
Now you should see the notification when you click save
also when you click clear
it will erase the signature pad. Note that I have used $id()
to give an id to the signature pad to show notifications.
Finishing touch
If you have noticed, The canvas was given a full height and width, but the signature pad would not take the whole width. Also when you load a signature from DB, it will be displayed smaller than the original signature. We need to resize the canvas and add the ratio as mentioned in their docs.
<div x-data="{
signaturePadId: $id('signature'),
signaturePad: null,
signature: null,
ratio: null,
init() {
this.resizeCanvas(); // resize canvas before initializing
this.signaturePad = new SignaturePad(this.$refs.canvas);
if (this.signature) {
// pass ratio when loading a saved signature
this.signaturePad.fromDataURL(this.signature, { ratio: this.ratio });
}
},
save() {
this.signature = this.signaturePad.toDataURL();
this.$dispatch('signature-saved', this.signaturePadId);
},
clear() {
this.signaturePad.clear();
this.signature = null;
},
// The resize canvas function https://github.com/szimek/signature_pad#tips-and-tricks
resizeCanvas() {
this.ratio = Math.max(window.devicePixelRatio || 1, 1);
this.$refs.canvas.width = this.$refs.canvas.offsetWidth * this.ratio;
this.$refs.canvas.height = this.$refs.canvas.offsetHeight * this.ratio;
this.$refs.canvas.getContext('2d').scale(this.ratio, this.ratio);
}
}" @resize.window="resizeCanvas">
<canvas x-ref="canvas" class="w-full h-full border-2 border-gray-300 border-dashed rounded-md "></canvas>
<div class="flex mt-2 space-x-2">
<a href="#" x-on:click.prevent="clear()" class="text-sm font-medium text-gray-700 underline">
Clear
</a>
<a href="#" x-on:click.prevent="save()" class="text-sm font-medium text-gray-700 underline">
Save
</a>
<span x-data="{
open: false,
saved(e) {
if (e.detail != this.signaturePadId) {
return;
}
this.open = true;
setTimeout(() => { this.open = false }, 900);
}
}" x-show="open" @signature-saved.window="saved" x-transition
class="text-sm font-medium text-green-700 " style="display:none">
Saved !
</span>
</div>
</div>
@pushonce('scripts')
<script src="https://cdn.jsdelivr.net/npm/signature_pad@4.0.0/dist/signature_pad.umd.min.js"></script>
@endpushonce
Integrating with Laravel Livewire
Livewire and Alpine go hand in hand and it is very much simple to integrate it with livewire. We can use the entangle
blade directive to sync the property of livewire with Alpine.js. I m using the attributes
property to get the wire:model
blade component. We can add wire:ignore
in the blade component to avoid initializing the component again when livewire re-renders.
<div {{ $attributes }} wire:ignore x-data="{
signaturePadId: $id('signature'),
signaturePad: null,
signature: @entangle($attributes->get('wire:model')),
.....
}" >
...
</div>
In some.blade.php
<x-signature-pad wire:model="user.signature"/>
Summary
The tutorial briefly explains the integration of the Signature Pad with Alpine.js. The complete component code can be found here. Alpine.js enables us to make simpler and more powerful components. The current implementation can be modified to use with Alpine.js(V2) too but you should try V3 just because it's more fun 😎.