I have the following timer card for a timer app that I am making and I want to have a border slowly wrap around it as it counts down similar to this code pen. https://codepen.io/Mamboleoo/pen/zYOJOGb The progress has to be controlled by JS.

I am using VueJS with Vuetify and here is the code that I have.
<v-col
cols="12"
sm="4"
xs="4" v-for="timer in formattedTimers" :key="timer.id">
<v-card :class="{jiggle : editmode}" max-width="200" class="mx-auto" outlined>
<v-list-item three-line>
<v-list-item-content>
<div class="headline mb-8 text-center">{{ timer.name }}</div>
<v-list-item-title class="headline mb-4 text-center">{{ parseTime(timer.timeLeft) }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-card-actions>
<div v-if="!editmode">
<v-btn @click="zeroTimer(timer.id)" left>Zero</v-btn>
<v-btn color="primary" @click="resetTimer(timer.id)" right absolute>Reset</v-btn>
</div>
<div v-else>
<v-btn color="primary" @click="deleteTimer(timer.id)" left>Delete</v-btn>
</div>
</v-card-actions>
</v-card>
</v-col>
You can directly use the SVG in the demo in Codepen you attached.
In order to make the border-image to be re-rendered when Reset is clicked, one trick is add empty space to the SVG string then convert it base64.
Below is one simple snippet:
Vue.component('v-timer',{
render (h) {
return h('div', {
style: {
border: '10px solid black',
borderImage: `url("data:image/svg+xml;base64,${this.computedSVGUrl}") 1`
}
}, [h('span', {}, `${this.inner} Secs`), h('button', {
on: {
click: () => {
this.inner = this.seconds
this.spaces += ' '
this.startTimer(this.seconds)
}
}
}, 'Reset')])
},
props: {
'seconds': {
type: Number,
default: 0
}
},
data () {
return {
spaces: '',
inner: 0,
intervalCtrl: null
}
},
computed: {
computedSVGUrl: function () {
return window.btoa(`<svg width='100' height='100' viewBox='0 0 100 100' fill='none' xmlns='http://www.w3.org/2000/svg'> <style>path{animation:stroke ${this.seconds}s linear;}@keyframes stroke{to{stroke-dashoffset:388;}}</style><linearGradient id='g' x1='0%' y1='0%' x2='0%' y2='100%'><stop offset='0%' stop-color='#2d3561' /><stop offset='25%' stop-color='#c05c7e' /><stop offset='50%' stop-color='#f3826f' /><stop offset='100%' stop-color='#ffb961' /></linearGradient> <path d='M1.5 1.5 l97 0l0 97l-97 0 l0 -97' stroke-linecap='square' stroke='url(#g)' stroke-width='3' stroke-dasharray='388'/> ${this.spaces} </svg>`)
}
},
watch: {
seconds: {
handler: function (newVal) {
this.inner = newVal
this.startTimer(newVal)
},
immediate: true
}
},
mounted: function () {
this.startTimer(this.seconds)
},
methods: {
startTimer: function (seconds) {
this.resetInterval()
this.intervalCtrl = setInterval(() => {
this.inner -= 1
this.inner <= 0 && this.resetInterval()
}, 1000)
},
resetInterval: function () {
this.intervalCtrl && clearInterval(this.intervalCtrl)
this.intervalCtrl = null
}
}
})
new Vue ({
el:'#app'
})
.timer {
width: 200px;
height: 30px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<div>
<div>
<v-timer :seconds="5" class="timer"></v-timer>
<v-timer :seconds="10" class="timer"></v-timer>
<v-timer :seconds="15" class="timer"></v-timer>
</div>
</div>
</div>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With