Creating a countdown in v-for for each result - vue.js

I have a loop, in which I get several result from my API call.
Each of them has an property called processing_time that shows how many seconds it has left until something happens.
How can I implement the countdown for each interval?
Here is my <progressbar> component which needs a value for initially displaying the progress (I need it to be dynamic)
<progress-bar :options="options"
:value="data.properties.processing_time"
/>
<span class="progress-bar-seconds"
>{{ data.properties.processing_time }} Seconds left
...</span>
I feel like I need to use computed but I don't exactly know how to do it.

A computed prop is probably not the best solution, since you need to change the timer value locally. I recommend using a local copy that is updated periodically:
Create a local data property (a ref).
Copy the value prop to the local data prop upon mounting.
Use setInterval to periodically decrement the value.
<template>
<div>
<span class="progress-bar-seconds">{{ seconds }} Seconds left ...</span>
</div>
</template>
<script>
import { ref, onMounted, onUnmounted } from 'vue'
export default {
props: {
value: {
type: Number,
default: 0,
required: true,
},
},
setup(props) {
const seconds = ref(0) // 1️⃣
let _timerId = null
onMounted(() => {
seconds.value = props.value // 2️⃣
if (seconds.value > 0) {
// 3️⃣
_timerId = setInterval(() => {
seconds.value--
if (seconds.value <= 0) {
clearInterval(_timerId)
}
}, 1000)
}
})
onUnmounted(() => clearInterval(_timerId))
return { seconds }
}
}
</script>
demo

Related

Vue child component not rerending after parent component's data value updates

