VueJS executing data function too late (?) - javascript

EDIT: Solved thanks to #Mythos, but I'm very grateful to anyone who put their time into helping me, I was stuck on this for hours. Thanks a lot!
I have a Vue.js project created using vue-cli 4 and Vue 2.
It seems like a v-for I'm using to render a list is getting data too late. This is how my component is set up:
import { readLocalStorage } from '../../public/localStorage.js'
export default {
name: 'lista',
components: {
codiceLista
},
data(){
let salvati = readLocalStorage()
return {
codici: salvati
}
}
}
I have a component (codiceLista) which is rendered using v-for and data from the data function, and I'm experiencing a very weird behaviour. Whenever I manually reload the page nothing renders, telling me that my v-for is trying to access data that is not defined. However, if I remove the : in front of the v-for, which causes an error, add it again, the server auto-reloads and I see the list, but if I manually reload, without touching the code nothing renders and I get the same error in the console. Note that I have other elements in the page apart from the list, but when the error in the console appears even those don't render, even if completely unrelated and aren't using nothing from the component's data function. Bear with me, as I'm a beginner in Vue.js and new to programming in general. Any help would be kindly appreciated.

Don't add a : in front of v-for. : is a shorthand for v-bind. You can't bind v-for.
Also, initialize all the data first, and then you can populate it in lifecycle hooks.
data() {
return {
codeici: [],
}
}
mounted() {
this.codeici = readLocalStorage();
}
see Vue Lifecyle Hooks

Related

Vue3 call a components method from another component

