I have a stopwatch code using Vue 3 and Vuex that has functions that can start, stop and reset.
This is the code :
store/stopwatch/index.js
export default {
state: {
time: "00:10.000",
timeStarted: null,
timeBegan: null,
timeStopped: null,
stoppedDuration: 0,
started: null,
running: false,
temp: {
min: "0",
sec: "0",
ms: "0",
secondsPassed: 0
}
},
actions: {
start({ state, commit, dispatch }) {
if (state.running) return;
if (state.timeBegan === null) {
state.timeBegan = new Date();
}
if (state.timeStopped !== null) {
state.stoppedDuration += new Date() - state.timeStopped;
}
commit("start", {
callback: () => {
dispatch("clockRunning");
}
});
},
async clockRunning({ state, commit, dispatch }) {
let currentTime = new Date();
let timeElapsed = new Date(
currentTime - state.timeBegan - state.stoppedDuration
);
let min = timeElapsed.getUTCMinutes();
let sec = timeElapsed.getUTCSeconds();
let ms = timeElapsed.getUTCMilliseconds();
commit("newTemp", {
key: "secondsPassed",
value: parseInt(Math.abs((state.timeStarted - new Date()) / 1000), 10)
});
if (state.running) {
await dispatch("zeroPrefix", { num: min, digit: 2 }).then(
(zeroPrefixResponse) => {
commit("newTemp", {
key: "min",
value: zeroPrefixResponse
});
}
);
await dispatch("zeroPrefix", { num: sec, digit: 2 }).then(
(zeroPrefixResponse) => {
commit("newTemp", {
key: "sec",
value: zeroPrefixResponse
});
}
);
await dispatch("zeroPrefix", { num: ms, digit: 3 }).then(
(zeroPrefixResponse) => {
commit("newTemp", {
key: "ms",
value: zeroPrefixResponse
});
}
);
state.time =
state.temp.min + ":" + state.temp.sec + "." + state.temp.ms;
}
},
zeroPrefix(context, payload) {
return new Promise((resolve) => {
let zero = "";
for (let i = 0; i < payload.digit; i++) {
zero += "0";
}
resolve((zero + payload.num).slice(-payload.digit));
});
}
},
mutations: {
newTemp(state, payload) {
state.temp[payload.key] = payload.value;
},
addSecondPassed(state, second) {
state.temp.secondsPassed += second;
},
resetSecondPassed(state) {
state.temp.secondsPassed = 0;
},
start(state, payload) {
state.started = setInterval(() => {
payload.callback();
}, 10);
state.running = true;
},
stop(state) {
state.running = false;
state.timeStopped = new Date();
clearInterval(state.started);
},
reset(state) {
state.running = false;
clearInterval(state.started);
state.stoppedDuration = 0;
state.timeBegan = null;
state.timeStopped = null;
state.time = "00:10.000";
}
},
getters: {}
};
App.vue
<template>
<div>
<span class="time">{{ $store.state.stopwatch.time }}</span>
<br />
<button #click="start">Start</button>
<button #click="stop">Stop</button>
<button #click="reset">Reset</button>
</div>
</template>
<script>
export default {
methods: {
start() {
this.$store.dispatch("start");
},
stop() {
this.$store.commit("stop");
},
reset() {
this.$store.commit("reset");
},
},
};
</script>
This is the demo code on codesandbox
What happens with the code above by starting from the 10 seconds and then clicking the start button, seconds starting from the number 0 then 1,2,3. do not continue from number 10.
How to start stopwatch from state time with 10 seconds?
So when click the start button, seconds continue from 10 then to 11,12,13 and so on.
Looks like the issue is that the 10s is not added to the timeBegan. If you subtract the 10 seconds, it should work.
if (state.timeBegan === null) {
state.timeBegan = new Date() - 10_000;
}
On a side note, a mutation should be limited to just the mutation
This code is adding and removing interval from within the mutation and goes against how mutations are supposed to work.
start(state, payload) {
state.started = setInterval(() => {
payload.callback();
}, 10);
state.running = true;
},
stop(state) {
state.running = false;
state.timeStopped = new Date();
clearInterval(state.started);
},
And executing a callback from the mutation is also not good. Not that it won't work, but it's creating a weird state flow that goes against the principles of how the store works.
Related
I have created a countdown timer which decrease a number in the template perfectly, but now I need it to launch a function declared within methods after it reaches 0. I've tried to check if condition is met within methods but it doesn't launch anything when reaching 0.
Here is my index.vue
<template>
<div>
{{ timerCount }}
</div>
</template>
<script>
export default {
data(){
return{
timerCount: 60
}
},
watch: {
timerEnabled(value) {
if (value) {
setTimeout(() => {
this.timerCount = this.timerCount - 1;
}, 1000);
}
},
timerCount: {
handler(value) {
if (value > 0 && this.timerEnabled) {
setTimeout(() => {
this.timerCount = this.timerCount - 1;
}, 1000);
}
},
immediate: true
},
},
methods:{
launchThis() {
// I need this function to be launched if timerCount reaches 0
}
}
}
</script>
Any guidance to make it work will greatly appreciated.
You can use something like this
<script>
export default {
watch: {
timerCount: {
handler(value) {
if (value > 0 && this.timerEnabled) {
setTimeout(() => {
this.timerCount = this.timerCount - 1;
}, 1000);
} else {
this.launchThis() // run your function here
}
},
immediate: true
},
},
}
</script>
We are developing a website with unique navigation. Part of it involves on each scroll either up or down, it fires JavaScript and navigates to a different HTML element. It is in Vue.js / Nuxt.
So far, everything works beautifully, minus the usage of the trackpad. The initial 2-finger swipe with the trackpad works -- however, this seems to initiate some sort of a smooth scroll, which takes a while to complete. If you try to 2-finger swipe again in the same direction, it's treated as one long scroll, which doesn't fire the JavaScript to advance to the next page. I did a console log of the deltaY and since it's a 2-finger swipe (smooth scroll?), the deltas take a second or two to finish.
This causes issues since you can't use the trackpad and swipe through sections quickly. You have to swipe down, wait until the scroll finishes, then swipe down again.
How would we fix this issue? Is there a way to kill the current scroll, or to eliminate smooth scrolling and just have scrolls go 100 deltas in one way or the other?
Thanks in advance
Vue.js code
export default {
layout: 'empty',
components: {
Footer,
Logo,
LottieAnimation,
ServiceIcon,
CloseBtn
},
async asyncData({ app }) {
try {
return await app.$api.$get('/services.json')
} catch (error) {
return {
error
}
}
},
data() {
return {
activePage: 0,
config: {},
error: null,
goToPageTimer: null,
isTouchpad: null,
page: {},
scrollDisabled: false,
scrollPercentage: 0,
scrollPercentageOld: 0,
scrollDirection: null,
scrollTimer: null,
touchStart: null,
touchTimer: null,
lastWheelDirection: null,
lastWheelEvent: null,
wheelEventsCount: 0,
wheelEventsLimit: 25,
wheelStopTime: 120
}
},
watch: {
scrollPercentage(newValue, oldValue) {
this.scrollDirection = newValue > oldValue ? 'bottom' : 'top'
if (this.scrollDirection === 'bottom') {
this.goToNextPage()
} else {
this.goToPreviousPage()
}
}
},
mounted() {
window.addEventListener('keydown', this.onKeyDown)
window.addEventListener('scroll', this.handleScroll)
window.addEventListener('wheel', this.onMouseWheel)
window.addEventListener('touchend', this.onTouchEnd)
window.addEventListener('touchstart', this.onTouchStart)
this.initPage()
},
destroyed() {
window.removeEventListener('keydown', this.onKeyDown)
window.removeEventListener('scroll', this.handleScroll)
window.removeEventListener('wheel', this.onMouseWheel)
window.removeEventListener('touchend', this.onTouchEnd)
window.removeEventListener('touchstart', this.onTouchStart)
},
methods: {
disableScrolling() {
this.scrollDisabled = true
if (this.goToPageTimer) {
clearTimeout(this.goToPageTimer)
}
this.goToPageTimer = setTimeout(() => {
this.scrollDisabled = false
}, 30)
},
getServicePageId(slug) {
let servicePageId = null
Object.keys(this.page.childPages).forEach((pageId) => {
if (this.page.childPages[pageId].slug === slug) {
servicePageId = parseInt(pageId)
}
})
return servicePageId
},
goToNextPage() {
if (
this.scrollDisabled ||
this.activePage === this.page.childPages.length
) {
return
}
this.activePage = this.activePage === null ? 0 : this.activePage + 1
if (this.activePage < this.page.childPages.length) {
this.scrollToActivePage()
}
},
goToPreviousPage() {
if (this.scrollDisabled) {
return
}
if (!this.activePage) {
return
}
this.activePage -= 1
this.scrollToActivePage()
},
goToPage(index) {
if (this.scrollDisabled) {
return
}
this.activePage = index
this.scrollToActivePage()
},
handleScroll() {
// If scrolling to top do nothing
if (!window.scrollY) {
return
}
if (this.activePage < this.page.childPages.length - 1) {
window.scrollTo(0, 0)
}
},
initPage() {
if (this.$route.query.service) {
this.activePage = this.getServicePageId(this.$route.query.service)
}
},
isServiceActiveOrIsLatestService(slug) {
const servicePageId = this.getServicePageId(slug)
// Service is active
if (this.activePage === servicePageId) {
return true
}
const latestServicePageId = this.page.childPages.length - 1
// Service is the latest and active page is over it (user is looking the footer)
return (
servicePageId === latestServicePageId &&
this.activePage > latestServicePageId
)
},
onKeyDown(e) {
const nextPageKeys = [
34, // Page down
39, // Arrow right
40 // Arrow down
]
const previousPageKeys = [
33, // Page up
37, // Arrow left
38 // Arrow up
]
if (nextPageKeys.includes(e.keyCode)) {
this.goToNextPage()
} else if (previousPageKeys.includes(e.keyCode)) {
this.goToPreviousPage()
}
},
onMouseWheel(event) {
const now = +new Date()
const millisecondsSinceLastEvent = this.lastWheelEvent
? now - this.lastWheelEvent
: 0
const eventsLimitReached = this.wheelEventsCount > this.wheelEventsLimit
const stopTime = this.wheelStopTime
const delta = Math.sign(event.deltaY)
const directionChanged = delta !== 0 && this.lastWheelDirection !== delta
this.lastWheelEvent = now
if (directionChanged) {
this.lastWheelDirection = delta
}
if (
!directionChanged &&
!eventsLimitReached &&
millisecondsSinceLastEvent &&
millisecondsSinceLastEvent <= stopTime
) {
this.wheelEventsCount += 1
return
}
this.wheelEventsCount = 0
if (delta === -1) {
this.goToPreviousPage()
} else {
this.goToNextPage()
}
},
onTouchEnd(e) {
const now = +new Date()
const touchEndX = e.changedTouches[0].clientX
const touchEndY = e.changedTouches[0].clientY
// Try to guess single touch based on touch start - touch end time
const isSingleTouch = now - this.touchTimer <= 100
// Single touch
if (isSingleTouch && this.touchStart.x === touchEndX) {
const horizontalPercentage = Math.ceil(
(touchEndX * 100) / window.innerWidth
)
const verticalPercentage = Math.ceil(
(touchEndY * 100) / window.innerHeight
)
if (horizontalPercentage <= 40) {
this.goToPreviousPage()
return
}
if (horizontalPercentage >= 60) {
this.goToNextPage()
return
}
if (verticalPercentage <= 40) {
this.goToPreviousPage()
return
}
if (verticalPercentage >= 60) {
this.goToNextPage()
return
}
}
// Touch move
if (this.touchStart.y > touchEndY + 5) {
this.goToNextPage()
} else if (this.touchStart.y < touchEndY - 5) {
this.goToPreviousPage()
}
},
onTouchStart(e) {
this.touchTimer = +new Date()
this.touchStart = {
x: e.touches[0].clientX,
y: e.touches[0].clientY
}
},
onVisibilityChanged(isVisible, entry) {
if (isVisible) {
entry.target.classList.add('dynamic-active')
} else {
entry.target.classList.remove('dynamic-inactive')
}
},
scrollToActivePage() {
this.scrollToService(this.page.childPages[this.activePage].slug)
},
scrollToRef(ref) {
if (!this.$refs[ref]) {
return
}
if (this.$refs[ref][0]) {
this.$scrollTo(this.$refs[ref][0], 100, {
force: true,
cancelable: false
})
} else {
this.$scrollTo(this.$refs[ref], 100, { force: true, cancelable: false })
}
this.disableScrolling()
},
scrollToService(slug) {
this.scrollToRef(`service-${slug}`)
},
serviceBgImageUrl(service) {
return require(`~/assets/img/services/backgrounds/${service.slug}.jpg`)
}
},
head() {
const data = {
bodyAttrs: {
class: 'services'
}
}
if (this.page.data) {
data.title = this.page.data.title
data.meta = this.page.metadata
}
return data
}
}
I'd like to know how can I make time constantly update itself. So when I press the play button the seconds start to update automatically from 0:00 to the end , because now it just updates onclick. I am trying to use HTML5 audio and I have successfully managed to get the time updating as a label from this line of code:
sound.ontimeupdate = function () { document.getElementById('Time').innerHTML = sound.currentTime.toFixed() }
But thats not what I need, I would like to get the time attribute in data() to get updated and displayed in the label as shown in my HTML code.
I tried adding an event listener but it did not work... It gets called and every call was logged with console.log but time attribute was not updated
let sound = null
export default {
data () {
return {
isPlaying: false,
time: 0,
duration: 0
}
},
methods: {
playMusic () {
if (!sound) {
sound = new Audio(require('assets/YES.mp3'))
}
this.isPlaying = true
sound.play()
// sound.addEventListener('timeupdate', function () { this.time = sound.currentTime.toFixed() }) -- did not work
this.time = sound.currentTime.toFixed()
}
Html:
<label id="Time" #timeupdate>
{ { time } }:{ { duration } }
</label>
Inside your addEventListener you get a different this than you might expect.
Either use fat arrow
sound.addEventListener('timeupdate', () => this.time = sound.currentTime.toFixed() )
or, the old way, save this
let that = this
sound.addEventListener('timeupdate', function () { that.time = sound.currentTime.toFixed() })
you could just add a generic timer dynamically. You can use a watch to add/remove it like so:
(untested code)
export default {
data() {
return {
isPlaying: false,
time: 0,
duration: 0,
intervalId: null,
sound: null
};
},
watch: {
isPlaying(isPlaying) {
if (this.intervalId !== null) {
clearInterval(this.intervalId);
}
if (isPlaying) {
this.sound.play();
this.intervalId = setInterval(() => {
this.time = this.sound.currentTime.toFixed();
}, 500);
} else {
this.sound.stop();
}
}
},
methods: {
playMusic() {
if (!this.sound) {
this.sound = new Audio(require("assets/YES.mp3"));
}
this.isPlaying = true;
}
}
};
Please help me it always says Cannot read property 'commit' of undefined. Here is my code in store.js.
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export const store = new Vuex.Store({
state:{
timer: null,
totaltime: (25 * 60)
},
mutations:{
startTimer: (state, context) => {
//here is the problem part I think
state.timer = setInterval(() => context.commit('countDown'),
1000)
},
countDown: state => {
var time = state.totaltime
if(time >= 1){
time--
}else{
time = 0
}
},
stopTimer: state => {
clearInterval(state.timer)
state.timer = null
},
resetTimer: state => {
state.totaltime = (25 * 60)
clearInterval(state.timer)
}
},
getters:{
minutes: state => {
const minutes = Math.floor(state.totaltime / 60);
return minutes;
},
seconds: (state, getters) => {
const seconds = state.totaltime - (getters.minutes * 60);
return seconds;
}
},
actions:{
}
})
I have problem it debugging. it always says like this
'Cannot read property 'commit' of undefined'
Here is my Timer.vue code for calling
methods: {
formTime(time){
return (time < 10 ? '0' : '') + time;
},
startTimer(){
this.resetButton = true
this.$store.commit('startTimer')
},
stopTimer(){
this.$store.commit('stopTimer')
},
resetTimer(){
this.$store.commit('resetTimer')
},
},
computed: {
minutes(){
var minutes = this.$store.getters.minutes;
return this.formTime(minutes)
},
seconds(){
var seconds = this.$store.getters.seconds;
return this.formTime(seconds);
},
timer: {
get(){
return this.$store.state.timer
}
}
}
My code in Timer.vue script computed and methods. I cannot track where the problem is... Please help me Im stuck with this here.
Mutations do not have access to any context. They are meant to be atomic, that is they work directly with one facet of state. You should make your startTimer an action that commits the timer and then starts the countdown
mutations: {
// add this one for setting the timer
setTimer (state, timer) {
state.timer = timer
}
},
actions: {
startTimer ({ commit }) {
commit('stopTimer') // just a guess but you might need this
commit('setTimer', setInterval(() => {
commit('countDown')
}, 1000))
}
}
This would need to be called via dispatch instead of commit
this.$store.dispatch('startTimer')
I have 2 Vuejs components that are connected to each other. The first component is updating data in the second one. What is the best approach for integration testing? I am using vuejs 2.
VoucherComponent:
import Store from '../store';
import Ajax from '../_helpers/ajax';
const Voucher = {
name: 'voucher',
props: ['id'],
template: '',
data () {
return {
voucherCode: null,
priceDetails: Store.priceDetails,
vouchers: Store.vouchers
}
},
beforeCreate() {
Store.vouchers = []
},
methods: {
validateVoucher() {
let totalPrice = Store.total;
let vouchers = this.vouchers;
let voucherCode = this.voucherCode;
let id = this.id;
let priceDetails = this.priceDetails;
let voucher = new Ajax('/vouchers/redeem/' + voucherCode + '/' + id + '/' + totalPrice + '/', 'GET');
if (!this.checkVoucherPriceDetails()) {
voucher.ajaxCall(function (response) {
if (!response.error) {
vouchers.push({
code: voucherCode,
value: parseFloat(response.data.discount).toFixed(2),
type: 'voucher',
name: 'voucher',
description: 'Your voucher code: ' + voucherCode
});
priceDetails.push({
code: voucherCode,
price: parseFloat((-1.00 * response.data.discount)).toFixed(2),
description: 'Your voucher code: ' + voucherCode,
type: 'voucher'
});
} else {
return false;
}
});
}
},
removeVoucher(voucher) {
this.voucherCode = voucher.code;
this.clearVouchersFromPriceDetails();
var stringifyVoucher = JSON.stringify(voucher);
for (var i = 0, len = this.vouchers.length; i < len; i++) {
if (stringifyVoucher === JSON.stringify(this.vouchers[i])) {
this.vouchers.splice(i, 1);
break;
}
}
// return true;
},
clearVouchersFromPriceDetails() {
for (var i = this.priceDetails.length - 1; i >= 0; i--) {
if (this.priceDetails[i].code === this.voucherCode) {
this.priceDetails.splice(i, 1);
}
}
},
checkVoucherPriceDetails() {
for (var i = this.priceDetails.length - 1; i >= 0; i--) {
if (this.priceDetails[i].code === this.voucherCode) {
return true;
}
}
return false;
}
},
mounted () {
Store.debug && console.log("Init voucher component");
}
};
export default Voucher;
PriceDetailsComponent:
import Store from '../store';
const PriceDetails = {
name: 'price-details',
props: ['price','fee'],
data() {
return {
priceDetails: Store.priceDetails,
store: Store
}
},
created() {
this.priceDetails.push({
price: this.price.toFixed(2),
description: "Buchung Preis",
type: 'booking'
});
this.priceDetails.push({
price: this.fee.toFixed(2),
description: "Buchungsgebühren",
type: 'booking_fee'
});
},
computed: {
totalPrice() {
let total = 0.00;
let insurancePrice = 0.00;
for (var detailKey in this.priceDetails) {
var detail = this.priceDetails[detailKey];
total += parseFloat(detail.price);
if (detail.type == 'insurance') {
insurancePrice = detail.price;
}
this.store.total = total;
}
return parseFloat(total).toFixed(2);
}
},
mounted() {
Store.debug && console.log("Init price-details");
}
};
export default PriceDetails;
Store:
const Store = {
debug: true,
priceDetails: [],
total: 0.00
};
export default Store;
Thanks a lot!