I am newbie in VueJs.(vue 2). I have a problem here. I have a table where I am dynamically populating data like this.
<tbody>
<tr v-bind:key="queProduct.value" v-for="queProduct in queueProducts">
<td class="has-text-centered">
<figure class="image is-48x48">
<img :src="queProduct.image" alt="Placeholder image">
</figure>
</td>
<td><span>{{queProduct.title}}</span></td>
<td class="has-text-centered"><a class="has-text-link">
<span class="icon is-size-4" #click="openModalPopup(queProduct.id)">
<i class="fa fa-edit" />
</span>
</a>
</td>
<td class="has-text-centered"><a class="has-text-link">
<span class="icon is-size-4" #click="openModalPopup(queProduct.id)">
<img :src="queProduct.indicatorImg" />
</span>
</a>
</td>
<td class="has-text-centered"><a class="delete is-large has-background-link" #click="removeFromQueue(queProduct.id)"></a></td>
</tr>
</tbody>
methods:{
loadQueue(){
const indicators = store.get('productIndicators');
if(indicators === undefined){
store.set('productIndicators', []);
} else {
this.savedProprogressIndicators = indicators;
}
this.queueProducts.forEach(product => {
product.indicatorImg = indicatorImgBaseUrl +'Level-0.png';
this.savedProprogressIndicators.forEach(indicator => {
if(indicator.id === product.id){
product.indicatorImg = indicatorImgBaseUrl +indicator.image;
}
})
})
}
}
When I console.log the product, I see the product object being updated with the new value. But the dom isnt getting updated. Like,
this.product looks like this.
{
id: "d6dd8228-e0a6-4cb7-ab83-50ca5a937d45"
image: "https://zuomod.ca/image/cache/catalog/products/2018/Single/medium/50105-1-800x800.jpg"
inQueue: false
indicatorImg: "https://cdn.shopify.com/s/files/1/0003/9252/7936/files/Level-2.png"
saved: false
sku: "50105"
title: "Interstellar Ceiling Lamp"
}
But in the DOM, it looks like this
{
id: "d6dd8228-e0a6-4cb7-ab83-50ca5a937d45"
image: "https://zuomod.ca/image/cache/catalog/products/2018/Single/medium/50105-1-800x800.jpg"
inQueue: false
indicatorImg: "https://cdn.shopify.com/s/files/1/0003/9252/7936/files/Level-0.png"
saved: false
sku: "50105"
title: "Interstellar Ceiling Lamp"
}
Can you please help me resolve this?
Thanks,
Vandanaa
As you use Vuex, you should get your products directly from you store like in computed property in your vue definition. This will refresh the data directly from store without any action from vue side.
{
...
computed:{
...mapGetters({
queueProducts : 'queueProducts'
})
}
...
}
Furthermore, if your are using vuex, try to keep your logic inside your store. You vue should only display data.
Hava a look to vuex documentation to know when and where you should use
Getters, Mutations and Actions.
Hope this help.
this.queueProducts.forEach(product => {
...
...
...
}
this.$forceUpdate(); // Trying to add this code
I guessed your product.indicatorImg was not been watch by Vue, so it will not update the DOM. Trying to add this.$forceUpdate() in the end. It will force Vue to update DOM.
Related
I'm using vue to create a page where I list all users and if I click on the edit button the details of that user then gets shown
next to the list.
What I'm trying to do is, if I update a user and click save then the user details in the list needs to change.
The problem I'm having is that I'm not able to get the details to change in the list after I've saved.
My vue
<template>
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-md-7">
<table class="table table-striped table-sm mt-2">
<thead>
<tr>
<th>Name</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="user in displayAllUsers">
<td>{{ user.name }}</td>
<td>
<button class="btn btn-sm btn-success" #click="manageUser(user)">Edit</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="col-md-5" v-if="user != null">
<div class="card">
<div class="card-header">
<h4 class="card-title mb-0">Manage {{ user.name }}</h4>
</div>
<div class="card-body">
<table class="table">
<tr>
<th>Name</th>
<td>
<input type="text" v-model="user.name">
</td>
</tr>
</table>
</div>
<div class="card-footer">
<button #click="updateUser()"class="btn btn-success"><i class="fa fa-save"></i> Save</button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
components: {
},
data: function () {
return {
users: [],
user: null
}
},
computed: {
displayAllUsers(){
return this.users;
}
},
methods: {
manageUser(user){
axios.get('/admin/user/'+user.id).then((response) => {
this.user = response.data.user;
});
},
updateUser(){
axios.put('/admin/user/'+this.user.id, {
name: this.user.name
}).then((response) => {
this.users = response.data.user;
});
}
},
mounted() {
axios.get('/admin/users').then((response) => {
this.users = response.data.users;
});
}
}
</script>
There are two possible solutions.
The first is to run this code at the end of the updateUser method:
axios.get('/admin/users').then((response) => {
this.users = response.data.users;
});
The second is to use a state manager like Vuex.
The first scenario will fetch again your users data from the remote API and will update your view with all your users.
With the second scenario, you will handle your application state way much better than just using the data attribute of your page module, but in the background, it is more or less the same as the first solution I suggest.
To update the current user only in the table you could do something like that at the end of the updateUser method:
let userIdx = -1;
for(let idx = 0, l = this.users.length; idx < l; idx++) {
if ( this.user.id === this.users[idx].id ) {
userIdx = idx;
break;
}
}
if ( -1 !== userIdx ) {
this.users[userIdx] = this.user;
this.user = {};
}
Other than your problem, it seems like you don't need this code:
computed: {
displayAllUsers(){
return this.users;
}
},
You could remove this code, and instead use this code in the HTML part:
<tr v-for="user in users">
For your updateUser function you could just return the modified user in the same format that you have for all the users in you user list and update the user by index. This is presuming that the user you want to update is in the users array to start with.
updateUser() {
axios.put('/admin/user/'+this.user.id, {
name: this.user.name
}).then((response) => {
const updatedUser = response.data.user;
// Find the index of the updated user in the users list
const index = this.users.findIndex(user => user.id === updatedUser.id);
// If the user was found in the users list update it
if (index >= 0) {
// Use vue set to update the array by index and force an update on the page
this.$set(this.users, index, updatedUser);
}
});
}
This could be a good starting point.
Unrelated Note:
You can add your mounted function code to its own method, for example
getUsers() {
axios.get('/admin/users').then((response) => {
this.users = response.data.users;
});
}
then
mounted() {
this.getUsers()
}
this makes it a little cleaner and easier if you ever need to get the users again (example: if you start having filters the user can change)
As it could get more complex vuex would be a great addition.
this is my first question on stackoverflow.
So, I try to delete a item from array, I see, that in Vue Dev Tools it was deleted, but UI not updating.
I become this array as response from Laravel API and send dynamic to Vue Component like this
...
<admin-panel :jurisdictions="{{ $jurisdictions }}"></admin-panel>
...
then in my AdminComponent I redirect to AdminHomeComponent with props like this
<template>
<router-view :jurisdictions="jurisdictions"></router-view>
</template>
...
props: ['jurisdictions'],
...
created() {
this.$router.push({ name: "AdminHomeComponent" }).catch(err => {});
},
...
In AdminHomeComponent I have props too and router link to another component JurisdictionsComponent like this
<template>
...
<router-link :to="{name: 'JurisdictionsComponent'}"> Jurisdictions</router-link>
...
</template>
<script>
...
props: ["jurisdictions"]
...
</script>
And then will fun, wenn in JurisdictionsComponent I add a new one, or editing old one it works, there are reactive, but if I try to delete one, it still be reactive and I see this in Vue Dev Tools, but I cann't unterstand, why UI not updating..
JurisdictionsComponent
<template>
<div class="w-100">
<div id="jurisdictionsContainer" ref="jurisdictionsContainer">
<div class="panel-heading d-flex justify-content-between">
<h3 class="panel-title">Jurisdictions</h3>
<div class="pull-right">
<button #click.prevent="$modal.show('create-edit-jurisdiction', {'action' : 'create'})">
<i class="fas fa-plus-square"/> Create new
</button>
</div>
</div>
<table class="table table-hover mt-2 rounded" id="jurisdictions-table">
<thead class="thead-dark ">
<tr>
<th>Title</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="jurisdictions-table-body">
here I make v-for
<tr v-if="jurisdictions !== null" v-for="(jurisdiction, index) in this.jurisdictions" v-bind:key="jurisdiction.id"
class="result clickable-row"
#click="show($event, jurisdiction)">
<td class="title">
{{ jurisdiction.title }}
</td>
<td class="position-relative">
<button #click="$modal.show('create-edit-jurisdiction', {'jurisdiction': jurisdiction, 'index': index, 'action' : 'edit'})">
<div class="not-clickable">Edit</div>
here a show a delete modal window, use can deside delete or not, my code from ModalDeleteComponent see below
</button>
<button #click="$modal.show('delete-jurisdiction', {'jurisdiction': jurisdiction, 'index': index})">
<div class="not-clickable">Delete</div>
<i class="fas fa-trash-alt not-clickable"/>
</button>
</tr>
</tbody>
<delete-jurisdiction #onDeleted="onClickDelete"/>
<create-edit-jurisdiction #create="onClickCreate" #edit="onClickEdit":errors="this.errors.createEdit"/>
</div>
</div>
</template>
<script>
export default {
name: "JurisdictionsComponent",
props: ["jurisdictions"],
data() {
return {
isAllSelected: false,
errors: {
createEdit: {},
addEvent: {}
},
}
},
methods: {
/**
* Create a new jurisdiction
*
* #param data form
*/
onClickCreate(data) {
axios.post("/admin-dashboard/jurisdictions", data.form)
.then(response => {
response.data.image === undefined ? response.data.image = null : response.data.image;
response.data.selected = false;
this.jurisdictions.push(response.data);
this.$modal.hide("create-edit-jurisdiction");
this.errors.createEdit = {}
})
.catch(errors => {
this.errors.createEdit = errors.response.data.errors;
});
Here a try to delete jurisdiction, it deletes from database, from props in Vue Dev Tools but not from UI
/**
* Delete jurisdiction request
*
* #param index
*/
onClickDelete(index) {
axios.delete("/admin-dashboard/jurisdictions/" + this.jurisdictions[index].id)
.then(() => {
this.$delete(this.jurisdictions, index);
this.$modal.hide("delete-jurisdiction");
})
.catch(errors => {
console.log(errors)
});
},
/**
* Edit a jurisdiction
*
* #param data form
*/
onClickEdit(data) {
axios.patch(this.jurisdictions[data.index].path, data.form)
.then(response => {
this.$set(this.jurisdictions, data.index, response.data);
this.$modal.hide("create-edit-jurisdiction");
this.errors.createEdit = {}
})
.catch(errors => {
this.errors.createEdit = errors.response.data.errors;
})
},
}
</script>
ModalDeleteComponent
<template>
<modal name="delete-jurisdiction" #before-open="beforeOpen" height="200" #before-close="beforeClose">
<div class="h-100">
<div v-if="jurisdiction !== null" class="p-4 mt-2">
<h3>Do you want really delete
<a :href="'/admin-dashboard/jurisdictions/'+jurisdiction.id"><strong>{{ jurisdiction.title }}</strong></a>
<span v-if="jurisdiction.events.length > 0">
with {{ jurisdiction.events.length }} {{ jurisdiction.events.length === 1 ? 'event' : "events"}}
</span>?
</h3>
</div>
<div class="bg-dark d-flex justify-content-around p-2 position-absolute w-100" style="bottom: 0">
<button class="btn btn-danger" #click="submitDelete">Delete</button>
<button class="btn btn-secondary" #click="$modal.hide('delete-jurisdiction')">Cancel</button>
</div>
</div>
</modal>
</template>
<script>
export default {
name: "ModalDeleteJurisdictionComponent",
data() {
return {
jurisdiction: null,
index: ""
}
},
methods: {
submitDelete() {
this.$emit('onDeleted', this.index);
},
beforeOpen (event) {
this.jurisdiction = event.params.jurisdiction;
this.index = event.params.index;
},
beforeClose(event) {
this.jurisdiction = null;
this.index = "";
}
}
}
</script>
I know, my question is too long, but if anyone tries to answer this, I will very happy))
I'm open to any contra questions. Sorry for my English
So, thanks oshell for a tipp. Ich have renamed in jurisdictions to dataJurisdictions and init in created() {this.dataJurisdictions = this.jurisdictions} as well. First of all I want to avoid duplication of data in components and work only with props, but nevertheless it works. Thanks a lot!
You are adding to jurisdictions, which is a prop.
this.jurisdictions.push(response.data);
However, you should either update the prop in the parent component, to trigger a prop change and re-render or assign the prop to the components data as initial value and then update data.
Changing prop in parent component can be done using $emit or by using Vuex.
Assigning data locally just needs a different value name.
this.localJurisdictions = this.jurisdictions
And for updating then use this new data value. (Use accordingly in template.)
this.localJurisdictions.push(response.data);
I have two other uses of v-for in separate components. They also sometimes throw errors. All three v-for invocations are wrapped with v-if/else. Here is the code that produces duplicate key errors & renders data twice:
AccountDashboard.vue
<tbody>
<tr v-if="!residents.length" class="table-info">
<td class="text-center">
<p>
No residents on record.
</p>
</td>
</tr>
<template v-else>
<tr is="AccountResidentList"
v-for="resident in residents"
v-bind:key="'resident-list-' + resident.id"
v-bind:first_name="resident.first_name"
v-bind:last_name="resident.last_name"
v-bind:dob="resident.dob | date_formatted"
>
</tr>
</template>
</tbody>
Note the unique id attempt in the binding of key.
Here is a look at the child component
ProviderAccountList.vue
<template>
<tr class="AccountResidentList">
<td>
{{ this.$attrs.id }}
</td>
<td>
{{ this.$attrs.first_name }} {{ this.$attrs.last_name }}
</td>
<td>
{{ this.$attrs.dob }}
</td>
<td>
<button #click="toResidentProfile({account_id, id})" class="btn btn-sm btn-purple btn-with-icon">
<div class="ht-25">
<span class="icon wd-25"><i class="fa fa-eye"></i></span>
<span class="pd-x-10">view</span>
</div>
</button>
</td>
<!--TODO: Add view profile button-->
</tr>
</template>
<script>
import Axios from "axios";
import router from "../../router";
import { mapGetters } from "vuex";
import moment from "moment";
export default {
name: "AccountResidentList",
computed: {
...mapGetters['Resident', {
resident: 'getResident'
}]
},
filters: {
date_formatted: (date) => {
return moment(date).format('MMMM Do, YYYY');
}
},
methods: {
toResidentProfile(account_id, resident_id) {
router.push(`/accounts/${account_id}/residents/${resident_id}`)
}
},
};
</script>
<style scoped></style>
My Axios call looks like:
Account.js (a namespaced vuex-module)
async retrieveAccount(context, account_id) {
// Axios.defaults.headers.common['Authorization'] = 'Bearer ' + window.$cookies.get('jwt')
let response
let valid_id = window.$cookies.get('valid_id');
response = await Axios.get(`http://localhost:3000/api/v1/providers/${valid_id}/accounts/${account_id}`, { headers: { 'Authorization': 'Bearer ' + window.$cookies.get('jwt') } })
.then((response) => {
let account = response.data.locals.account;
let account_address = response.data.locals.account_address;
let residents = response.data.locals.residents;
// set Account
context.dispatch('Account/setId', account.id, {root: true});
context.dispatch('Account/setProviderId', account.provider_id, {root: true});
.
.
.
// set AccountAddress
// !Array.isArray(array) || !array.length
if (account.address) {
context.dispatch('Account/setAddressId', account_address.id, {root: true});
context.dispatch('Address/setId', account_address.id, {root: true});
.
.
.
// set AccountResidents
// !Array.isArray(array) || !array.length
residents.forEach(resident => {
if (resident) {
// Add object to parent's list
context.dispatch('Account/setResidents', resident, {root: true}); // Set attr values for object
context.dispatch('Resident/setId', resident.id, {root: true});
.
.
.
(remaining attrs removed for brevity)
}
})
router.push(`/providers/${account.provider_id}/accounts/${account_id}`);
})
.catch(function(error) {
console.log(error);
})
Note: the Account action #setResidents simply calls the mutator that adds one resident to a list total.
i.e state.list.push(resident)
I logged the response to the console and can confirm that the data isn't being sent twice (or more) from my Axios call.
I have reviewed & attempted the following to no avail:
https://alligator.io/vuejs/iterating-v-for/
https://www.reddit.com/r/vuejs/comments/7n3zi4/vue_warn_duplicate_keys_detected_vfor_with/
https://github.com/hejianxian/vddl/issues/23
https://github.com/hejianxian/vddl#warning
https://medium.com/#chiatsai/vue-js-common-issue-duplicate-keys-stops-components-rendering-df415f31838e
Finally, It should be mentioned that I have tried variations of using/not using template to wrap the list, including/not including the for loop in the template, etc..
Did not anticipate it would be this bothersome to iterate a collection.
Am I overlooking something obvious?
Update: What worked for me
I needed access to the resident.id also the id declared in the paren seems like an index. So here is a look at what removed the duplicate render errors and allow me access to the resident's id even after fixing the duplicate keys error:
<template v-else>
<tr is="AccountResidentList"
v-for="(resident, id) in residents"
v-bind:key="id"
v-bind:id="resident.id"
v-bind:first_name="resident.first_name"
v-bind:last_name="resident.last_name"
v-bind:dob="resident.dob | date_formatted"
>
</tr>
</template>
Thanks again #Billal Begueradj for the assist!
For me, I suspect that in residents there are entries which have the same id. So we have to find out a way to overcome this issue. We can give it an efficient try as follows:
<tr
is="AccountResidentList"
v-for="(resident, id) in residents"
:key="id"
// rest of your code
Say I have a group of cars and I want to display each row...3 seconds at a time. How can I do this in Vuejs2?
<tbody>
<tr v-for="(car) in cars">
<td><img v-bind:src="car.photo" width="40px" height="40px" alt=""></td>
<td><router-link :to="{path:'/car/' + car.id}" >{{ car.name }}</router-link></td>
<td>{{ car.make }}</td>
<td></td>
<td>{{ car.created }}</td>
</tr>
</tbody>
something like this.
stored what to show currently in currentCarIndex.
use setInterval to change currentCarIndex every 3 seconds
btw, v-for and v-if shouldn't be used together, so I add a <template> tag as an empty wrapper to execute v-for
<template>
<tbody>
<template v-for="(car,i) in cars">
<tr :key="i" v-if="i<=currentCarIndex">
<td><img v-bind:src="car.photo" width="40px" height="40px" alt=""></td>
<td>
<router-link :to="{path:'/car/' + car.id}">{{ car.name }}</router-link>
</td>
<td>{{ car.make }}</td>
<td></td>
<td>{{ car.created }}</td>
</tr>
</template>
</tbody>
</template>
<script>
export default {
data() {
return {
currentCarIndex: 0,
cars: "..."
};
},
mounted() {
const interval = setInterval(() => {
if (this.currentCarIndex + 1 < this.cars.length) this.currentCarIndex++;
else clearInterval(interval);
}, 3000);
}
};
</script>
I was having this exact problem a couple of hours ago on an app I'm working on. I have a list of reviews and I wanted the reviews to display at interval so that it looks like the list is 'filled in' top down so that I can create a cascading effect. Something like this:
The documentations points out that you can use transition-group but personally I wasn't able to get them working for me so what I did is I created a wrapper component with a delay property on it and I passed in the time the component should wait before rendering. I did this using a simple v-if in the component's template.
What you could do is add a show-in and visible-for prop to a wrapper component like this:
<flashing-row v-for="(car, i) in cars" :show-in="i * 3000" :visible-for="2900">
// Stuff inside my row here....
</flashing-row>
and then define flashing-row like this:
Vue.component('flashing-row', {
props: {
showIn: {
type: Number,
required: true,
},
visibleFor: {
type: Number,
required: true,
},
},
data() {
return {
isVisible: false,
};
},
created() {
setTimeout(() => {
// Make component visible
this.isVisible = true;
// Create timer to hide component after 'visibleFor' milliseconds
setTimeout(() => this.isVisible = false, this.visibleFor);
}, this.showIn);
},
template: '<tr v-if="isVisible"><slot></slot></tr>'
});
You can see an example of the code in JSFiddle. This approach is especially good because:
You don't repeat yourself if you're going to be doing this at more than one place.
Makes your code more maintainable and easier to browse, read, and thus understand and modify later on.
And of course you can play around with the props and expand on it depending on what you need. The possibilities are really endless.
I am having a problem updating my shown class when the data changes.
I have a servers array that calls to get the server status every 10 seconds. If the data changes, the data changes, but the class doesn't
The part that isn't changing is showing the font-awesome icon based on the status
'fas fa-exclamation-triangle critical' : 'fas fa-check ok'">
The text does change {{server.status}} just not the font-awesome class in the if statement.
Any ideas on what I need to change to get it to show correctly?
<tr v-for="server in servers">
<td>
{{server.name}}
<a v-bind:href="server.url" target="_blank">{{server.url}}</a>
</td>
<td style="min-width: 125px">
<i :class="server.status === 'CRITICAL' ? 'fas fa-exclamation-triangle critical' : 'fas fa-check ok'"></i>
{{server.status}}
</td>
<td>{{server.revision}}</td>
<td>{{server.notify}}</td>
<td>{{server.count}}</td>
</tr>
<script>
import axios from 'axios'
export default {
name: 'ServerMonitor',
data() {
return {
servers: []
}
},
created() {
this.fetchData();
},
mounted: function () {
setInterval(function () {
this.fetchData();
}.bind(this), 10000)
},
methods: {
fetchData() {
axios.get('https://SERVER/serverinfo')
.then((resp) => {
this.servers = resp.data[0].servers;
})
.catch((err) => {
console.log(err);
})
}
}
}
</script>
Also I have tried it without the :class like this:
<i v-if="server.status === 'CRITICAL'" class="fas fa-exclamation-triangle critical"></i>
<i v-if="server.status === 'OK'" class="fas fa-check ok"></i>
Vue's v-bind:class takes an object or an Array and not a string, which is probably your issue.
<td style="min-width: 125px">
<i :class="['fas', server.status === 'CRITICAL' ? 'fa-exclamation-triangle critical' : 'fa-check ok']"></i>
{{server.status}}
</td>
Updating my answer based on comments below:
You need to use the font-awesome Vue component. What's happening is that FontAwesome is converting the <i> icons to SVG once, and doesn't rerender them at any future point.
Edit 2
Alternatively you can use the v4 upgrade shim:
<script defer src="https://use.fontawesome.com/releases/v5.0.6/js/v4-shims.js"></script>
https://jsfiddle.net/6tfqp4nb/12/
If you are using font-awesome in js way, you can try this:
FontAwesomeConfig = { autoReplaceSvg: 'nest' }
doc: https://fontawesome.com/how-to-use/svg-with-js#auto-replace-svg-nest