I have the following Vue JS component that I am having an issue re-rendering on data changes.
Vue.component('careers-list', {
template: `
<div id="career-list">
<div class="context">
<div v-for="career in careerData">
<h1>{{ career.fields.jobTitle }}</h1>
<div v-html="career.fields.jobDescription">
</div>
</div>
</div>
<div id="career-pagination">
<button>prev</button>
<button>1</button>
<button v-on:click="increaseCount">next</button>
</div>
</div>`,
data: function() {
return {
careerData: [],
paginationCount: 0
}
},
created: function() {
this.fetchData();
},
methods: {
fetchData: function() {
client.getEntries({
skip: this.paginationCount,
limit: this.paginationCount + 7,
order: 'sys.createdAt'
})
.then(entries => {
this.careerData = entries.items;
this.careerData.map(function (career) {
career.fields.jobDescription = (marked(career.fields.jobDescription));
});
});
},
increaseCount: function() {
this.paginationCount += 7;
}
}
});
var app = new Vue({
el: '#app'
});
So as you can see I have a fetchData method that fetches my data and formats it. Take a look at these lines within this method:
skip: this.paginationCount,
limit: this.paginationCount + 7,
Within my app these lines determine how many records on database will be returned. I am using a count for this within my data object paginationCount: 0.
I have then set an event on one of my buttons within my component template: <button v-on:click="increaseCount">next</button>
When this is clicked it updates the paginationCount using the increaseCount method.
When I render my application and click the button the paginationCount does increase however my fetchData method does not re-render the application to show the appropriate data based on the count.
I thought that Vue automatically updates any methods that have a changed data object in them however I am guessing this isn't the case or I am doing this wrong.
Any idea how I can update my fetchData method when the paginationCount is changed?
Thanks, Nick
You need to use watchers here, which suits exactly your use-case:
This is most useful when you want to perform asynchronous or expensive operations in response to changing data.
Method is not executed as you are not returning anything from it, which depends on the vue data variable.
You have to invoke your method when paginationCount variable changes:
data: {
careerData: [],
paginationCount: 0
},
watch: {
// whenever paginationCount changes, this function will run
paginationCount: function (newPaginationCount) {
this.fetchData();
}
},
Related
I am very new to VUE so please forgive if my terminology is not correct.
I am trying to set a variable which is defined in the script tag "data()" function.
I am trying to set a new value for a variable defined in data() from inside the "created()" lifecycle event. This works fine if done at the root level, but i need to do it within 2 nested calls as shown below and it will not work when nested inside the function calls:
app.vue
<template>
<div class="container">
<Button #btn-click="providerPicked" id="current-provider" :text="'Current Provider: ' + currentProvider" />
</div>
</template>
<script>
import { ZOHO } from "./assets/ZohoEmbededAppSDK.min.js";
import Button from './components/Button'
export default {
name: 'App',
components: {
Button,
},
data() {
return{
currentProvider: 'x'
}
},
created() {
console.log("CREATED HOOK")
ZOHO.embeddedApp.on("PageLoad",function(data)
{
console.log(data);
//Custom Business logic goes here
let entity = data.Entity;
let recordID = data.EntityId[0];
ZOHO.CRM.API.getRecord({Entity:entity,RecordID:recordID})
.then(function(data){
console.log(data.data[0])
console.log(data.data[0].name)
//THIS DOES NOT WORK - variable still comes back 'x' in template, notice this is nested twice.
this.currentProvider = data.data[0].name;
});
});
ZOHO.embeddedApp.init();
//THIS DOES WORK - SETS VAR TO "reee" in template, notice this is not nested
this.currentProvider = "reee"
}
}
</script>
Use arrow functions instead of anonymous functions.
Arrow functions do not bind this, and thus this will refer to the outer scope.
created() {
console.log("CREATED HOOK")
ZOHO.embeddedApp.on("PageLoad", (data) => {
console.log(data);
//Custom Business logic goes here
let entity = data.Entity;
let recordID = data.EntityId[0];
ZOHO.CRM.API.getRecord({ Entity: entity, RecordID: recordID })
.then((data) => {
console.log(data.data[0])
console.log(data.data[0].name)
//THIS DOES NOT WORK - variable still comes back 'x' in template, notice this is nested twice.
this.currentProvider = data.data[0].name;
});
});
ZOHO.embeddedApp.init();
//THIS DOES WORK - SETS VAR TO "reee" in template, notice this is not nested
this.currentProvider = "reee"
}
I'm trying to call a computed property (which relies on a data variable to be set from incoming json) upon page creation but it says that it can't read the data for 'undefined'. This is due to the data I need not being fully loaded, I'm pretty sure. When I set it to run 2 seconds after created(), it works just fine.
How can I neatly set this to only run the function on my data variable one time but ONLY once the data results are actually set?
<div id="app">
<div v-for="(value, employee) in employeeFunction" :key="employee">
#{{value}}
</div>
</div>
<script>
var vm =
new Vue({
el: "#app",
data: {
results: [
{//data is in here
}
],
},
created: function(){
this.interval = setInterval(() => this.employeeFunction(), 2000);
},
computed: {
employeeFunction() {
console.log('employee data')
employeeResults = this.results
console.log(employeeResults)
return employeeResults
},
}
});
</script>
Instead of using the created() lifecycle-hook try with mounted()
I have stored a userProfile in Vuex to be able to access it in my whole project. But if I want to use it in the created() hook, the profile is not loaded yet. The object exists, but has no data stored in it. At least at the initial load of the page. If I access it later (eg by clicking on a button) everything works perfectly.
Is there a way to wait for the data to be finished loading?
Here is how userProfile is set in Vuex:
mutations: {
setUserProfile(state, val){
state.userProfile = val
}
},
actions: {
async fetchUserProfile({ commit }, user) {
// fetch user profile
const userProfile = await fb.teachersCollection.doc(user.uid).get()
// set user profile in state
commit('setUserProfile', userProfile.data())
},
}
Here is the code where I want to acess it:
<template>
<div>
<h1>Test</h1>
{{userProfile.firstname}}
{{institute}}
</div>
</template>
<script>
import {mapState} from 'vuex';
export default {
data() {
return {
institute: "",
}
},
computed: {
...mapState(['userProfile']),
},
created(){
this.getInstitute();
},
methods: {
async getInstitute() {
console.log(this.userProfile); //is here still empty at initial page load
const institueDoc = await this.userProfile.institute.get();
if (institueDoc.exists) {
this.institute = institueDoc.name;
} else {
console.log('dosnt exists')
}
}
}
}
</script>
Through logging in the console, I found out that the problem is the order in which the code is run. First, the method getInstitute is run, then the action and then the mutation.
I have tried to add a loaded parameter and played arround with await to fix this issue, but nothing has worked.
Even if you make created or mounted async, they won't delay your component from rendering. They will only delay the execution of the code placed after await.
If you don't want to render a portion (or all) of your template until userProfile has an id (or any other property your users have), simply use v-if
<template v-if="userProfile.id">
<!-- your normal html here... -->
</template>
<template v-else>
loading user profile...
</template>
To execute code when userProfile changes, you could place a watcher on one of its inner properties. In your case, this should work:
export default {
data: () => ({
institute: ''
}),
computed: {
...mapState(['userProfile']),
},
watch: {
'userProfile.institute': {
async handler(institute) {
if (institute) {
const { name } = await institute.get();
if (name) {
this.institute = name;
}
}
},
immediate: true
}
}
}
Side note: Vue 3 comes with a built-in solution for this pattern, called Suspense. Unfortunately, it's only mentioned in a few places, it's not (yet) properly documented and there's a sign on it the API is likely to change.
But it's quite awesome, as the rendering condition can be completely decoupled from parent. It can be contained in the suspensible child. The only thing the child declares is: "I'm currently loading" or "I'm done loading". When all suspensibles are ready, the template default is rendered.
Also, if the children are dynamically generated and new ones are pushed, the parent suspense switches back to fallback (loading) template until the newly added children are loaded. This is done out of the box, all you need to do is declare mounted async in children.
In short, what you were expecting from Vue 2.
I'm struggling to make this work properly every time I rendered the component by pushing it using the router object something like this this.$router.push('/Home/mypath'); which will focus only on the first input text element with index = 0 after the component is rendered even if I passed another value for index. Basically, I passed an index value to the ref of the input text element which is inside the v-for loop of a component and so at mounted(), I have something like this
mounted() {
this.$nextTick(() =>
{
this.$refs.newInp[index].focus();
});
}
but it keeps focusing on the first input element even though I passed a value of 1 or 2. When I looked at the console log, it shows this error on the console.
TypeError: Cannot read property '0' of undefined
pointing on this line this.$refs.newInp[index].focus();
Sample Code to fetch the data in the v-for
async GetContentDetails() {
let testRes =
await axios.get('myUrl/api', {
params: {
ContentId: this.$cookie.get('OpV-ContId')
}
}).then(res => this.contentItems = res.data)
.then()
.catch(error => console.log(error));
this.testData = testRes;
}
Template:
<div v-for="(contItem, index) in contentItems" :key="contItem.commentId">
<textarea class="reply-commented" ref="newInp"></textarea>
</div>
How to fix this type of issues? What is the solution for this?
Thanks.
From what I understood, you want to focus a textarea after fetching some data, that said trying to focus inside the mounted method wont work because you can't tell if the data has been fetch and the textareas already exist in the DOM.
So the best way to do this is to focus after being sure the data has been fetched, inside the then callback.
new Vue({
el: '#app',
data: {
posts: [],
index: 3 // Change this to focus whatever textarea you want
},
mounted () {
this.fetchItems();
},
methods: {
fetchItems () {
const url = 'https://jsonplaceholder.typicode.com/posts'
axios.get(url).then(response => {
this.posts = response.data
this.$nextTick(_ => {
this.$refs.newInp[this.index].focus()
})
})
}
}
});
<div id="app">
<div v-for="(post, index) in posts" :key="post.id">
<textarea class="reply-commented" ref="newInp" v-text="post.body"></textarea>
</div>
</div>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
For few days of research and thorough testing and observation on DOM behavior on how vue.js renders component and of course basing on the suggestions from other folks on this thread. I realized you can't really focus in the created/mounted properties on a specific index of an element within the for loop particulary in this case the input text element if the data being fetch to bind on the component is coming from the server due to its asynchronous behavior and you have to wait until the component is completely rendered. So I found a solution at least on my case to use a dynamic watcher either in the created or mounted properties and set a dummy or duplicate data properties for the default change of the data properties for the purpose of only to activate the watcher to focus on the specific element after the component has been rendered. This how it looks like. Hope this help to folks that encountering the same scenario as me.
created() {
this.GetContentDetails();
this.$watch('commentItems2', function () {
this.$nextTick(() => {
this.$refs.newRep[mapCredential.state.index2].focus();
});
});
},
methods: {
async GetComment2() {
let testRes =
await axios.get('myUrl/api/GetContent/GetComments')
.then(this.GetReply2())
.catch(error => console.log(error));
this.commentItems = testRes.data;
this.commentItems2 = testRes.data;
},
}
I'm giving Vue.js a try and so far I'm loving it because it's much simpler than angular. I'm currently using vue-router and vue-resource in my single page app, which connects to an API on the back end. I think I've got things mostly working with a the primary app.js, which loads vue-router and vue-resource, and several separate components for each route.
Here's my question: How do I use props to pass global data to the child components when the data is fetched using an asynchronous AJAX call? For example, the list of users can be used in just about any child component, so I would like the primary app.js to fetch the list of users and then allow each child component to have access to that list of users. The reason I would like to have the app.js fetch the list of users is so I only have to make one AJAX call for the entire app. Is there something else I should be considering?
When I use the props in the child components right now, I only get the empty array that the users variable was initialized as, not the data that gets fetched after the AJAX call. Here is some sample code:
Simplified App.js
var Vue = require('vue');
var VueRouter = require('vue-router')
Vue.use(VueRouter);
var router = new VueRouter({
// Options
});
router.map({
'*': {
component: {
template: '<p>Not found!</p>'
}
},
'/' : require('./components/dashboard.js'),
});
Vue.use(require('vue-resource'));
var App = Vue.extend({
ready: function() {
this.fetchUsers();
},
data: function() {
return {
users: [],
};
},
methods: {
fetchUsers: function() {
this.$http.get('/api/v1/users/list', function(data, status, response) {
this.users = data;
}).error(function (data, status, request) {
// handle error
});
}
}
});
router.start(App, '#app')
Simplified app.html
<div id="app" v-cloak>
<router-view users = "{{ users }}">
</router-view>
</div>
Simplified dashboard.js
module.exports = {
component: {
ready: function() {
console.log(this.users);
},
props: ['users'],
},
};
When dashboard.js gets run, it prints an empty array to the console because that's what app.js initializes the users variable as. How can I allow dashboard.js to have access to the users variable from app.js? Thanks in advance for your help!
p.s. I don't want to use the inherit: true option because I don't want ALL the app.js variables to be made available in the child components.
I believe this is actually working and you are being misled by the asynchronous behavior of $http. Because your $http call does not complete immediately, your console.log is executing before the $http call is complete.
Try putting a watch on the component against users and put a console.log in that handler.
Like this:
module.exports = {
component: {
ready: function() {
console.log(this.users);
},
props: ['users'],
watch: {
users: {
handler: function (newValue, oldValue) {
console.log("users is now", this.users);
},
deep: true
}
}
}
};
In the new version of Vue 1.0.0+ you can simply do the following, users inside your component is automatically updated:
<div id="app" v-cloak>
<router-view :users="users"></router-view>
</div>