Waiting time counter for each object in array Vue js - vue.js

To get in context, I have a table that shows incoming calls and the waiting time for every call. The data array looks like:
[
{
id: 1,
patient_name: lorem ipsum,
created_at: 2022-02-02 09:10:35,
...
},
{
id: 2,
patient_name: dolor ipsum,
created_at: 2022-02-02 09:00:35,
...
}
]
I'm trying to figure out how to assign a setTimeout for each object, but I'm completely lost.
So far, I found that a counter can be made through a watcher, but of course this only acts as a "global" counter.
watch: {
timerCount: {
handler (value) {
if (value > 0) {
setTimeout(() => {
this.timerCount++
}, 1000)
}
},
immediate: true // This ensures the watcher is triggered upon creation
}
},
Is there a way to use a function to show a counter on each object? I was thinking in something like this:
<template>
<span v-for="call in calls" :key="call.id">
Requested at: {{ call.created_at }}
waiting time: {{ showWaitingTime(call.created_at) }} // <- Not sure if this can be done
</span>
</template>
...
<script>
....
methods: {
showWaitingTime (created_at) {
// get diff in seconds between created_at and now
// do something here with a timeOut() to increment the object property...
}
}
</script>
In addition, I would like to return the waiting time in HH:mm:ss format.

One solution is to wrap the {{ showWaitingTime(call.created_at) }} with a <span> that is keyed on timerCount, so that the <span> is re-rendered when timerCount changes (thus calling showWaitingTime again to compute the new time string):
In the template, wrap the timestamp string with a <span> that has its key bound to timerCount:
waiting time: <span :key="timerCount">{{ showWaitingTime(call.created_at) }}</span>
In a watcher on calls, use setInterval to start a periodic timer. Be sure to stop the timer with clearInterval before starting a new timer and when unmounting the component.
export default {
beforeUnmount() {
clearInterval(this._updateTimer)
},
// Vue 2 equivalent of beforeUnmount()
beforeDestroy() {
clearInterval(this._updateTimer)
},
watch: {
calls: {
handler(calls) {
clearInterval(this._updateTimer)
if (calls.length) {
this._updateTimer = setInterval(() => this.timerCount++, 1000)
}
},
immediate: true,
},
},
}
The watcher you have on timerCount is effectively implementing setInterval. Remove that code since it's obviated by the code in step 2.
export default {
watch: {
// timerCount: {⋯} // ⛔️ remove this watch
}
}
In showWaitingTime(), calculate the HH:mm:ss from the difference between the given time and now:
export default {
methods: {
showWaitingTime(created_at) {
const diff = new Date() - new Date(created_at)
const twoD = x => `${Math.floor(x)}`.padStart(2, '0')
const HH = twoD((diff / (60 * 60 * 1000)) % 24)
const mm = twoD((diff / (60 * 1000)) % 60)
const ss = twoD((diff / 1000) % 60)
return `${HH}:${mm}:${ss}`
},
},
}
demo

Related

Moment Duration - expected Number got Object

