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...
Related
I can't seem to pass dynamically modified properties from layouts into the <Nuxt /> component.
This is my ~/layouts/default.vue
<template>
<div>
<input v-model="myprop" />
<span>{{myprop}}</span>
<Nuxt />
</div>
</template>
<script>
export default {
provide: function () {
return {myprop: this.myprop};
},
data: () => ({
myprop: 'hello galaxy',
}),
}
</script>
This is my ~/pages/index.vue
<template>
<div>My Prop is: {{myprop}}</div>
</template>
<script>
export default {
inject: ["myprop"]
}
</script>
On the web page I see hello galaxy printed 3 times, once in the input, once in a span, and once in the Nuxt component. But when I edit the input field, only the span is updated. The Nuxt component does not capture the changes in myprop. The Nuxt component continues to show only hello galaxy while put the input and span shows changes as I type on my keyboard
What am I doing wrong?
The provide/inject is useful for simple situation, but if you've some reactive stuff the vuex store is more convenient :
in store/index.js
add a state called search and its mutations and actions :
export const state=()=>({
search:''
})
export const mutations ={
SET_SEARCH(state,payload){
state.search=payload
}
}
export const actions ={
setSearch(context,payload){
context.commit('SET_SEARCH',payload)
}
}
in layout/default.vue add computed property with setter/getter bound to the store:
<template>
<div>
<input v-model="search" />
<span>{{search}}</span>
<Nuxt />
</div>
</template>
<script>
export default {
computed:{
search:{
get(){
return this.$store.state.search
},
set(val){
this.$store.dispatch('setSearch',val)
}
}
}
}
</script>
in pages/index.vue :
<template>
<div>My search is: {{search}}</div>
</template>
<script>
export default {
computed:{
search(){
return this.$store.state.search
}
}
}
</script>
I'm writing my first app using NUXT. I'm stuck at this issue for 2 days, so I decided to ask even if I think this is a question with a simple answer (it has to be).
On my project's layouts I have a default.vue and a home.vue
default.vue:
<template>
<div>
<!-- call Header component, this has an nav menu -->
<Header />
<!-- call Hero component -->
<Hero />
<nuxt />
<Footer />
</div>
</template>
<script>
import Header from '~/components/Header.vue'
import Footer from '~/components/Footer.vue'
import Hero from '~/components/Hero.vue'
export default {
components: {
Header,
Footer,
Hero
},
}
</script>
I want to display data from each page (title, subtitle and imageUrl). This data sometimes come from an apollo query request other times are defined on page file.
I've read the docs and searched here for the answer but I wans't able to implement it. I think it has to be done thought Vuex store but I don't know how.
Thank you
You can use nuxtServerInit action in vuex as one way to populate page data.
If you are using nuxt >= 2.12, you can use the new-and-improved fetch hook inside your layouts to make your apollo queries.
I DID IT!
So, it took a time to figure out, but I've learnt a lot during this process.
I'll let here some references I've used to come with this solution.
Very nice article on passing data through props, custom events and Vuex Store
CodeSandBox from Nuxt Documentation.
This question has a method to await apollo data and then render data
Let's go to the way I did it. I don't know if it's the best, but worked like a charm here.
I've created a hero.js file on my store folder:
data: {
title: "",
subtitle: "",
imgUrl: ""
}
})
export const mutations = {
setData (state, obj) {
state.data = {...state.data, ...obj}
}
}
export const getters = {
getHero (state) {
return state.data
}
}
Then on my default.vue I did:
<div>
<!-- call Header component -->
<Header />
<!-- call Hero component with his slots-->
<Hero>
<template v-slot:title>
<h1 class="title">{{ hero.title }}</h1>
</template>
<template v-slot:subtitle>
<h2 class="subtitle">{{ hero.subtitle }}</h2>
</template>
<template v-slot:heroImg>
<img :src="hero.imgUrl" />
</template>
</Hero>
<!-- This is where all yours pages will be -->
<nuxt />
<Footer />
</div>
</template>
<script>
// Import Header component
import Header from '~/components/Header.vue'
import Footer from '~/components/Footer.vue'
import Hero from '~/components/Hero.vue'
import { mapGetters } from 'vuex'
export default {
data(){
return {
//declaring hero Obj to contain hero data
hero: {
title: "",
subtitle: "",
imgUrl: ""
}
}
},
components: {
Header,
Footer,
Hero
},
//Getting getHero getter from hero.js and saving it to newHero
computed: mapGetters({
newHero: 'hero/getHero'
}),
//watching newHero to change and then updating this.hero Obj. This action will update the displayed data
watch: {
newHero: function (obj) {
this.hero = {...this.hero, ...obj}
}
}
}
</script>
Here I declare the variables and store than into Vuex Store:
<template>
...
</template>
<script>
export default {
data() {
return {
hero: {
title: "Awesome Static title",
subtitle: "Awesome static subtitle"
}
}
},
//Saving the declared Hero to Vuex Store, then my default.vue will be able to get it through this.$store.getters
mounted() {
this.$store.commit("hero/setData", this.hero);
},
}
</script>
At some pages the title are fetched from the database (GraphQL using Apollo). Then I did:
<template>
...
</template>
<script>
import getLojaInfo from '~/apollo/queries/loja/loja.gql'
export default {
//declaring data
data() {
return {
lojas: Array,
loading: 0,
hero: {
title: "",
subtitle: "",
imgUrl: ""
}
}
},
//making the query
apollo: {
lojas: {
$loadingKey: 'loading',
prefetch: true,
query: getLojaInfo,
variables () {
return { slug: this.$route.params.singleLoja }
},
//it will wait for query result that and then populate this hero, it will update the hero title, subtitle and image
result(ApolloQueryResult, key) {
this.hero.title = ApolloQueryResult.data.lojas[0].name
this.hero.subtitle = ApolloQueryResult.data.lojas[0].description
this.hero.imgUrl = ApolloQueryResult.data.lojas[0].logo.url
//then commit it to Vuex Store
this.$store.commit("hero/setData", this.hero);
}
},
},
}
</script>
Thank you all, I would appreciate contributions to my code.
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.
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
Trying to learn vuejs I got to the question how to pass any data from one component to all, using $emit but without using any #click.
It is possible some how that the data to be just available and grab it any time, without using the click?
Let's say we have this example with normal #click and $emit.
main.js
export const eventBus = new Vue()
Hello.vue
<template>
<div>
<h2>This is Hello component</h2>
<button
#click="emitGlobalClickEvent()">Click me</button>
</div>
</template>
<script>
import { eventBus } from '../main'
export default {
data () {
return {
msg: 'Welcome to Your Vue.js App'
}
},
methods: {
emitGlobalClickEvent () {
eventBus.$emit('messageSelected', this.msg)
}
}
}
</script>
User.vue
<template>
<div>
<h2>This is User component</h2>
<user-one></user-one>
</div>
</template>
<script>
import { eventBus } from '../main'
import UserOne from './UserOne.vue'
export default {
created () {
eventBus.$on('messageSelected', msg => {
console.log(msg)
})
},
components: {
UserOne
}
}
</script>
UserOne.vue
<template>
<div>
<h3>We are in UserOne component</h3>
</div>
</template>
<script>
import { eventBus } from '../main'
export default {
created () {
eventBus.$on('messageSelected', msg => {
console.log('From UserOne message !!!')
})
}
}
</script>
I want to get this message : Welcome to Your Vue.js App from Hello.vue in all components, but without #click, if is possible.
You can create another Javascript file which holds an Object with your initial state. Similar to how you define data in your components.
In this file your export your Object and import it in all Components which need access to this shared state. Something along the lines of this:
import Store from 'store';
data() {
return {
store
}
}
This might help:
https://v2.vuejs.org/v2/guide/state-management.html
At this point if you app grows even more in complexity you might also start checking out Vuex which helps to keep track of changes(mutations) inside of your store.
The given example is essential a very oversimplified version of Vuex.