Access page data on component - vue.js

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.

Related

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

VueJS 2.x Child-Component doesn't react to changed parent-property

I have the problem, that a component doesn't recognize the change of a property.
The component is nested about 5 levels deep. Every component above the faulty one does update with the same mechanics and flawlessly.
I invested some time to get to the problem, but I can't find it.
The flow is:
Dashboard (change value and pass as prop)
TicketPreview (Usage and
pass prop)
CommentSection (Pass prop)
CommentList (FAULTY / Usage of prop)
Everything down to the commentSection is being updated as expected, but the commentList doesn't get the update notification (beforeUpdate doesn't get triggered).
Since I tested quite a few things I will only post the essential code from commentSection (parent) and commenList (child)
DISCLAIMER: This is a prototype code without backend, therefore typical API-Requests are solved with the localStorage of the users browser.
commentSection
<template>
<div id="comment-section">
<p>{{selectedTicket.title}}</p>
<comment-form :selectedTicket="selectedTicket" />
<comment-list :selectedTicket="selectedTicket" />
</div>
</template>
<script>
import CommentForm from "#/components/comment-section/CommentForm";
import CommentList from "#/components/comment-section/CommentList";
export default {
name: "CommentSection",
components: {
CommentForm,
CommentList,
},
props: {
selectedTicket: Object,
},
beforeUpdate() {
console.log("Comment Section");
console.log(this.selectedTicket);
},
updated() {
console.log("Comment Section is updated");
}
}
</script>
CommentList
<template>
<div id="comment-list">
<comment-item
v-for="comment in comments"
:key="comment.id"
:comment="comment"
/>
</div>
</template>
<script>
import CommentItem from "#/components/comment-section/CommentItem";
export default {
name: "CommentList",
components: {
CommentItem,
},
data() {
return {
comments: Array,
}
},
props: {
selectedTicket: Object,
},
methods: {
getComments() {
let comments = JSON.parse(window.localStorage.getItem("comments"));
let filteredComments = [];
for(let i = 0; i < comments.length; i++){
if (comments[i].ticketId === this.selectedTicket.id){
filteredComments.push(comments[i]);
}
}
this.comments = filteredComments;
}
},
beforeUpdate() {
console.log("CommentList");
console.log(this.selectedTicket);
this.getComments();
},
mounted() {
this.$root.$on("updateComments", () => {
this.getComments();
});
console.log("CL Mounted");
},
}
</script>
The beforeUpdate() and updated() hooks from the commentList component are not being fired.
I guess I could work around it with an event passing the data, but for the sake of understanding, let's pretend it's not a viable option right now.
It would be better to use a watcher, this will be more simple.
Instead of method to set comments by filtering you can use computed property which is reactive and no need to watch for props updates.
CommentSection
<template>
<div id="comment-section">
<p>{{ selectedTicket.title }}</p>
<comment-form :selectedTicket="selectedTicket" />
<comment-list :selectedTicket="selectedTicket" />
</div>
</template>
<script>
import CommentForm from "#/components/comment-section/CommentForm";
import CommentList from "#/components/comment-section/CommentList";
export default {
name: "CommentSection",
components: {
CommentForm,
CommentList
},
props: {
selectedTicket: Object
},
methods: {
updateTicket() {
console.log("Comment section is updated");
console.log(this.selectedTicket);
}
},
watch: {
selectedTicket: {
immediate: true,
handler: "updateTicket"
}
}
};
</script>
CommentList
<template>
<div id="comment-list">
<comment-item
v-for="comment in comments"
:key="comment.id"
:comment="comment"
/>
</div>
</template>
<script>
import CommentItem from "#/components/comment-section/CommentItem";
export default {
name: "CommentList",
components: {
CommentItem
},
props: {
selectedTicket: Object
},
computed: {
comments() {
let comments = JSON.parse(window.localStorage.getItem("comments"));
let filteredComments = [];
for (let comment of comments) {
if (comment.ticketId == this.selectedTicket.id) {
filteredComments.push(comment);
}
}
// // using es6 Array.filter()
// let filteredComments = comments.filter(
// (comment) => comment.ticketId == this.selectedTicket.id
// );
return filteredComments;
}
}
};
</script>
I found the problem: Since commentList is only a wrapper that doesn't use any of the values from the prop, the hooks for beforeUpdate and updated are never triggered. The Vue Instance Chart is misleading in that regard. The diagram shows it like beforeUpdate would ALWAYS fire, when the data changed (then re-render, then updated), but beforeUpdate only fires if the Component and Parent has to be re-rendered.
The Object updates as expected, it just never triggered a re-render on the child component because the wrapper has not been re-rendered.

