I'm in trouble to display a specific element in an array with vuejs 2.
After this query I obtain an array with 20 elements :
this.$http.jsonp('https://api.instagram.com/v1/users/self/media/recent/?access_token=mytoken').then((response) => {
this.photos = response.body.data
}, (err) => {
console.log(err)
})
If I try to display the content of {{ photos }}, {{ photos[0] }} it works but if I try to display the content of a key like this {{ photos[0].id }} it does not work and it's the same for all the others keys.
The console return this error :
Error when rendering component
The component renders for the first time before the AJAX request has completed. Assuming you initialize photos = [] in the data function, there will be an empty array when the component is first rendered. At this point, photos[0] is undefined and photos[0].id causes a TypeError.
To get round this, use v-if to only render when the objects you are trying to access exist.
<!-- only render if photos contains at least 1 item -->
<div v-if="photos.length">
{{photos[0].id}}
</div>
Related
In my component I display many properties (only showing two below) of my selectedPlan store variable after getting its value from the store:
<div>
{{ selectedPlan.periodTitle }}
</div>
<div>
{{ selectedPlan.currency }}
</div>
...mapGetters(['selectedPlan', 'whatever'])
Problem: this variable is null by default and only gets populated after some unrelated component makes an API call.
The variable does get displayed so no problem on screen. But in console I get:
TypeError: Cannot read property 'periodTitle' of null
How to fix this problem?
Since selectedPlan may not be available(or has null value) on the first render, you are facing that error. You have one of three ways to solve this issue:
Add a loader and wait until selectedPlan is available (best way in my opinion from the UX perspective)
Add a null check before accessing values like:
{{ (selectedPlan || {}).periodTitle }}
Or much better, use a computed property:
computed: {
safeSelectedPlan: () => {
return this.selectedPlan || {}
}
}
and then use safeSelectedPlan in your template.
Use a library like lodash to get the values for variables safely.
Here is the documentation link to lodash: https://lodash.com/docs/4.17.15
Short question
The v-model which binds a string to an input field won't update in some cases.
Example
I am using Vue within a Laravel application. This is the main component which contains two other components:
<template>
<div>
<select-component
:items="items"
#selectedItem="updateSelectedItems"
/>
<basket-component
:selectedItems="selectedItems"
#clickedConfirm="confirm"
#clickedStopAll="stopAll"
/>
<form ref="chosenItemsForm" method="post">
<!-- Slot for CSRF token-->
<slot name="csrf-token"></slot>
<input type="text" name="chosenItems" v-model="selectedItemsPipedList" />
</form>
</div>
</template>
<script>
export default {
props: ["items"],
data: function() {
return {
selectedItems: [],
selectedItemsPipedList: ""
};
},
methods: {
updateSelectedItems: function(data) {
this.selectedItems = data;
this.selectedItemsPipedList = this.selectedItems
.map(item => item.id)
.join("|");
},
confirm() {
this.$refs.chosenItemsForm.submit();
},
stopAll() {
this.updateSelectedItems([]);
this.confirm();
}
}
};
</script>
The method updateSelectedItems is called from the select-component and it works fine. In the end, the selectedItemsPipedList contains the selected items from the select-component, which looks like "1|2|3" and this value is bound to the input field in the chosenItemsForm. When the method confirm is called from the basket-component, this form is posted to the Laravel backend and the post request contains the chosen items as piped list. So far, so good.
The method stopAll is called from the basket-component and it will remove all the selected items from the array. Therefore it will call the method updateSelectedItems with an empty array, which will clear the selectedItems array and then clear the selectedItemsPipedList. After that, confirm is called which will post the form again. But, the post value still contains the selected items (e.g. '1|2|3'), instead of "". It looks like the v-model in my form is not updated, which is strange because it does work when selecting items. Why is it working when adding items, and doesn't when removing all items?
I believe you have a timing issue here. The value of the properties haven't been propagated to the DOM yet, so the form submission is incorrect. Try this instead:
stopAll() {
this.updateSelectedItems([]);
//NextTick waits until after the next round of UI updates to execute the callback.
this.$nextTick(function() {this.confirm()});
}
After the user goes back from viewing the item (question) component, I am trying to keep the user's filtered list of questions based on two selects (category and sub category) and selected page from pagination in the questions list component.
I am new to Vue environment and not sure if I am following the best practice but here what I have tried to do:
1- I tried using the event bus, but I reached a point where I am able to get an object that contains the question category object to the main component and then it would be used as an attribute inside of the Onchange method which normally be triggered after the category select event happens. I was able to show the user the same list of filtered items, however, the problem with this approach that:
a- I can not update the select values to show the selected options which gave the questions list.
I have tried to get the selected element using the ref attributes, however the select element is undefined even though I put it in the mounte stage of the life cycle. It works with an ugly hack by using a setTimeout method and it shows the element after one second.
b- The filtered list of items has pagination and this approach does not show the user the same page that they picked the item from.
c- Calling the server again
2- I tried to store a global value in mixins file, however after saving the value in the mixins file and even though the object value is received in the mixins file but after updating the mixins data and then calling it from the questions list, it returns an empty object.
3- I tried using keep-alive
Code for approach 1:
The category object is eager loaded with the question using an event emit. After the user goes back from viewing the question I pass the category object with the event using beforeDestroy method:
beforeDestroy () {
EventBus.$emit('backToQuestions', this.question.category);
}
This is how the category object look like:
{…}=>
__ob__: Object { value: {…}, dep: {…}, vmCount: 0 }
created_at:
id:
parent_id:
title:
updated_at:
This is how I populate the filtered questions list
created () {
EventBus.$on('backToQuestions', category => {
this.onChange(category)
});
this.fetch(`/categories`);
}
My select:
<div class="col-md-4">
<select ref="main" class="form-control" v-model="mainSelected" #change="onChange(mainSelected)">
<option disabled value="">Questions Category (All)</option>
<option v-for="option in parentCategories" :value="option">{{ option.title }}</option>
</select>
</div>
<div v-if="subCategory" class="btn-group">
<select class="form-control" v-model="subSelected" #change="onChange(subSelected)">
<option disabled value="" selected >{{ categoryTitle }} Qs Categories</option>
<option v-for="option in childCategories" :value="option">{{ option.title }}</option>
</select>
</div>
The following is my onChange method just for reference:
onChange(option) {
this.categoryOption = option;
this.dataReady = false;
this.subCategory = true;
this.questions= {};
this.questionsArray =[];
this.categoryTitle = option.title;
this.categoryId = option.id;
if(option.children){
this.childCategories = option.children;
}
axios.get(`/categories/${this.categoryId}`)//getting the category questions
.then(({data}) => {
this.questions = data;
this.questionsArray.push(...data.data);
this.nextUrl = data.next_page_url;
this.dataReady = true;
this.emptyCheck(this.questionsArray);
})
.catch((err) => {
swal("Failed!", err.response.data.message, "info");
this.$router.push('dashboard') ;
})
}
$refs of the select divs always return undefined unless I used setTimeout.
Code for approach 2:
After including the mixins file in both components I put the following code in mixins:
setCategory (questionCategory) {
console.log("TCL: setCategory -> questionCategory", questionCategory)
this.category = questionCategory;
console.log("TCL: setCategory -> this.category", this.category)
}
,
getCategory () {
return this.category ;
}
The value of the object received by the set method is correct but after updating the mixins data method this.category is returning the following only:
__ob__: Object { value: {…}, dep: {…}, vmCount: 0 }
i.e. without the category object details. I tried to stringify the object and then call it, this.category shows an empty variable.
3- Using keep-alive, however it does not work, I tried to wrap both the router-view and router-link.
<keep-alive include="questions-list">
<router-link to="/questions-list" class="nav-link">
<i class="nav-icon fas fa-question-circle orange"></i>
<p>
Questions
</p>
</router-link>
</keep-alive>
I even tried using include the name of the questions list component with no result.
Sorry for the long question but I have been stuck for a while with this and it is the core of my application.
I am now using keep-alive approach. The silly mistake was that I only named the component when I declared my routes.
{ path: '/questions-list', name:"questions-list", component: require('./components/qa/Questions.vue')}
For those who are facing the same problem, you should declare the name of the component inside of the component export object like the following:
export default {
name: 'questions-list',
data () {
return {
}
}
I am working on a vuejs SPA.
I have a view that shows a list of items and another view that shows details for a specific Item.
when I click the item I switch views using:
this.$router.push('/item/' + event.ItemId );
The data is managed using vuex modules.
I would like to allow some temporary display while the item details are being retried (i.e. not to block the rendering of the item details view which should know on its own to indicate that it is still awaiting data).
And I would also have to consider that it should work if the URL is changed (I think I read that there is an issue with the view not being reloaded/recreated when only the item id would change in the URL.
Where would be the appropriate place (code/lifecycle) to trigger the (async) retrieval of the data required for rendering the item details view?
I would like to allow some temporary display while the item details are being retried (i.e. not to block the rendering of the item details view which should know on its own to indicate that it is still awaiting data).
One way to achieve this, is to define a state variable, named e.g. isLoading, in the data context of the Vue component. This variable would then be true while the data is retrieved asynchronously. In the template, you can use v-if to display a spinner while loading, and displaying the content after that.
If you are retrieving the data multiple times (refreshing the view), I would move the retrieving code into a method, e.g. called loadData. In the mounted section of the Vue component you then can just initially call this method once.
Here is some example code:
<template>
<div>
<button #click="loadData" :disabled="isLoading">Refresh</button>
<div class="item" v-if="!isLoading">
{{ item }}
</div>
<div class="spinner" v-else>
Loading...
</div>
</div>
</template>
<script>
import HttpService from '#/services/HttpService';
export default {
name: 'item-details',
data () {
return {
isLoading: false,
item: {}
};
},
methods: {
loadData () {
this.isLoading = true;
HttpService.loadData().then(response => {
this.item = response.data;
this.isLoading = false;
}, () => {
this.item = {};
this.isLoading = false;
});
}
},
mounted () {
this.loadData();
}
};
</script>
And I would also have to consider that it should work if the URL is changed (I think I read that there is an issue with the view not being reloaded/recreated when only the item id would change in the URL.
This issue you mentioned occurs if you are not using the HTML5 history mode, but an anchor (#) in the URL instead. If you are just changing the part after the anchor in the URL, the page is not actually refreshed by the browser. The Vue component won't be reloaded in this case and the state is still old. There are basically two ways around this:
You are switching from anchors in the URL to a real URL with the HTML5 history mode, supported by the Vue Router. This requires some back-end configuration, though. The browser then does not have this faulty behavior, because there is no anchor. It will reload the page on every manual URL change.
You can watch the $route object to get notified on every route change. Depending on if the user is changing the part after the anchor, or before, the behavior is different (it also depends where the cursor is, when you hit enter). If the part after the anchor is changed (your actual Vue route), only the component is notified. Otherwise, a full page refresh is made. Here's some example code:
// ...inside a Vue component
watch: {
'$route' (to, from) {
this.loadData();
}
}
I've got a VueJs front end that fetches some data from an API. The app uses vue-router.
The data fetched for one component is similar to the following:
{
name: ...,
email: ...,
order: {
data: {
line_items: [
{
quantity: ...
}
]
}
}
}
The component is instantiated with a data object called info:
data () {
return {
info: {}
}
}
In the beforeRouteEnter hook, the data is fetched by a vue-resource http.get and info is set to the body of the result like this:
vm.info = result.body
When the component renders, the following errors are produced:
TypeError: undefined is not an object (evaluating _vm.order.data.line_items')
In the template, the data is referenced in curly braces as per usual, however, if I just reference info in the template like this:
{{ info }}
it will output all of the data and not complain at all.
What is the correct way to assign a deeply nested data object?
If you are finding #saurabh answer is not working then you may need to check how you are assigning the new values to your object.
Firstly is the data being accidiently set as a string? hence {{ info }} working (or appearing to). May be worth using response.json() to set the data.
If thats not it then the error may be produced as the data you have set is not reactive. As you are assigning a nested object you may need to use different methods to make it reactive, i.e
Vue.set(vm.someObject, 'b', 2)
or
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })
check out: https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats
because your response is an object you may want to break out your data into corresponding params, i.e.
data () {
return {
info: {
name: '',
email: '',
order: {},
},
}
}
then you can assign name & email as you expected (info.email = ...).
For info.order you'd use Vue.set:
Vue.set(this.info, 'order', result.body.order)
The actual issue here is a life cycle one. The route guard beforeRouteEnter is called after the component is created so the error is thrown because the data isn’t there when the component tries to access it.
You have to use condition rendering here, which you can easily do with help of Vue directive v-if. It may give error if the data is not populated and you try to access it, so v-if will render that part of HTML only when data is present.
You need to do something like following:
<div v-if="info.order">
<span>
{{ info.order }}
</span>
</div>
In my scenario, I had to use one Vue.set that wrapped an Object.assign:
I'm trying to set state.workouts[state.workoutDate].isDone in a Vuex mutation
toggleWorkout(state, isDone) {
Vue.set(
state.workouts,
state.workoutDate,
Object.assign({}, state.workouts[state.workoutDate], { isDone: isDone })
);
},
Force update object setting a new object with same content
<button #click="my.object.nested.variable = 2; my = Object.assign({}, my);">
or
this.my.object.nested.variable = 2;
this.my = Object.assign({}, this.my);