Create Signature Pad Component in Vue

raphbensimon

BENSIMON Raphaël

Posted on September 14, 2021

Create Signature Pad Component in Vue

Hello, today we will see how to create a signature pad component using vuejs.

Creating your own components is very useful when you have specific needs, it also allows you to learn the logic behind the component.

Use canvas

We are going to use a canvas HTML tag, this will allow the user to draw his signature.

<template>
    <canvas />
</template>
Enter fullscreen mode Exit fullscreen mode

Add some style :

<style scoped>
canvas {
    border: 1px solid black;
    background-color: white;
    cursor: crosshair;
}
</style>
Enter fullscreen mode Exit fullscreen mode

Note:

  • Here we use the scoped property on the style tag which allows to keep the style inside the component.
  • I define the cursor by a crosshair (the details make the differences).

It's time to use javascript !

First of all we will get our canvas and pass it some parameters

data() {
    return {
        ctx :  null,
    }
},
mounted(){
    this.ctx  = this.$el.getContext('2d')
    this.ctx.strokeStyle  =  'black'
    this.ctx.lineWidth  =  2
}
Enter fullscreen mode Exit fullscreen mode

Notes:

  • strokeStyle is the color of the signature
  • lineWidth is the width of the signature

Let's add the mousedown event to our canvas which will let us know when the user clicks on our canvas.

<template>
    <canvas @mousedown=”onMouseDown” />
</template>
Enter fullscreen mode Exit fullscreen mode
data(){
    return {
        ...
        sign  : false,
        prevX : null,
        prevY : null
    }
}
methods: {
    onMouseDown($event){
        this.sign = true
        this.prevX = $event.offsetX
        this.prevY = $event.offsetY
    }
}
...
Enter fullscreen mode Exit fullscreen mode
  • The sign property allows to know if the user has clicked on the canvas.
  • The prevX and prevY properties allow to know the current position of the cursor by retrieving it from $event.

We shift into second gear !

We will add the mousemove event to our canvas :

<template>
    <canvas ... @mousemove="onMouseMove" />
</template>
Enter fullscreen mode Exit fullscreen mode
methods: {
    ...
    mousemove($event) {
        if(this.sign) {
            const  currX  = $event.offsetX
            const  currY  = $event.offsetY
        }
    },
}
Enter fullscreen mode Exit fullscreen mode

Here we get the current position of the pointer which will allow us to draw the signature thanks to the previous position we got in the @onmousedown event.

Draw the signature

methods: {
    ...
    mousemove($event) {
        if(this.sign) {
            const  currX  = $event.offsetX
            const  currY  = $event.offsetY
            this.draw(this.prevX, this.prevY, currX, currY)
            this.prevX  =  currX
            this.prevY  =  currY
        }
    },
    draw(depX, depY, destX, destY){
        this.ctx.beginPath()
        this.ctx.moveTo(depX, depY)
        this.ctx.lineTo(destX, destY)
        this.ctx.closePath()
        this.ctx.stroke()
    }
}
Enter fullscreen mode Exit fullscreen mode

Remarks:

  • beginPath() allows to start a path
  • moveTo() allows to initialize the starting point
  • lineTo() allows to describe the arrival point
  • closePath() closes the path
  • stroke() allows to apply the path to the canvas

Now we will prevent the user from drawing on the canvas if :

  • His cursor is outside the canvas
  • His cursor is not clicking anymore
<template>
    <canvas ... @mouseup="sign = false" @mouseout="sign = false" />
</template>
Enter fullscreen mode Exit fullscreen mode

Get the v-model and store the canvas.

Let's define the emit update and the modelValue props

emits : ['update:modelValue'],
props : {
    modelValue : {
        type :  null,
        required :  true
    }
},
Enter fullscreen mode Exit fullscreen mode

Let's transform our canvas drawing into an image and update the v-model in our draw method:

methods: {
    ...
    draw(depX, depY, destX, destY) {
        this.ctx.beginPath()
        this.ctx.moveTo(depX, depY)
        this.ctx.lineTo(destX, destY)
        this.ctx.closePath()
        this.ctx.stroke()

        const img = this.$el.toDataURL('image/png').replace('image/png',        'image/octet-stream')
        this.$emit('update:modelValue', img)
    }
}
Enter fullscreen mode Exit fullscreen mode

Last step !

Now we have to check if the v-model of our component is empty in order to remove our canvas drawing

watch : {
    modelValue(model) {
        if(!model) {
            this.ctx.clearRect(0, 0, this.$el.width, this.$el.height)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

That's it!

To use our component in a parent view here is how to do it:

<template>
    <MyCanvasComponent v-model="canvas" />
    <button @click="canvas = null">Delete your signature</button>
</template>
Enter fullscreen mode Exit fullscreen mode
import MyCanvasComponent from '@/components/MyCanvasComponents.vue
export default {
    components : {
        MyCanvasComponent
    },
    data(){
        return {
            canvas : null
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The entire component code :

<template>
    <canvas @mousedown="mousedown" @mousemove="mousemove" @mouseup="sign = false" @mouseout="sign = false" />
</template>
Enter fullscreen mode Exit fullscreen mode
export  default {
    emits : ['update:modelValue'],
    props : {
        modelValue : {
            type :  null,
            required :  true
        }
    },
    data() {
        return {
            ctx :  null,
            sign :  false,
            prevX :  0,
            prevY :  0,
        }
    },
    methods : {
        mousedown($event) {
            this.sign  =  true
            this.prevX  = $event.offsetX
            this.prevY  = $event.offsetY
        },
        mousemove($event) {
            if(this.sign) {
                const  currX  = $event.offsetX
                const  currY  = $event.offsetY
                this.draw(this.prevX, this.prevY, currX, currY)
                this.prevX  =  currX
                this.prevY  =  currY
            }
        },
        draw(depX, depY, destX, destY) {
            this.ctx.beginPath()
            this.ctx.moveTo(depX, depY)
            this.ctx.lineTo(destX, destY)
            this.ctx.closePath()
            this.ctx.stroke()

            const img = this.$el.toDataURL('image/png').replace('image/png', 'image/octet-stream')
            this.$emit('update:modelValue', img)
        },
    },
    watch : {
        modelValue(model) {
            if(!model) {
            this.ctx.clearRect(0, 0, this.$el.width, this.$el.height)
            }
        }
    },
    mounted() {
        this.ctx  = this.$el.getContext('2d')
        this.ctx.strokeStyle  =  'black'
        this.ctx.lineWidth  =  2
    }
}
Enter fullscreen mode Exit fullscreen mode
<style scoped>
canvas {
    border: 1px solid black;
    background-color: white;
    cursor: crosshair;
}
</style>
Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
raphbensimon
BENSIMON Raphaël

Posted on September 14, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related