I am trying to have a child component update its props that were passed from the parents at the start of the rendering. Since the value is coming from a fetch call, it takes a bit of time to get the value, so I understand that the child component will receive a 'null' variable. But once the fetch call is completed, the value is updated but the child component still has the original null value.
During my search for a solution, I found that another way was to use Vuex Stores, so I implemented it with the count variable and had a button to call a commit and later dispatch with an action function to the store to increment it's value but when the increment happens, it doesn't show the new value on the screen even though with console logs I confirmed it did change the value when the function was called.
I guess I don't fully understand how to update the value of a variable without reassigning it within it's own component or having to call a separate function manually right after I change the value of a data variable.
App.vue
<template>
<div id="app">
<div id="banner">
<div>Title</div>
</div>
<p>count: {{count}}</p> // a small test i was doing to figure out how to update data values
<button #click="update">Click </button>
<div id="content" class="container">
<CustomDropdown title="Title Test" :valueProps="values" /> // passing the data into child component
</div>
</div>
</template>
<script>
import CustomDropdown from './components/CustomDropdown.vue'
export default {
name: 'App',
components: {
CustomDropdown,
},
data() {
return {
values: null
count: this.$store.state.count
}
},
methods: {
update() {
this.$store.dispatch('increment')
}
},
async created() {
const response = await fetch("http://localhost:3000/getIds", {
method: 'GET',
headers: {
'Accept': 'application/json, text/plain, */*',
'Content-Type': 'application/json'
}
});
const data = await response.json();
this.values = data // This is when I expect the child component to rerender and show the new data. data is an array of objects
console.log("data", data, this.values) // the console log shows both variables have data
}
}
</script>
CustomDropDown.vue
<template>
<div id="dropdown-container" class="">
<b-dropdown class="outline danger" variant="outline-dark" :text="title" :disabled="disabled">
<b-dropdown-item
v-for="value in values"
:key="value.DIV_ID"
href="#">
{{value.name}}
</b-dropdown-item>
</b-dropdown>
</div>
</template>
<script>
export default {
name: 'CustomDropdown',
components: {},
props: {
title: String,
valuesProp: Array,
disabled: Boolean
},
data() {
return {
values: this.valuesProp
}
},
methods: {
},
created() {
console.log("dropdown created")
console.log(this.valuesProp) //Always undefined
}
}
</script>
store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state() {
return {
count: 0,
divisionIds: []
}
},
mutations: {
increment (state) {
console.log("count", state.count)
state.count++
}
},
actions: {
increment (state) {
console.log("count action", state.count)
state.commit('increment')
}
}
})
data in your child component CustomDropdown.vue is not reactive: therefore the value of this.values is not updated when the prop changes. If you want to alias a prop, use computed instead:
export default {
name: 'CustomDropdown',
components: {},
props: {
title: String,
valuesProp: Array,
disabled: Boolean
},
computed: {
values() {
return this.valuesProp;
}
},
created() {
console.log("dropdown created");
}
}
If you want to console log the most updated values of this.valuesProp, you will need to watch it: the same if you want for this.values.
One thing you can do is to use a v-if in your child component to only render it after you get your result from you api.
It would be something like:
<CustomDropdown title="Title Test" :valueProps="values" v-if="values"/>
This way you would make sure that your child component gets rendered only when values are available.
It would only be a bad solution if this api call took so long and you needed to display the child component data to the user before that.
Hey you can simply watch it your child component
watch: { valuesProp: function(newVal, oldVal) { // watch it if(newVal.length > 0) do something }
it will watch for the value changes and when you get your desired value you can perform whatever hope it will help you you dont need store or conditional binding for it.

Vue: Updating a data property triggers re-evaluation of entire data?

I've been working on Vue project for almost a year, and I've just observed unexpected behavior below for the first time today...
Here is a link to code sandbox:
https://codesandbox.io/s/2bnem?file=/src/App.vue
And a code snippet from above link:
<template>
<div>
<div>{{a}}</div>
<div>{{translator(b)}}</div>
<input v-model="a" />
</div>
</template>
<script>
export default {
data() {
return {
a: 'a',
b: 'b',
}
},
computed: {
translator: function() {
return function(value) {
console.log(`translated...: ${value}`)
return value
}
}
}
}
</script>
Now every time I hit the key on input, the translator is triggered.
Is this a correct behavior?
If so, what is the cause of this problem or a background reasoning of this behavior?
Note that my vue version is 2.6.14(latest).
Your original issue is that you were attempting to use a method to render parts of your template. Methods used like this will execute for every update cycle, regardless of what changed.
The better solution is to use a computed property. Here's a somewhat dynamic example that wraps each of your data properties with a computed translator_x property
<template>
<div>
<div>{{ a }}</div>
<div>{{ translator_b }}</div>
<input v-model="a" />
</div>
</template>
<script>
const defaultData = {
a: "a",
b: "b"
}
export default {
data: () => ({ ...defaultData }),
computed: Object.fromEntries(Object.keys(defaultData).map(k => [
`translator_${k}`,
vm => {
console.log("translated...", k)
return vm[k]
}
]))
};
</script>
Each translator_x property will only be evaluated if the underlying data property is changed.

Pass Vue js search filter functionality through single file components with EventBus

I have the following components:
/components/SearchBlogs.vue Search component to filter on blog.title and blog.description.
/components/BlogList.vue Here I list all the Blog items.
SearchBlogs.vue
<template>
<div>
<input type="text" v-model="search" #change="emitSearchValue" placeholder="search blog">
</div>
</template>
<script>
import { EventBus } from '../event-bus.js'
export default {
name: 'SearchBlogs',
data: () => {
return {
search: ''
}
},
methods: {
emitSearchValue() {
EventBus.$emit('search-value', 'this.search')
}
}
}
</script>
BlogList.vue
<template>
<div>
<div v-for="blog in filteredBlogs" :key="blog">
<BlogListItem :blog="blog" />
</div>
</div>
</template>
<script>
import BlogListItem from './BlogListItem'
import { EventBus } from '../event-bus.js'
export default {
name: 'BlogList',
components: {
BlogListItem,
},
data: () => {
return {
blogs: [],
searchvalue: ''
}
},
computed: {
filteredBlogs() {
return this.blogs.filter(blog =>
blog.name.toLowerCase().includes(
this.searchvalue.toLowerCase()
)
)
}
},
created() {
fetch('http://localhost:3000/blogs')
.then(response => {
return response.json();
})
.then(data => {
this.blogs = data;
}),
EventBus.$on('search-value', (search) => {
this.searchvalue = value;
})
}
}
</script>
In another page component Blogs I register both components:
<template>
<div>
<h1>Blog</h1>
<TheSidebar>
<SearchBlogs />
</TheSidebar>
<BlogList/>
</div>
</template>
Can anybody see what's missing here? I want, as soon as the user types something in the search input (from the SearchBlogs.vue component), it start filtering and updating the list.
Look at my solution condesandbox
Here is an explanation:
You don't need to use EventBus. You can communicate with Search Component by v-model, using prop value and emiting updated value from the Input.
Then your Main (List) Component is responsible for all the logic.
It keeps the state of a Search
It keeps the items and filtered Items
Thanks to that your Search Component is very clear and has no data, that means it has very little responsibility.
Please ask questions if I can add something to help you understand 😉
UPDATE:
EventBus is a great addition in some cases. Your case is simple enough, there is no need to add it. Right now your architecture is "over engineered".
When you have added listener on EventBus, on created:hookyou should always remove it while Component is being destroyed. Otherwise you can encounter a trouble with double calling function etc. This is very hard to debug, tryst me I'he been there 😉
Going with my suggestion gives you comfort of "no-need-to-remember-about-this" because Vue is doing it for you.
Hope that help.
Couple of issues but essentially the computed prop filteredData will look like:
computed: {
filteredData() {
return this.experiences.filter(
el => el.category.indexOf(this.search) > -1
);
}
}
Also, used quotes around 'this.search' when passing its value back which made it a string.
Fixed sandbox
https://codesandbox.io/s/reverent-lamarr-is8jz

Issue with method not returning values in interpolation nuxjs/vue

In my nuxtjs app static folder I have a file called data.json
in my component I use this data like so
<script>
import data from '~/static/data.json';
export default {
data ({ params }) {
return {
data
}
}
}
</script>
now I have a method that will basically take values from that data and create a little counting up animation like so
methods: {
countUp(value) {
for (let i = 0; i <= value; i++) {
setTimeout(() => {
return i;
}, 100);
}
}
}
and in my template I am calling it like so
<template>
<div>
<p>{{countUp(data.number)}}</p>
</div>
</template>
now the expected result is for the number to quickly change from 0 to the value but nothing is being printed on the dom if I inspect the html element its empty??
What am I doing wrong??
setTimeout doesn't work the way you think it does:
You can't return a value from inside the callback function; nothing is being returned from the countUp method.
The call to setTimeout doesn't block, meaning it will return immediately after being called and the callback function passed to it is scheduled for execution asynchronously after the timeout has passed. So every setTimeout call in the for loop will be executed all at once after 100 ms instead of staggered.
You will need to store the current counter value as data on the component so Vue knows to rerender when its value is changed.
The simplest example I can provide follows, but you might want to encapsulate the logic in a separate reusable component.
const value = 50
new Vue({
el: '#app',
data: {
counter: 0,
},
methods: {
countUp() {
const interval = setInterval(() => {
this.counter++
if (this.counter >= value) {
clearInterval(interval)
}
}, 100)
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<button #click="countUp">Count Up</button>
{{ counter }}
</div>

VueJS 2: Child does not update on parent change (props)

When I update the parent's singleIssue variable, it does not get updated inside my <issue> component. I am passing it there using props. I have achieved this in other projects already, but I can't seem to get what I am doing wrong.
I have reduced my code to the relevant parts, so it is easier to understand.
IssueIndex.vue:
<template>
<div class="issue-overview">
<issue v-if="singleIssue" :issue="singleIssue"></issue>
<v-server-table url="api/v1/issues" :columns="columns" :options="options" ref="issuesTable">
<span slot="name" slot-scope="props">{{props.row.name}}</span>
<div slot="options" slot-scope="props" class="btn-group" role="group" aria-label="Order Controls">
<b-btn class="btn-success" v-b-modal.issueModal v-
on:click="showIssue(props.row)">Show</b-btn>
</div>
</v-server-table>
</div>
</template>
<script>
export default {
mounted() {
let app = this;
axios.get('api/v1/issues/')
.then(response => {
app.issues = response.data;
})
.catch(e => {
app.errors.push(e);
});
},
data: () => {
return {
issues: [],
singleIssue: undefined,
columns: ['name', 'creation_date', 'options'],
options: {
filterByColumn: true,
filterable: ['name', 'creation_date'],
sortable: ['name', 'creation_date'],
dateColumns: ['creation_date'],
toMomentFormat: 'YYYY-MM-DD HH:mm:ss',
orderBy: {
column: 'name',
ascending: true
},
initFilters: {
active: true,
}
}
}
},
methods: {
showIssue(issue) {
let app = this;
app.singleIssue = issue;
// This gets the action history of the card
axios.get('api/v1/issues/getCardAction/' + issue.id)
.then(response => {
app.singleIssue.actions = response.data;
})
.catch(error => {
// alert
});
}
}
}
</script>
Issue.vue:
<template>
<div>
{{ issue }}
</div>
</template>
<script>
export default {
props: ['issue']
}
</script>
So after showIssue() is triggered, it will get actions for the issue. But after then, I can't see the actions in the issue component.
If I update the issue-model in the issue component using form inputs, it will also start showing the actions. So I assume it's just in a weird state where it needs a refresh.
Thanks in advance!
If the singleIssue.actions property does not exist at the time when you're setting it, Vue will not be able to detect it. You need to use $set, or just define the property before you assign singleIssue to app.
Change this:
app.singleIssue = issue;
to this:
issue.actions = undefined;
app.singleIssue = issue;
The app.singleIssue property is reactive (because it was declared in the data section), so Vue will detect when this property is assigned to and make the new value reactive if it isn't already. At the time when issue is being assigned, it will be made reactive without the actions property, and Vue cannot detect when new properties are being added to reactive objects later on (hence why $set is required for those situations).