How can I pass a variable value from a "page" to "layout" in Nuxt JS?

I'm a beginner in VUE and donnow this one is the correct syntax. I need the variable {{name}} to be set from a page. Which means I need to change the value of the variable page to page. How can I achieve that? Help me guys.
My "Layout" Code is like below -
<template>
<div class="login-page">
<div class="col1">{{ name }}</div>
<div class="col2">
<div class="content-box">
<nuxt />
</div>
</div>
</div>
</template>
<script>
export default {
props: ['name']
}
</script>
And my "Page" code is following -
<template>
<div>Welcome</div>
</template>
<script>
export default {
layout: 'login',
data: function() {
return {
name: 'Victor'
}
}
}
</script>
this can be achieved by using the vuex module. The layout have access to the vuex store, so once a page is open, you can call a mutation to set the page name and listen the name state in the layout component.
First the Vuex module, we can add a module by creating a file in the store folder,
in this case we are creating the page module:
// page.js file in the store folder
const state = {
name: ''
}
const mutations = {
setName(state, name) {
state.name = name
}
}
const getters = {
getName: (state) => state.name
}
export default {
state,
mutations,
getters
}
Now we can use the setPageName mutation to set the pageName value once a page reach the created hook (also can be the mounted hook):
// Page.vue page
<template>
<div>Welcome</div>
</template>
<script>
export default {
layout: 'login',
created() {
this.$store.commit('page/setName', 'Hello')
},
}
</script>
And in the layout component we have the computed property pageName (or name if we want):
<template>
<div class="login-page">
<div class="col1">{{ name }}</div>
<div class="col2">
<div class="content-box">
<nuxt />
</div>
</div>
</div>
</template>
<script>
export default {
computed: {
name() {
return this.$store.getters['page/getName']
}
}
}
</script>
And it's done!
Answer to your question in the commets:
The idea behind modules is keep the related information to some functionality in one place. I.e Let's say you want to have name, title and subtitle for each page, so the page module state variable will be:
const state = { name: '', title: '', subtitle: ''}
Each variable can be updated with a mutation, declaring:
const mutations = {
setName(state, name) {
state.name = name
},
setPageTitle(state, title) {
state.title = title
},
setPageSubtitle(state, subtitle) {
state.subtitle = subtitle
},
}
And their values can be updated from any page with:
this.$store.commit('page/setPageTitle', 'A page title')
The same if you want to read the value:
computed: {
title() {
// you can get the variable state without a getter
// ['page'] is the module name, nuxt create the module name
// using the file name page.js
return this.$store.state['page'].title
}
}
The getters are good for format or filter information.
A new module can be added anytime if required, the idea behind vuex and the modules is to have a place with the information that is required in many places through the application, in one place. I.e. the application theme information, if the user select the light or dark theme, maybe the colors can be changed. You can read more about vuex with nuxt here: https://nuxtjs.org/guide/vuex-store/ and https://vuex.vuejs.org/

Nuxtjs: Best way to show data in page header