My backend returns timespan in the following format:
"00:34:49.4073541"
I have read and I can use moment.duration to return that data.
I want to display minutes and seconds and disable buttons based on this timeout.
<template>
<countdown :time="this.time">
<p>Time Remaining: {{ this.time.minutes }} minutes, {{ this.time.seconds }} seconds.</p>
</countdown>
</template>
import moment from 'moment'
export default {
data () {
return {
otp: '',
initialOtpSent: false,
time: 0,
}
},
methods: {
createOtp (bvModalEvt) {
if (res.data.resetOtpRetriesTimestamp === undefined) {
this.initialOtpSent = true
this.$bvToast.toast('The otp has been sent!', {
title: 'SUCCESS!',
variant: 'success',
solid: true,
static: true,
noAutoHide: false
})
// check on retries timestamp and disable sendotp and resend otp
} else {
const timeDuration = moment.duration(res.data.resetOtpRetriesTimestamp)
this.time = timeDuration
The error is:
Invalid prop: type check failed for prop "time". Expected Number, got
Object
Refer to the example in the usage docs for the component you referenced, which shows the :time attribute as a number, in terms of milliseconds.
You can change your Moment code to this.time = timeDuration.asMilliseconds();
Then change the template to match the format shown in the docs. For example:
<countdown :time="this.time">
<template slot-scope="props">Time Remaining:{{ props.minutes }} minutes, {{ props.seconds }} seconds.</template>
</countdown>

Unexpected side effect in "isExpiryComing" computed property

I tried to make the nearExpiry attribute to become TRUE if it is within the range of 30 days. But I hit an error which is Unexpected side effect in "isExpiryComing" computed property, is there any way I can overcome this?
I'm not sure how do I use slice at the isExpiryComing computed properties. Is there any workaround for this error?
<template>
<div class="container wrapper d-flex flex-column justify-content-center align-items-center">
<h1 class="text-info">Ingredients List</h1>
<ingredients-list class="justify-content-center"
v-for="(ingredient,index) in sortedItems"
:key="index"
:index='index'
:food-name="ingredient.food"
:food-expiry="ingredient.expiryDate"
:is-expiry="isExpiryComing.nearExpiry"></ingredients-list>
</div>
</template>
<script>
export default {
data() {
return {
ingredients: [
{
food: 'CARROT',
expiryDate: '2020-12-12',
nearExpiry: false
},
{
food: 'PAPAYA',
expiryDate: '2018-1-15',
nearExpiry: false
},
{
food: 'ORANGE',
expiryDate: '2021-10-13',
nearExpiry: false
},
{
food: 'CHICKEN',
expiryDate: '2019-4-23',
nearExpiry: false
},
{
food: 'MEAT',
expiryDate: '2021-5-23',
nearExpiry: false
},
],
}
},
computed: {
sortedItems() {
return this.ingredients.slice().sort((a, b) => {
return new Date(a.expiryDate) - new Date(b.expiryDate);
});
},
isExpiryComing() {
const now = new Date().getTime()
const expiryDate = new Date(this.expiryDate).getTime()
if (now - expiryDate > (30 * 24 * 60 * 60 * 1000)) {
this.nearExpiry = false
} else {
this.nearExpiry = true
}
return this.nearExpiry
}
},
}
</script>
It's because you're chagning another variable inside a computed property, this.nearExpiry - your use case doesn't need it.
I assume you want to check the expiration date for every product inside v-for.
I would suggest removing isExpiryComing entirely and changing the sortedItems to:
return this.ingredients
.slice()
.sort((a, b) => {
return new Date(a.expiryDate) - new Date(b.expiryDate)
})
.map((ingredient) => {
const now = new Date().getTime()
const expiryDate = new Date(ingredient.expiryDate).getTime()
return { ...ingredient, nearExpiry: now - expiryDate > 30 * 24 * 60 * 60 * 1000 }
})
Might be also a good idea to move const now = new Date().getTime() higher, to data().
A couple of issues I see with this:
isExpiryComing is scoped to your current component (like ingredients and sortedItems), not to the iteration of your v-for (like ingredient and index).
Setting to this.nearExpiry = true is likely what is causing the error. Computed properties should be pure-functions, i.e. they should not affect the state (normally this wouldn't be the case, as this should be scoped to the function with the method signature you used, but this is Vue so good luck with upholding that). If you want to affect state, you should use a watched property. (and since you are just returning this.nearExpiry, why not just make nearExpiry a local variable?).
To fix the error, just make nearExpiry a local variable instead of a vue-instance variable (this.nearExpiry). But, per my first point, that's probably not what you want either. Probably move the computed property to your ingredients-list component.

realtime clock with vue js

I've written a small clock component for one of my projects,
but I didn't get the value for my clock refreshed.
A short extract of my code:
time() {
let now = new Date();
let hour = this.zeroPadding(now.getHours());
let minute = this.zeroPadding(now.getMinutes());
let second = this.zeroPadding(now.getSeconds());
console.log(hour.toString() + minute.toString() + second.toString())
if(!this.realtime)
return this.value
else
return hour.toString() + ":" + minute.toString() + ":" + second.toString()
}
},
mounted() {
setInterval(() => {
this.time()
}, 1000)
},
beforeDestroy () {
clearInterval(this.polling)
}
Does anyone finde the mistake?
Did I understand the polling wrong?
Greetings,
Matthias
The time value that you want to display needs to be a data / computed property so that it's reactive and Vue can track it. Concise way to do it:
export default {
data() {
return {
interval: null,
time: null
}
},
beforeDestroy() {
// prevent memory leak
clearInterval(this.interval)
},
created() {
// update the time every second
this.interval = setInterval(() => {
// Concise way to format time according to system locale.
// In my case this returns "3:48:00 am"
this.time = Intl.DateTimeFormat(navigator.language, {
hour: 'numeric',
minute: 'numeric',
second: 'numeric'
}).format()
}, 1000)
}
}

How Do I update the Dom (Re-render element) when value changes?

So I have a variable startWeek, that is being displayed and when I click a button, a Have a method that will add 7 days to the date. I want the page to show the new date but not sure how to go about doing this?
I want to get the dates to transition in and out everytime the date range is updated, but Im not sure how to do it. I have thought about using two v-if statements and transitioning back and fourth between them but im sure there is a better way. I have looked into watchers and computed properties but im not quite sure if they are the answer or how to implement them in this given situation.
Example:
<template>
<b-button #click="subtractWeek(7)></b-button>
<span id="thingIwantToUpdateInDOM">{{ startWeek + " - " + endWeek}}</span>
<b-button #click="addWeek(7)></b-button>
</template>
export default {
data(){
return{
startWeek: null,
endWeek: null,
}
},
methods: {
addWeek(days){
this.startWeek.setDate(this.startWeek.getDate() + days)
this.endWeek.setDate(this.endWeek.getDate() + days)
},
substractWeek(7){
this.startWeek.setDate(this.startWeek.getDate() - days)
this.endWeek.setDate(this.endWeek.getDate() - days)
},
getInitialDate(){
this.startWeek = new Date();
var tempEndWeek = new Date();
this.endWeek = tempEndWeek;
},
created() {
this.getInitialDate();
}
}
}
My ultimate goal is to have the date range to swipe our transition out similar to a carousel effect on every button click or value change. Any bit of advise is greatly appreciated!
Your end goal is lots of optimizations away, but the snippet below should get you started. Your date operations are fine, so a few notes about the implementation:
I don't exactly know the internals of how Vue does state change tracking in the data objects, but I'm fairly sure it involves getter and setter property accessors. When you do this.x = new Date(); this.x.setDate(this.x.getDate() + 7);, this.x tracks the date object and not its value, so the change will not be seen by Vue. You need to clone the date first, set a new date, and then reassign it to this.x (see the navigateWeeks method below).
watch is useful when you want to react to a single, specific property change, in your case, the dynamic startWeek is a perfect candidate. If the fact that something changed is more important than what exactly, use the updated hook (typical use-case: destroying & re-initializing 3rd party library widgets with new parameters).
computed is useful for keeping a property derived from another property in sync at all times, in your example the endDate is always 7 days after the startDate, so it is a perfect candidate for this. In the snippet I also used a computed value for the ISO date format that HTML date inputs expect.
Finally, you can do quite advanced stuff with setTimeout, some CSS keyframes, and toggling a .transitioning class
Vue.component('fading-date', {
template: `
<span><input :class="className" type="date" :value="htmlValue"></span>
`,
props: {
value: { type: Date },
fadeDuration: { type: Number, default: 1 }
},
data() {
return { transitioning: false, timer: null };
},
computed: {
htmlValue() {
return this.value.toISOString().split('T')[0];
},
className() {
return this.transitioning ? 'transitioning' : '';
}
},
watch: {
value() {
clearTimeout(this.timer);
this.transitioning = true;
this.timer = setTimeout(() => {
this.transitioning = false;
}, 1000 * this.fadeDuration);
}
}
});
new Vue({
el: '#app',
data: {
selectedWeek: new Date()
},
computed: {
weekAfterSelected() {
const date = this.selectedWeek;
const endDate = new Date(date);
endDate.setDate(date.getDate() + 7);
return endDate;
}
},
methods: {
navigateWeeks(numWeeks = 1) {
const newDate = new Date(this.selectedWeek);
newDate.setDate(newDate.getDate() + (7 * numWeeks));
this.selectedWeek = newDate;
}
}
});
input[type="date"] {
background: transparent;
border: none;
}
#keyframes fade{
0% {
opacity:1;
}
50% {
opacity:0;
}
100% {
opacity:1;
}
}
.transitioning {
animation: fade ease-out 1s;
animation-iteration-count: infinite;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<fading-date :value="selectedWeek" :fade-duration="1"></fading-date>
<fading-date :value="weekAfterSelected" :fade-duration="1"></fading-date>
<br>
<button type="button" #click="navigateWeeks(-1)">A week sooner</button>
<button type="button" #click="navigateWeeks(1)">A week later</button>
</div>

Vue v-for list not re-rendering after computed data update

I am implementing pagination for a huge list of cards, I display 10 cards at once and wish to show the 10 next (or 10 previous) by clicking on two buttons.
Here's how I do it:
export default {
...
data() {
return {
pois: [], // My list of elements
pageNumber: 0, // Current page number
};
},
props: {
size: {
type: Number,
required: false,
default: 10, // 10 cards per page
},
},
computed: {
pageCount() {
// Counts the number of pages total
const l = this.pois.length;
const s = this.size;
return Math.floor(l / s);
},
paginatedData() {
// Returns the right cards based on the current page
const start = this.pageNumber * this.size;
const end = start + this.size;
return this.pois.slice(start, end);
},
},
methods: {
nextPage() {
this.pageNumber += 1;
},
prevPage() {
this.pageNumber -= 1;
},
}
...
};
And my template:
<div v-for="poi in paginatedData" :key="poi.id">
<card :poi="poi"/>
</div>
Everything should work (and a page change does output the correct cards in the console) but my list is not updated even though the computed method is called on each click.
What is causing this issue? I've read it could be linked to a :key value missing, but it's there, and no data is being updated directly and manually in the array, only sliced out.
First, try this change, just for sure, and let me know in comment it works or not.
export default {
...
computed: {
paginatedData() {
...
const end = start + this.size - 1;
...
},
...
};
And yes: instead of id, try to use index:
<div v-for="(poi, idx) in paginatedData" :key="idx">
<card :poi="poi"/>
</div>