I'm trying to have a component call a function from another component but just from
inside the setup.
I've read this from vuemastery https://www.vuemastery.com/blog/understanding-vue-3-expose/
and I see that you can accomplish this like this:
methods: {
reset () {
this.$refs.counter.reset()
},
terminate () {
this.$refs.counter.terminate()
}
}
however, I don't have access to those methods inside the setup, and I also can't use this.$refs inside the setup. Is there a way I can do the same thing inside setup, or a way to access these methods inside the setup?
Those methods are undefined in setup, and I cannot access data setup from within those functions, and I cannot use $refs in setup.
The $refs is a very easy way to call a function from another component - but I can't seem to find a relatively easy way to do this with vue3 composition api - am I missing something?
Hey I figured it out https://vuejs.org/guide/essentials/template-refs.html#accessing-the-refs
If I go
<script>
export default {
setup(props) {
const counter = ref(null);
//now I have access to counter.value.reset after it's mounted
counter.value.reset(); //does not work! it's null here
onMounted(() => {
counter.value.reset(); //here it works
})
//make sure to return counter in return
looks like there is an easy way to do this in setup just has a caveat
you cant just call it right away because setup happens before mounted https://vuejs.org/guide/essentials/lifecycle.html
I am calling it on a trigger - so if someone selects something from a dropdown I am watching the v-model and am able to call it there no problem.
Thanks for all the input.
Putting this answer here in case anyone else needs to accomplish the same thing

Why is my array undefined when Vue component is created?

I'm having trouble understanding why an array is undefined in my Vue component. Here is the flow:
SelectCategories.vue component uses router to push to Quiz.vue component. Here I use props to pass on data to Quiz component. Here is how it is done:
else {
this.$router.push({
name: 'quiz',
params: {
items: selectedCategories
}
})
}
We move on to Quiz.vue component, where I do the following on the created life cycle hook:
async created() {
for (const item of this.items) {
var questions = await QuestionService.getQuestionsByCategory(item);
this.questions.push(questions);
console.log(this.questions);
}
}
The QuestionService reaches out to the database and gets some information, which is pushed into the questions array I have defined here:
export default {
name: 'quiz',
props: ['items'],
data: function() {
return {
questions: [],
error: '',
};
}
Finally, I try to access this data in the template using an h1:
<h1>{{questions[0][0].description}}</h1>
However, I end up with the following error:
TypeError: Cannot read property '0' of undefined
What I am seeing in the console is that:
{{questions[0][0].description}}
Is happening before I populate the questions array in the created life cycle hook. As you can see here:
It is my understanding that created is called before the template, but I might be wrong. What I want to accomplish is to have the functionality in created() have happen before the template is loaded, so the array gets populated first. Thank you for the help.
What I want to accomplish is to have the functionality in created() have happen before the template is loaded
.. this is not possible. Read more on JS asynchrony. await does not block the execution of whole app. created() hook just returns promise and is scheduled to continue when async call finishes.
Only way is to write your template in a way that the data are not present at first and will come later...
Making created async and using await inside it does not prevent the component from being created and mounted. It only prevents the execution of code placed in created after await, until the promise resolves.
Which means your component will render without the expected contents in questions (until questions get updated, after the call returns).
The solution is to prevent anything currently breaking inside your component from breaking by wrapping those blocks in if conditions (or v-if in <template>).
A common solution for this type of problem is to make the call from outside the component and condition its rendering (creation) based on the response. Typically you want to place data that's consumed by more than one component in a store (single source of truth) which can then be injected anywhere you need it.
For more complex stores you could use Vuex and for very simple ones (basic data sharing) you could use Vue.observable().

vue.js - dynamically generated component in v-for does not update bound property correctly

I came upon a problem in vue.js and I'm not sure how I should solve it. I'll try to explain the gist of it as clear and short as possible.
Situation
I load data from a rest API. I created a class using the javascript prototype syntax to create a class called DataLoader which handles all the loading and internal state changes. More often than not, the loaded data is displayed in a repetitive way (e.g. list or cards). I created a new component called DataLoaderWrapper for this purpose, taking a DataLoader object as property. The DataLoaderWrapper displays loading spinners according to the loading state of the DataLoader object. In addition, the component markup includes the following tag:
<slot :dataLoader='dataLoader' />
.. to allow for easy access to the loaded data. The idea is to use the component like so:
<DataLoaderWrapper :dataLoader="myDataLoader">
<template slot-scope="props">
<template v-for="item in props.dataLoader.data">
{{item}}
</template>
</template>
</DataLoaderWrapper>
Now just to clear this up right away; why do I use props.dataLoader instead of myDataLoader within the template? The basic idea is that the DataLoader object sometimes is generated dynamically and therefore the most direct access to the object instance is via the component bound property. (I also tried direct access to the loader and found no different behavior, but this way makes the component easier to use for me)
Now let's extend this basic concept.
Imagine you have a DataLoader object which is configured to load a list of music releases. This would look something like this:
<DataLoaderWrapper :dataLoader="myReleaseLoader">
...
</DataLoaderWrapper>
Furthermore, every release is published by one or more artists. The list of artists needs to be loaded separately. I can extend the example like this:
<DataLoaderWrapper :dataLoader="myReleaseLoader">
<template slot-scope="props">
<template v-for="release in props.dataLoader.data">
<h1>release.Title</h1>
Artists:
<DataLoaderWrapper :dataLoader="getArtistsLoader(release)">
<template slot-scope="nestedProps">
{{nestedProps.dataLoader.data.map(artist => artist.Name).join(', ')}}
</template>
</DataLoaderWrapper>
</template>
</template>
</DataLoaderWrapper>
The Problem
The releases are loaded correctly and the templates of myReleaseLoader are rendered correctly. As soon as this happens, a new DataLoader instance is created dynamically and ordered to load the artists for a given release. This happens for every release. While the artist loaders are loading, the loading spinner is displayed. The problem is, that the nestedProps variable does not get updated as soon as the DataLoader instance is done loading the data.
I've been trying to solve this issue for at least two days, meaning I've debugged and checked everything that came to my mind.
I am sure that the data is correctly loaded and existent. But the loading spinner of the DataLoaderWrapper component is still displayed and printing nestedProps.dataLoader reveals data: [] which does not line up with the state shown when inspecting the component. This brings me to the conclusion, that the nestedProps property is not updated correctly.
I think this could be because vue bindings are one-directional and an update in a child component will not trigger the parent to update.
Having the page loaded on my local machine with 'hot-reloading' on performs an update to the UI every time I save changes in my files without performing a full reload of the page. The loaders are in a different state when updating the UI now than they are when loading the page for the first time. The data does not get loaded again, because the getArtistsLoader(..) function implements a cache which guarantees that always the same loader is returned for a given release when requested, this means that the data is essentially already existent when re-rendering the page, resulting in the desired output being displayed. I wasn't able to get into the correct state otherwise.
I tried to work with computed properties, direct access to the loader and more direct access to the desired property (e.g. props.data instead of props.dataLoader.data), none of which solved the update issue.
How can I do this?
The problem is getArtistsLoader does not return data synchronously
const data = getArtistsLoader(release);
// data === undefined
so inside the DataLoaderWrapper, it gets undefined for the dataLoader props
Solution
DataLoaderWrapper can accept a promise as data, so it can update when promise resolved.
1. update DataLoaderWrapper to accept promise
I use an async prop to distinguish dataLoader is asynchronous or not
<template>
<div>
<slot :dataLoader="realData"/>
</div>
</template>
<script>
export default {
props: {
dataLoader: null,
async: Boolean
},
data() {
let realData = this.dataLoader;
if (this.async) {
realData = [];
this.dataLoader
.then(data => {
this.realData = data;
})
.catch(error => {
console.log(error);
});
}
return {
realData: realData
};
}
};
</script>
2. make getArtistsLoader return promise
function getArtistsLoader(release) {
retrun axios.get('/artists?release=' + release.id)
.then((response) => response.data)
}
3. add async prop
<DataLoaderWrapper async :dataLoader="getArtistsLoader(release)">
full demo

VUE Deallocating memory using THREE JS between routes

I'm in the process of learning VUE JS. I have a very basic SPA that routes between various pages.
I have a number of THREE JS demos that I've built in my spare time and I've noticed that if I jump between pages on them eventually they grind to a halt from a build up of memory. I want to avoid pasting their scripts in here as they're massive and I don't think the problem lies within them.
I think the problem the lies somewhere in between how I'm instantiating them and my lack of knowledge in VUE JS is what's causing me trouble with this problem.
Here is an example of one of the views I'm routing to in VUE JS:
<template>
<div class="particles">
<main-menu></main-menu>
<div id="demo"></div>
</div>
</template>
<script>
import mainMenu from 'root/views/menu.vue';
export default {
components: { mainMenu },
mounted() {
var Particles = require('root/webgl/particles.js');
var demo = new Particles();
demo.run();
}
}
</script>
The original demos were built using traditional JavaScript (it's a ES5/6 class) and I was hoping that I could just plug them into my VUE SPA. Within each demo I am doing things like:
this.vsParticles = document.getElementById('vs-particles').textContent;
to load the shaders and to attach my THREE demo to a DOM element.
The issue I'm having is that somewhere something isn't being deleted. Within my demos i'm not creating anything new in the DOM and they run fine in non-SPA demos, but once I place them into a SPA app and jump between pages they build up.
I'm under the impression that when you change routes everything should get wiped. So all of the elements within my template tags and any objects I create within mounted(). But this doesn't seem to be the case and I'm just wondering is there something extra I need to include in VUE to completely clean everything up inbetween pages?
As Mathieu D. mentionned, you should move your require outside of the method.
Also, you may need to clear the WebGL context on the destroyed () Vue Component's hook.
I am not sure if it is the right way to do, but here is how I dispose it in my program :
this.renderer.forceContextLoss()
this.renderer.context = null
this.renderer.domElement = null
this.renderer = null
Could you try to import particles.js out of mounted method ?
For example, under your import of mainMenu ?
The import would be done one for all of your instanciation of Particles.
This will give you this code :
import mainMenu from 'root/views/menu.vue';
import Particles from 'root/webgl/particles.js';
export default {
components: { mainMenu },
mounted() {
var demo = new Particles();
demo.run();
}
}
You could also read reactivity in depth in the documentation. It will help you understand how VueJS store and access data. I don't know if in your component code you store some data of your ThreeJS examples, but if so, it could consume some memory. In that case, use destroyed hook to clean your data.

VueJS render doesn't get called

At work we are working with VueJS and we want to render one element with the contents from a 2D array. This needs 2 for loops to iterate through which isn't possible with a template. We found out we can do this with the render() function.
Our code (but simplified a bit) is:
export default {
name: 'od-table',
data() {
return {
testData: []
}
},
render() {
console.log("render function")
}
};
When we include this component in the project the render function doesn't get called. I can see the component has loaded because I can read the test-data. No logs or any other sign the render function has been called.
It could be it's an obvious mistake since we have never got this working before, but since this is something we have to use for this problem we would like to know what is wrong with the code.
Given it worked when you recreated your component file from scratch, the issue might have been that in your original file you had mistakenly left a section. That's what happened to me. If your component comes with its own <template>, it will be used for rendering and your render() function will never get called at all.
I still don't know what was wrong, but deleting the component and copying the old code in the new files seemed to solve the problem.
Any particular reason why you're using render? You might be looking for mounted.
Here is a complete list of lifecycle hooks for Vue.js 2.0. https://v2.vuejs.org/v2/api/#Options-Lifecycle-Hooks

Categories

Resources