VueJs / Vuex : Rendering list of items - vue.js

I'm trying to render a list of offers from my vuex store. The problem is when i'm loading my list of offers page, they are not rendered.
Here is my code :
offers.js
export const namespaced = true
export const state = {}
export const mutations = {
[types.UPDATE_OFFERS] (state, offers) {
Object.assign(state, offers)
}
}
export const actions = {
async fetchOffers ({ commit }) {
const { data } = await axios.get('/api/offers')
commit(types.UPDATE_OFFERS, data)
}
}
offers.vue (my page component)
<template>
<div>
<div v-if="offers"
v-for="offer in offers"
:key="offer.id">
<div>
<div>
<router-link :to="{ name: 'offer', params: { offer: offer.id } }">
{{ offer.project }}
</router-link>
</div>
<div>
<div v-if="offer.versions[0] && offer.versions[0].status == 'edition'">
Validate the offer
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapState, mapGetters } from 'vuex'
export default {
computed: {
...mapState(['offers'])
},
beforeMount () {
this.$store.dispatch('offers/fetchOffers')
}
}
</script>
When i'm loading the page, I can see that the store as loaded the offers but the page doesnt render them. The weird thing is when I load the page offers then another page (example createAnOffer) and then I come back to offers, it renders the offers proplery.
I tried beforemount, beforeCreate, mounted, created.
I admit I have no clue what's going on.
Any tip ?
Thank you for your answers.
Louis

Related

vue test utils mount option data not working with compostion api

When i am learning the vue-test-utils from the offical site conditional-rendering.
I tried to change the option api to composition api.
It seems like the mount option data not working with the composition api.
Nav.vue Composition API test FAIL
<template>
<div>
<a id="profile" href="/profile">My Profile</a>
<a v-if="admin" id="admin" href="/admin">Admin</a>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const admin = ref(false)
</script>
Nav.vue Option API test PASS
<template>
<div>
<a id="profile" href="/profile">My Profile</a>
<a v-if="admin" id="admin" href="/admin">Admin</a>
</div>
</template>
<script>
export default {
data() {
return {
admin: false,
}
},
}
</script>
Nav.spec.js test
test('renders an admin link', () => {
const wrapper = mount(Nav, {
data() {
return {
admin: true
}
}
})
// Again, by using `get()` we are implicitly asserting that
// the element exists.
expect(wrapper.get('#admin').text()).toEqual('Admin')
})
I found a solution but I don't know if it's a good solution
test("renders an admin link", async () => {
const wrapper = mount(Nav);
wrapper.vm.admin = true;
await nextTick();
expect(wrapper.get("#admin").text()).toEqual("Admin");
});

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...

Problem with passing data from Vuex getters to child component

I am creating movie-app with Vue and Vuex which will display all movies in Home component and on click single movie in Details (child) component. I get all movies in Home component but I can not display single movie in Details component.
I have enabled props in child component but I have problem with passing movie ID to method imported from getters (in Details component) which should filter selected movie from movies list.
Home component
<template>
<div class="home">
<router-view />
<h1>Home component:</h1>
<div v-for="movie in movies"
:key="movie.id">
<div>
<router-link :to="'/movie/' + movie.id">{{ movie.title }}</router-link>
</div>
</div>
</div><!--home-->
</template>
<script>
import axios from 'axios'
import { mapGetters, mapActions } from 'vuex'
export default {
name: "home",
methods: {
...mapActions(['fetchMovies']),
},
computed: {
...mapGetters(['movies'])
},
created() {
this.fetchMovies()
}
};
</script>
Details component
<template>
<div>
<h1>Movie details - child component</h1>
<h2>Title: {{ movie.title }}</h2>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
props: ['id'],
computed: {
movie() {
return this.$store.getters.singleMovie(this.id)
}
}
}
</script>
Vuex store
const state = {
movies: []
};
const getters = {
movies: state => state.movies,
singleMovie: state => movieId => {
return state.movies.find(item => item.id === movieId);
}
};
const actions = {
async fetchMovies({ commit }) {
const response = await axios.get(
movie_url + "movie/popular" + "?api_key=" + api_key
);
commit("setMovies", response.data.results);
}
};
const mutations = {
setMovies: (state, items) => (state.movies = items)
};
export default {
state,
getters,
actions,
mutations
};
I tried to replace {{movie.title}} with {{id}} and then I get displayed movie ID as I click on listed movies in Home component. I also tried to hard code movie id as parameter: return this.$store.getters.singleMovie(299536) and I successfully get title displayed but in console I get error "TypeError: Cannot read property 'title' of undefined". Obviously I am making mistake with passing delivered movie ID.
Your computed property movie is not loaded in every vue life cycle. So, in the first time you get movie, it is undefined and your mustache {{ movie.title }} is actually something like {{ 'undefined'.title }}, which causes the error.
To solve it, you can both add a v-if="movie" conditional in template or return a default value in computed property movie. Like this, for example: return state.movies.find(item => item.id === movieId) || {};

