adplus-dvertising

Signature Pad with Alpine.js

TLDR; The post describes the implementation of signature pads with Alpine.js. It also discusses how to integrate it with laravel livewire.

Posted by Mohamed Irfan on 2022-May-11

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 😎.