Vue 3 (CLI): Cannot read properties of undefined (reading 'get') - vue.js

When switching to Vue 3 CLI and consequently refactoring the code, this.$http.get('/api/todo/') no longer works. Instead of being returned a list of todos from the database, I receive a Cannot read properties of undefined error in the console:
app.js:209 Uncaught TypeError: Cannot read properties of undefined (reading 'get')
at Proxy.getTodos (Todos.vue?4897:38:1)
at Proxy.mounted (Todos.vue?4897:28:1)
at eval (runtime-core.esm-bundler.js?d2dd:2722:1)
at callWithErrorHandling (runtime-core.esm-bundler.js?d2dd:155:1)
at callWithAsyncErrorHandling (runtime-core.esm-bundler.js?d2dd:164:1)
at hook.__weh.hook.__weh (runtime-core.esm-bundler.js?d2dd:2697:1)
at flushPostFlushCbs (runtime-core.esm-bundler.js?d2dd:341:1)
at render (runtime-core.esm-bundler.js?d2dd:6247:1)
at mount (runtime-core.esm-bundler.js?d2dd:4440:1)
at app.mount (runtime-dom.esm-bundler.js?2725:1574:1)
Additionally, I observe that both the apps and components lists are empty in Vue devtools.
After searching and experimenting with solutions for a few hours, I have not found a solution that works yet.
This is the current code causing the issues:
In Todos.vue, the template is rendered because I do se "Hi there", but I do not see any list items anymore:
<template>
<div id="TODOS">
Hi there
<ol>
<li v-for="todo in v_todos" :key="todo.id">
<span style="color:red;">{{ todo.name }}</span>
</li>
</ol>
</div>
</template>
<script>
export default {
// el: "#TODOS",
name: 'Todos',
data() {
return {
v_todos: [],
}
},
computed: {},
components: {},
mounted() {
this.getTodos();
},
methods: {
getTodos: function () {
this.$http.get('/api/todo/')
.then((response) => {
this.v_todos = response.data;
})
.catch((err) => {
console.log(err);
})
},
}
</script>
In App.vue:
<template>
<div id="App">
<Todos />
</div>
</template>
<script>
import Todos from './components/Todos.vue'
export default {
name: 'App',
components: {
Todos
}
}
</script>
In the HTML page todos.html:
...
<div id="App"></div>
...
In base.html, at the end of the body:
<script type="text/javascript" src="{% static 'src/vue/dist/js/chunk-vendors.js' %}"></script>
<script type="text/javascript" src="{% static 'src/vue/dist/js/app.js' %}"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-resource#1.3.5"></script>
I am completely new to Vue, so I would strongly appreciate if solution proposals are presented in a simple-to-understand way.

add to your main.js
import axios from 'axios';
const app = createApp(App);
app.config.globalProperties.$http = axios;

Related

Vue 3 - cannot name ref same as prop - why?

If I define a property with the name 'foobar' and then try to create a ref with the name 'foobar', Vue complains that 'foobar' is undefined when I try to access that ref. This only happens when building the vue app e.g. npm run build, but not when serving it e.g. npm run dev. I'm not sure if it happens with Vue 2.
I can fix the issue simply by naming them different things but I am curious as to why this happens as there's nothing in the docs about it?
<script setup>
defineProps({
foobar: {
type: String
}
})
</script>
<template>
<div>
<div ref="foobar">
{{ foobar }}
</div>
</div>
</template>
<script>
export default {
data() {
return {
}
},
mounted() {
// error!
const el = this.$refs.foobar;
}
}
</script>
No error:
<script setup>
defineProps({
foobar: {
type: String
}
})
</script>
<template>
<div>
<div ref="foobar2">
{{ foobar }}
</div>
</div>
</template>
<script>
export default {
data() {
return {
}
},
mounted() {
// no error!
const el = this.$refs.foobar2;
}
}
</script>

Vue3 Components in a Separate File

I have an app that I am porting from Vue2 to Vue3. I am not using a bundler as Vue is just providing simple interactivity on a page. This is the simplified version in Vue2:
// my-modal.js
var myModal = Vue.component('my-modal',
{
template:
`<div id="myModal">
<button #click.prevent="open=true">Open Modal</button>
<div v-if="open" class="my-modal">
<p>Hello from the modal!</p>
<button #click.prevent="open=false">Close</button>
</div>
</div>`,
data() {
return {
open: false
}
}
})
In Vue 2 this Works;
<script src="https://unpkg.com/vue#2"></script>
<div id="app">
<div class="outer">
<h3>Modal Example - {{ message }}</h3>
<div>
<my-modal />
</div>
</div>
</div>
<script src="/js/my-modal.js"></script>
<script>
const app = new Vue(
{
el: '#app',
data() {
return {
message: 'Hello Vue!'
}
}
});
</script>
For Vue3, per the documentation at: https://v3-migration.vuejs.org/breaking-changes/global-api.html#a-new-global-api-createapp I switched things over to what I expected would work:
<script src="https://unpkg.com/vue#3"></script>
<div id="app">
<div class="outer">
<h3>Modal Example - {{ message }}</h3>
<div>
<my-modal />
</div>
</div>
</div>
<script src="/js/my-modal.js"></script>
<script>
Vue.createApp({
data()
{
return {
message: 'Hello Vue!'
}
}
}).mount('#app')
</script>
and in the my-modal.js file I changed the first few lines to use the Global Vue:
const { createApp } = Vue;
const app = createApp({});
app.component('my-modal', ...
The Vue instance works but the component is not found with the error message: "Failed to resolve component: my-modal". I tried adding a 'components' section to the Vue instance and a few other things with no luck.
Any suggestions?
Each instance from createApp() is unique. That is, calling createApp() in index.html does not return the same instance from the previous call in my-modal.js.
One solution is to declare the global app instance before importing my-modal.js:
<!-- index.html -->
<script>
window._app = Vue.createApp({⋯})
</script>
<script src="/js/my-modal.js"></script>
<script>
// finally mount
window._app.mount('#app')
</script>
// js/my-modal.js
window._app.component('my-modal', {⋯})
demo

Async loading child component doesn't trigger v-if

Hi everyone and sorry for the title, I'm not really sure of how to describe my problem. If you have a better title feel free to edit !
A little bit of context
I'm working on a little personal project to help me learn headless & micro-services. So I have an API made with Node.js & Express that works pretty well. I then have my front project which is a simple one-page vue app that use vuex store.
On my single page I have several components and I want to add on each of them a possibility that when you're logged in as an Administrator you can click on every component to edit them.
I made it works well on static elements :
For example, here the plus button is shown as expected.
However, just bellow this one I have some components, that are loaded once the data are received. And in those components, I also have those buttons, but they're not shown. However, there's no data in this one except the title but that part is working very well, already tested and in production. It's just the "admin buttons" part that is not working as I expect it to be :
Sometimes when I edit some codes and the webpack watcher deal with my changes I have the result that appears :
And that's what I expect once the data are loaded.
There is something that I don't understand here and so I can't deal with the problem. Maybe a watch is missing or something ?
So and the code ?
First of all, we have a mixin for "Auth" that isn't implemented yet so for now it's just this :
Auth.js
export default {
computed: {
IsAdmin() {
return true;
}
},
}
Then we have a first component :
LCSkills.js
<template>
<div class="skills-container">
<h2 v-if="skills">{{ $t('skills') }}</h2>
<LCAdmin v-if="IsAdmin" :addModal="$refs.addModal" />
<LCModal ref="addModal"></LCModal>
<div class="skills" v-if="skills">
<LCSkillCategory
v-for="category in skills"
:key="category"
:category="category"
/>
</div>
</div>
</template>
<script>
import LCSkillCategory from './LCSkillCategory.vue';
import { mapState } from 'vuex';
import LCAdmin from '../LCAdmin.vue';
import LCModal from '../LCModal.vue';
import Auth from '../../mixins/Auth';
export default {
name: 'LCSkills',
components: {
LCSkillCategory,
LCAdmin,
LCModal,
},
computed: mapState({
skills: (state) => state.career.skills,
}),
mixins: [Auth],
};
</script>
<style scoped>
...
</style>
This component load each skills category with the LCSkillCategory component when the data is present in the store.
LCSkillCategory.js
<template>
<div class="skillsCategory">
<h2 v-if="category">{{ name }}</h2>
<LCAdmin
v-if="IsAdmin && category"
:editModal="$refs.editModal"
:deleteModal="$refs.deleteModal"
/>
<LCModal ref="editModal"></LCModal>
<LCModal ref="deleteModal"></LCModal>
<div v-if="category">
<LCSkill
v-for="skill in category.skills"
:key="skill"
:skill="skill"
/>
</div>
<LCAdmin v-if="IsAdmin" :addModal="$refs.addSkillModal" />
<LCModal ref="addSkillModal"></LCModal>
</div>
</template>
<script>
import LCSkill from './LCSkill.vue';
import { mapState } from 'vuex';
import LCAdmin from '../LCAdmin.vue';
import LCModal from '../LCModal.vue';
import Auth from '../../mixins/Auth';
export default {
name: 'LCSkillCategory',
components: { LCSkill, LCAdmin, LCModal },
props: ['category'],
mixins: [Auth],
computed: mapState({
name: function() {
return this.$store.getters['locale/getLocalizedValue']({
src: this.category,
attribute: 'name',
});
},
}),
};
</script>
<style scoped>
...
</style>
And so each category load a LCSkill component for each skill of this category.
<template>
<div class="skill-item">
<img :src="img(skill.icon.hash, 30, 30)" />
<p>{{ name }}</p>
<LCAdmin
v-if="IsAdmin"
:editModal="$refs.editModal"
:deleteModal="$refs.deleteModal"
/>
<LCModal ref="editModal"></LCModal>
<LCModal ref="deleteModal"></LCModal>
</div>
</template>
<script>
import LCImageRendering from '../../mixins/LCImageRendering';
import { mapState } from 'vuex';
import Auth from '../../mixins/Auth';
import LCAdmin from '../LCAdmin.vue';
import LCModal from '../LCModal.vue';
export default {
name: 'LCSkill',
mixins: [LCImageRendering, Auth],
props: ['skill'],
components: { LCAdmin, LCModal },
computed: mapState({
name: function() {
return this.$store.getters['locale/getLocalizedValue']({
src: this.skill,
attribute: 'name',
});
},
}),
};
</script>
<style scoped>
...
</style>
Then, the component with the button that is added everywhere :
LCAdmin.js
<template>
<div class="lc-admin">
<button v-if="addModal" #click="addModal.openModal()">
<i class="fas fa-plus"></i>
</button>
<button v-if="editModal" #click="editModal.openModal()">
<i class="fas fa-edit"></i>
</button>
<button v-if="deleteModal" #click="deleteModal.openModal()">
<i class="fas fa-trash"></i>
</button>
</div>
</template>
<script>
export default {
name: 'LCAdmin',
props: ['addModal', 'editModal', 'deleteModal'],
};
</script>
Again and I'm sorry it's not that I haven't look for a solution by myself, it's just that I don't know what to lookup for... And I'm also sorry for the very long post...
By the way, if you have some advice about how it is done and how I can improve it, feel free, Really. That how I can learn to do better !
EDIT :: ADDED The Store Code
Store Career Module
import { getCareer, getSkills } from '../../services/CareerService';
const state = () => {
// eslint-disable-next-line no-unused-labels
careerPath: [];
// eslint-disable-next-line no-unused-labels
skills: [];
};
const actions = {
async getCareerPath ({commit}) {
getCareer().then(response => {
commit('setCareerPath', response);
}).catch(err => console.log(err));
},
async getSkills ({commit}) {
getSkills().then(response => {
commit('setSkills', response);
}).catch(err => console.log(err));
}
};
const mutations = {
async setCareerPath(state, careerPath) {
state.careerPath = careerPath;
},
async setSkills(state, skills) {
state.skills = skills;
}
}
export default {
namespaced: true,
state,
actions,
mutations
}
Career Service
export async function getCareer() {
const response = await fetch('/api/career');
return await response.json();
}
export async function getSkills() {
const response = await fetch('/api/career/skill');
return await response.json();
}
Then App.vue, created() :
created() {
this.$store.dispatch('config/getConfigurations');
this.$store.dispatch('certs/getCerts');
this.$store.dispatch('career/getSkills');
this.$store.dispatch('projects/getProjects');
},
Clues
It seems that if I remove the v-if on the buttons of the LCAdmin, the button are shown as expected except that they all show even when I don't want them to. (If no modal are associated)
Which give me this result :
Problem is that refs are not reactive
$refs are only populated after the component has been rendered, and they are not reactive. It is only meant as an escape hatch for direct child manipulation - you should avoid accessing $refs from within templates or computed properties.
See simple demo below...
const vm = new Vue({
el: "#app",
components: {
MyComponent: {
props: ['modalRef'],
template: `
<div>
Hi!
<button v-if="modalRef">Click!</button>
</div>`
}
},
data() {
return {
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<my-component :modal-ref="$refs.modal"></my-component>
<div ref="modal">I'm modal placeholder</div>
</div>
The solution is to not pass $ref as prop at all. Pass simple true/false (which button to display). And on click event, $emit the event to the parent and pass the name of the ref as string...

Declaring-Reactive-Properties in Vue from the documentation example

I am trying to create a simple with vue-cli and the router that fetches Covid-19 cases by Country from a JSON object of arrays. This is my first Vue app. However, I keep getting an error about "Declaring Reactive Properties". I searched dozens of similar errors on many different forums and seemed to do the trick.
Most of the code is from vue.org, except for the JSON link.
Api.js:
import axios from "axios";
import Vue from "vue";
new Vue({
el: "#app",
data() {
return {
info: null,
loading: true,
errored: false
};
},
template: "<div>{{ output.info }}</div>",
mounted() {
axios
.get("https://pomber.github.io/covid19/timeseries.json")
.then(response => {
this.info = response.data;
console.log(this.info);
})
.catch(error => {
console.log(error);
this.errored = true;
})
.finally(() => (this.loading = false));
}
});
export default {
name: "About",
props: {
loading: String,
errored: String,
info: String
}
};
About.js
<template>
<h1>Covid-19 cases by Country</h1>
<section v-if="errored">
<p>
We're sorry, we're not able to retrieve this information at the moment,
please try back later
</p>
</section>
<section v-else>
<div v-if="loading">Loading...</div>
<div v-else v-for="data in info" :key="data" class="currency">
<h1>{{ data.Portugal[0].deaths }}</h1>
</div>
</section>
Error:
[Vue warn]: Property or method "errored" is not defined on the
instance but referenced during render. Make sure that this property is
reactive, either in the data option, or for class-based components, by
initializing the property
I can see the warning 3 times, for each of the props errored, loading and info, the most important one.
There's a bit of confusion in what you have.
In Api.js you're mounting a Vue app without much of a template to an element with an id of app. And then you export a Vue-like object which, if imported in another component could be used like <about />. However, we don't know if you use it anywhere else.
In About.js you seem to only have a <template>, without controller or style. You probably wanted to couple the two together, which would look similar to this (I can't use SFC's on SO, so I just declared the component inline, with Vue.component()):
Vue.config.productionTip = false;
Vue.config.devtools = false;
Vue.component('About', {
template: `<div>
<h1>Covid-19 cases by Country</h1>
<section v-if="errored">
<p>
We're sorry, we're not able to retrieve this information at the moment,
please try back later
</p>
</section>
<section v-else>
<div v-if="loading">Loading...</div>
<div v-else v-for="(data, name) in info" :key="name" class="currency">
<h1>{{ name }}</h1>
<div v-for="(entry, key) in data" :key="key">{{entry}}</div>
</div>
</section>
</div>`,
data: () => ({
info: null,
loading: true,
errored: false
}),
mounted() {
axios
.get("https://pomber.github.io/covid19/timeseries.json")
.then(response => {
this.info = response.data;
// console.log(this.info);
})
.catch(error => {
// console.log(error);
this.errored = true;
})
.finally(() => (this.loading = false));
}
})
new Vue({
el: '#app'
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.2/axios.min.js"></script>
<div id="app">
<About/>
</div>
If you want to do the loading of data in parent, you have to pass the required props to <About />, along these lines:
In App.js template:
<About :loading="loading"
:info="info"
:errored="errored"
/>
In About.jss props:
props: {
loading: Boolean,
info: Object,
errored: Boolean
}
That's the gist of it. But, in your case, that would seem an unnecessary complication.
As a side note, to speed things up towards your end goal, I took the liberty to add a few more features to your code: https://codesandbox.io/s/compassionate-dan-6z3zo
I hope you'll find them helpful.

Prop passed to child component is undefined in created method

I am using Vue.js 2.
I have a problem with passing value to the child component as a prop. I am trying to pass card to card-component.
In card-component I can access the prop in the Card goes here {{card}} section.
However when I try to access it in created or mounted methods it's undefined.
Parent:
<template>
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<card-component :card="place.card"></card-component>
</div>
</div>
</div>
</template>
<script>
import CostComponent from './CostComponent';
import CardComponent from './CardComponent';
export default {
components: {
CostComponent, CardComponent
},
props: ['id'],
data() {
return {
place: []
}
},
created() {
axios.get('/api/places/' + this.id)
.then(response => this.place = response.data);
}
}
</script>
Child:
<template>
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<ul class="list-unstyled">
Card goes here {{card}}
</ul>
</div>
</div>
</div>
</template>
<script>
import CardItemComponent from './CardItemComponent';
export default {
components: {
CardItemComponent
},
props: ['card'],
created() {
console.log(this.card); // undefined
},
mounted() {
console.log(this.card); // undefined
},
}
</script>
I did a lot of googling but none of the solutions I found have fixed my issue.
This is purely a timing issue. Here's what happens...
Your parent component is created. At this time it has an empty array assigned to place (this is also a problem but I'll get to that later). An async request is started
Your parent component creates a CardComponent instance via its template
<card-component :card="place.card"></card-component>
at this stage, place is still an empty array, therefore place.card is undefined
3. The CardComponent created hook runs, logging undefined
4. The CardComponent is mounted and its mounted hook runs (same logging result as created)
5. Your parent component is mounted
6. At some point after this, the async request resolves and changes place from an empty array to an object, presumably with a card property.
7. The new card property is passed down into your CardComponent and it reactively updates the displayed {{ card }} value in its template.
If you want to catch when the card prop data changes, you can use the beforeUpdate hook
beforeUpdate () {
console.log(this.card)
}
Demo
Vue.component('CardComponent', {
template: '<pre>card = {{ card }}</pre>',
props: ['card'],
created () {
console.log('created:', this.card)
},
mounted () {
console.log('mounted:', this.card)
},
beforeUpdate () {
console.log('beforeUpdate:', this.card)
}
})
new Vue({
el: '#app',
data: {
place: {}
},
created () {
setTimeout(() => {
this.place = { card: 'Ace of Spades' }
}, 2000)
}
})
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<div id="app">
<card-component :card="place.card" />
</div>
See https://v2.vuejs.org/v2/guide/instance.html#Lifecycle-Diagram
If place is meant to be an object, you should not be initialising it as an array. Also, if your CardComponent relies on data being present, you may want to conditionally render it.
For example
data () {
return { place: null }
}
and
<card-component v-if="place" :card="place.card"></card-component>
then CardComponent will only be created and mounted after place has data.
Make sure you have props: true in the router file. It is a simple solution but many of us forget this.
{
path: '/path-to',
name: 'Name To',
component: Component,
props: true
}