I started to use Nuxt.js for SSR purposes. In the header I have a navigation menu which it's items should be requested from server side.
Here is default.vue
<template>
<div>
<app-header></app-header>
<nuxt />
<app-footer></app-footer>
</div>
</template>
<script>
import Header from '~/components/Header.vue'
import Footer from '~/components/Footer.vue'
export default {
components: {
'app-header': Header,
'app-footer': Footer,
}
}
</script>
So, what is the best way to fetch items and prevent sending request on every single page?
I thought of using Vuex to store this data. But I don't if it's a good solution or not.
You can use Vuex and declare your navbar items inside the store.
state: {
navbarItems: []
},
mutations: {
FETCH_ITEMS(state, navbarItems) {
state.navbarItems = navbarItems
}
},
actions: {
fetchItems({ commit }) {
Vue.http.get("your api here ") // or axios
.then((response) => {
commit("FETCH_ITEMS", response.body);
})
.catch((error => {
console.log("error")
}))
}
And inside your header
created() {
this.$store.dispatch("fetchItems")
}
Thanks

Vuex - changing value based on component's current index and passing the value to other components

I have hit a wall and cannot get over it without your help now. I've spent a good few days trying to get my head around mutations and actions but this particular case I have doesn't seem to apply to tutorials online, also the answers on here are for different scenarios to mine. So here goes:
Setup:
- Project is using vuex and store for data and state management
- Currently App.vue has two child components: PizzaGenerator.vue and BaseButtons.vue
I am trying to achieve this:
- When I click on a specific button in BaseButtons.vue I need a centrally managed showBaseIndex to be assigned an index value. This value is then available to the other, PizzaGenerator.vue, component which will reflect the change and show a layer that matches the new value of showBaseIndex.
Please see all the two components and store below.
Can you help me head in the right direction?
PizzaGenerator.vue
<template>
<div class="pizza-generator section" id="screen3" data-anchor="screenThree">
<ul class="pizza-layers">
<!-- Display pizzas -->
<li v-for="(item, index) in getBase" class="pizza-canvas pizza-canvas--base" v-bind:class="item.class" v-if="$store.state.showBaseIndex == index"></li>
<!-- END Display pizzas -->
</ul>
<div class="pizza-items">
<app-base-buttons></app-base-buttons>
</div>
</div>
</template>
<script>
import Base from './pizza-buttons/BaseButtons'
import { mapGetters, mapActions } from 'vuex'
export default {
components: {
appBaseButtons: Base
},
computed: {
getBase(){
return this.$store.state.base
}
},
methods: mapActions([
'baseLayer',
]),
}
</script>
BaseButtons.vue
<div class="pizza-items-selector pizza-items-selector--dough">
<div class="pizza-items-selector__items pizza-items-selector__items--dough">
<div class="sliding-buttons">
<button v-for="(item, index) in getBase" class="sliding-buttons__button dough-button" :key="index" #click="baseLayer = index"> {{ item.name }}</button>
</div>
<button class="buttons-prev-1 prev">prev</button>
<button class="buttons-next-1 next">next</button>
</div>
</div>
<script>
import { mapActions, mapMutations } from 'vuex'
export default {
computed:{
getBase(){
return this.$store.state.base
},
},
methods:{
...mapMutations([
'baseLayer',
]),
baseLayerIndex() {
this.$store.commit('baseLayer');
}
},
}
store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export const store = new Vuex.Store({
state: {
showBaseIndex: 1,
base : [
{ name: 'Dünner Italienisch', class: 'pizza-canvas--base-italienisch', id: 1 },
{ name: 'Dünner Italienisch - Vollkorn', class: 'pizza-canvas--base-italienisch--vollkorn', id: 2 },
{ name: 'Dicker Amerikanisch', class: 'pizza-canvas--base-amerikanisch', id: 3 },
{ name: 'Dicker Amerikanisch / Chili-Käse-Rand', class: 'pizza-canvas--base-amerikanisch--chilli-kaese-rand', id: 4 },
{ name: 'Dicker Amerikanisch / Käse-Rand', class: 'pizza-canvas--base-amerikanisch--kaese-rand', id: 5 }
],
},
getters: {
//
},
mutations: {
baseLayer (state){
state.showBaseIndex
}
},
});
export default store;
Mutations are functions, not simple values. You should check the Vuex guide about mutations, they are quite straightforward.
What you should do is declaring the given mutation in this way, so it will also accept a parameter:
mutations: {
baseLayer (state, id){
state.showBaseIndex = id;
}
},
and commit the mutation properly in the component:
methods:{
...mapMutations([
'baseLayer',
]),
baseLayerIndex(index) { // call this at #click on button, giving the index as parameter
this.$store.commit('baseLayer', index);
}
}
This will set the desired index in the store, and from that on you could get the current base from store using vuex getters like:
getters: {
getSelectedBase(state){
return state.base.find(base=>{return base.id === state.showBaseIndex});
}
},