Vuex: Child component wait for parent component dispatch action

Parent component (Dashboard):
<template>
<div id="dashboard">
<Header />
<b-container>
<div>
<b-row>
<b-col>
<Overview />
</b-col>
</b-row>
</div>
</b-container>
</div>
</template>
<script>
import Header from '#/components/common/Header';
import Overview from '#/components/dashboard/Overview';
import { mapGetters } from 'vuex';
export default {
name: 'dashboard',
components: {
Header,
Overview
},
mounted() {
const sessionId = this.$cookie.get('connect.sid');
this.$store.dispatch('user/getUser', sessionId).then((userData) => {
this.$store.dispatch('project/getProject', userData.data.user);
});
},
computed: {
...mapGetters('user', {
user: 'getUser'
})
}
}
</script>
Child component (Overview):
<template>
<div class="overview">
<div class="overview__title">
<h1>
Welcome {{user.cn[0]}} // Works
</h1>
</div>
<div class="overview__project">
<p v-for="project in runningprojects" :key="project._id">
{{project.name}} // Does not work at refresh
</p>
</div>
</div>
</template>
<script>
import {mapGetters} from 'vuex';
export default {
name: 'dashboard-overview',
data() {
return {
runningprojects: []
}
},
computed: {
...mapGetters('user', {
user: 'getUser'
}),
...mapGetters('project', {
projects: 'getProjects',
allProjects: 'getAllProjects'
})
},
mounted() {
console.log("mounted this.projects", this.projects);
// add something from this.projects to this.runningprojects
},
methods: {
calcReq() {
...
},
...
}
</script>
In my Dashboard component (parent) I fetch the user data with a vuex action dispatch('user/getUser) and after that I fetch the projects of this user dispatch('project/getProject).
In my Overview component (child) I want to show the project information of this user. I call my mapGetters and I have a component variable runningprojects inside data(). In my mounted() lifecycle I want to push data from my getters to this data array.
The following problem is given:
When I refresh my application, the console.log from my child component mounted() is called before the dispatch jobs are finished in the parent component (dashboard).
It only works if change something in my local files and vue-cli does a live reload.
Because of the page lifecycle of the vue app. when component renders mounted is called after created and it wont wait for the ajax or any async calls.
One solution would be to not render the child component until the async return
<template>
<div id="dashboard">
<Header />
<b-container>
<div>
<b-row>
<b-col>
<Overview v-if="finished"/>
</b-col>
</b-row>
</div>
</b-container>
</div>
</template>
<script>
import Header from '#/components/common/Header';
import Overview from '#/components/dashboard/Overview';
import { mapGetters } from 'vuex';
export default {
name: 'dashboard',
data() {
return {
finished: false,
}
},
components: {
Header,
Overview
},
mounted() {
const sessionId = this.$cookie.get('connect.sid');
this.$store.dispatch('user/getUser', sessionId).then((userData) => {
this.$store.dispatch('project/getProject', userData.data.user);
this.finished = true;
});
},
computed: {
...mapGetters('user', {
user: 'getUser'
})
}
}
</script>
Just add a v-if in the child component and when dispatch has return then set the value to true which will render the child component and the then the mounted will have the values you want
Other solution would be.
Use updated function instead of mounted and which will be called when ever there is a change in the state.

vuex module mode in nuxtjs

I'm trying to implement a todo list using modules mode in the vuex store in nuxtjs but get the error this.$store.todo is undefined and cant find much about this relating to nuxt
Can anyone assist please I have
store index.js
export const state = () => ({
})
export const mutations = {
}
store todo.js
export const state = () => ({
todos: [],
})
export const mutations = {
mutations ...
}
export const actions = {
actions ...
}
export const getters = {
getters ...
}
index.vue page
<template>
<div>
<h2>Todos:</h2>
<p> Count: {{ doneTodosCount }} </p>
<ul v-if="todos.length > 0">
<li v-for="(todo, i) in todos" :key="i">
...
</li>
</ul>
<p v-else>Done!</p>
<div class="add-todo">
<input type="text" v-model="newTodoText">
<button #click="add">Add todo</button>
</div>
</div>
</template>
<script>
import { mapState, mapMutations, mapActions, mapGetters } from 'vuex'
export default {
name: 'app',
data () {
return {
newTodoText: ""
}
},
created () {
this.$store.todo.dispatch('loadData')
},
computed: {
...mapState(['todos', ]),
...mapGetters([ 'doneTodosCount', 'doneTodos'])
},
methods: {
toggle (todo) {
this.$store.todo.dispatch('toggleTodo', todo)
},
}
}
</script>
From what i read I thought this should work but doesn't
I should add it all works fine if i don't use modules mode and just have a single index.js setup
Many Thanks
You need to call it differently
this.$store.dispatch('todo/toggleTodo', todo)
Also better to call it in fetch method, not created