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>
Add some style :
<style scoped>
canvas {
border: 1px solid black;
background-color: white;
cursor: crosshair;
}
</style>
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
}
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>
data(){
return {
...
sign : false,
prevX : null,
prevY : null
}
}
methods: {
onMouseDown($event){
this.sign = true
this.prevX = $event.offsetX
this.prevY = $event.offsetY
}
}
...
- 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>
methods: {
...
mousemove($event) {
if(this.sign) {
const currX = $event.offsetX
const currY = $event.offsetY
}
},
}
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()
}
}
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>
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
}
},
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)
}
}
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)
}
}
}
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>
import MyCanvasComponent from '@/components/MyCanvasComponents.vue
export default {
components : {
MyCanvasComponent
},
data(){
return {
canvas : null
}
}
}
The entire component code :
<template>
<canvas @mousedown="mousedown" @mousemove="mousemove" @mouseup="sign = false" @mouseout="sign = false" />
</template>
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
}
}
<style scoped>
canvas {
border: 1px solid black;
background-color: white;
cursor: crosshair;
}
</style>
Top comments (5)
thanks but please can you explain how you used the modelValue prop?
Awesome!
Cool! Thanks for sharing ♥
I Tru this bit my clear drawing function is not working, I don't know if there is something wrong with the watcher that's is suppose to update if v-model is empty
Thanks